Skip to content

Commit 6388eca

Browse files
committed
feat(markdown-docx): add clause transformer - #397
transformation logic(OOXML<->CiceroMark) rules for clause tests Signed-off-by: k-kumar-01 <[email protected]>
1 parent 2b617d1 commit 6388eca

File tree

6 files changed

+184
-16
lines changed

6 files changed

+184
-16
lines changed

packages/markdown-docx/src/ToCiceroMarkVisitor.js

+64-12
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,25 @@ class ToCiceroMarkVisitor {
8383
}
8484
}
8585

86+
/**
87+
* Gets the node type based on the color property.
88+
*
89+
* @param {Array} properties the variable elements
90+
* @returns {string} the type of the node
91+
*/
92+
getNodeType(properties) {
93+
let nodeType = TRANSFORMED_NODES.variable;
94+
for (const property of properties) {
95+
if (property.name === 'w15:color') {
96+
// eg. "Shipper1 | org.accordproject.organization.Organization"
97+
if (property.attributes['w:val'] === '99CCFF') {
98+
nodeType = TRANSFORMED_NODES.clause;
99+
}
100+
}
101+
}
102+
return nodeType;
103+
}
104+
86105
/**
87106
* Checks if the node is a thematic break or not
88107
*
@@ -158,9 +177,11 @@ class ToCiceroMarkVisitor {
158177
/**
159178
* Generates all nodes present in a block element( paragraph, heading ).
160179
*
161-
* @param {object} rootNode Block node like paragraph, heading, etc.
180+
* @param {object} rootNode Block node like paragraph, heading, etc.
181+
* @param {boolean} returnConstructedNode return the constructed node if true else appends it to nodes array
182+
* @returns {*} Node if returnConstructedNode else None
162183
*/
163-
generateNodes(rootNode) {
184+
generateNodes(rootNode, returnConstructedNode = false) {
164185
if (this.JSONXML.length > 0) {
165186
let constructedNode;
166187
constructedNode = this.constructCiceroMarkNodeJSON(this.JSONXML[0]);
@@ -206,7 +227,11 @@ class ToCiceroMarkVisitor {
206227
}
207228
}
208229
this.JSONXML = [];
209-
this.nodes = [...this.nodes, rootNode];
230+
if (returnConstructedNode) {
231+
return rootNode;
232+
} else {
233+
this.nodes = [...this.nodes, rootNode];
234+
}
210235
}
211236
}
212237

@@ -258,9 +283,12 @@ class ToCiceroMarkVisitor {
258283
* Traverses the JSON object of XML elements in DFS approach.
259284
*
260285
* @param {object} node Node object to be traversed
261-
* @param {object} parent Parent node name
286+
* @param {string} parent Parent node name
287+
* @returns {*} GeneratedNode if parent is of type clause else none
262288
*/
263289
traverseElements(node, parent = '') {
290+
// Contains node present in a codeblock or blockquote, etc.
291+
let blockNodes = [];
264292
for (const subNode of node) {
265293
if (subNode.name === 'w:p') {
266294
if (!subNode.elements) {
@@ -277,8 +305,12 @@ class ToCiceroMarkVisitor {
277305
const thematicBreakNode = {
278306
$class: TRANSFORMED_NODES.thematicBreak,
279307
};
280-
this.nodes = [...this.nodes, thematicBreakNode];
281-
continue;
308+
if (parent === TRANSFORMED_NODES.clause) {
309+
blockNodes = [...blockNodes, thematicBreakNode];
310+
} else {
311+
this.nodes = [...this.nodes, thematicBreakNode];
312+
continue;
313+
}
282314
}
283315

284316
this.traverseElements(subNode.elements);
@@ -289,13 +321,21 @@ class ToCiceroMarkVisitor {
289321
level,
290322
nodes: [],
291323
};
292-
this.generateNodes(headingNode);
324+
if (parent === TRANSFORMED_NODES.clause) {
325+
blockNodes = [...blockNodes, this.generateNodes(headingNode, true)];
326+
} else {
327+
this.generateNodes(headingNode);
328+
}
293329
} else {
294330
let paragraphNode = {
295331
$class: TRANSFORMED_NODES.paragraph,
296332
nodes: [],
297333
};
298-
this.generateNodes(paragraphNode);
334+
if (parent === TRANSFORMED_NODES.clause) {
335+
blockNodes = [...blockNodes, this.generateNodes(paragraphNode, true)];
336+
} else {
337+
this.generateNodes(paragraphNode);
338+
}
299339
}
300340
} else if (subNode.name === 'w:sdt') {
301341
// denotes the whole template if parent is body
@@ -304,7 +344,6 @@ class ToCiceroMarkVisitor {
304344
} else {
305345
let nodeInformation = {
306346
properties: [],
307-
value: '',
308347
nodeType: TRANSFORMED_NODES.variable,
309348
name: null,
310349
elementType: null,
@@ -313,11 +352,23 @@ class ToCiceroMarkVisitor {
313352
if (variableSubNodes.name === 'w:sdtPr') {
314353
nodeInformation.name = this.getName(variableSubNodes.elements);
315354
nodeInformation.elementType = this.getElementType(variableSubNodes.elements);
355+
nodeInformation.nodeType = this.getNodeType(variableSubNodes.elements);
316356
}
317357
if (variableSubNodes.name === 'w:sdtContent') {
318-
for (const variableContentNodes of variableSubNodes.elements) {
319-
if (variableContentNodes.name === 'w:r') {
320-
this.fetchFormattingProperties(variableContentNodes, nodeInformation);
358+
if (nodeInformation.nodeType === TRANSFORMED_NODES.clause) {
359+
const nodes = this.traverseElements(variableSubNodes.elements, TRANSFORMED_NODES.clause);
360+
const clauseNode = {
361+
$class: TRANSFORMED_NODES.clause,
362+
elementType: nodeInformation.elementType,
363+
name: nodeInformation.name,
364+
nodes,
365+
};
366+
this.nodes = [...this.nodes, clauseNode];
367+
} else {
368+
for (const variableContentNodes of variableSubNodes.elements) {
369+
if (variableContentNodes.name === 'w:r') {
370+
this.fetchFormattingProperties(variableContentNodes, nodeInformation);
371+
}
321372
}
322373
}
323374
}
@@ -328,6 +379,7 @@ class ToCiceroMarkVisitor {
328379
this.fetchFormattingProperties(subNode, nodeInformation);
329380
}
330381
}
382+
return blockNodes;
331383
}
332384

333385
/**

packages/markdown-docx/src/ToOOXMLVisitor/helpers.js

+34-2
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,46 @@ function titleGenerator(title, type) {
3535
return `${title} | ${type}`;
3636
}
3737

38+
/**
39+
* Generates a radom string
40+
*
41+
* @returns {string} ID generated
42+
*/
43+
function generateRandomId() {
44+
let id = '';
45+
for (let i = 0; i < 25; i++) {
46+
id += Math.floor(Math.random() * 10);
47+
}
48+
return id;
49+
}
50+
51+
/**
52+
* Wraps the OOXML in locked content controls
53+
*
54+
* @param {string} ooxml OOXML string to be wrapped
55+
* @returns {string} OOXML wrapped in locked content controls
56+
*/
57+
function wrapAroundLockedContentControls(ooxml) {
58+
return `
59+
<w:sdt>
60+
<w:sdtPr>
61+
<w:lock w:val="contentLocked" />
62+
<w:alias w:val="${generateRandomId()}"/>
63+
</w:sdtPr>
64+
<w:sdtContent>
65+
${ooxml}
66+
</w:sdtContent>
67+
</w:sdt>
68+
`;
69+
}
70+
3871
/**
3972
* Wraps OOXML in docx headers.
4073
*
4174
* @param {string} ooxml OOXML to be wrapped
4275
* @returns {string} OOXML wraped in docx headers
4376
*/
4477
function wrapAroundDefaultDocxTags(ooxml) {
45-
4678
const HEADING_STYLE_SPEC = `
4779
<pkg:part pkg:name="/word/styles.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml">
4880
<pkg:xmlData>
@@ -264,4 +296,4 @@ function wrapAroundDefaultDocxTags(ooxml) {
264296
return ooxml;
265297
}
266298

267-
module.exports = { sanitizeHtmlChars, titleGenerator, wrapAroundDefaultDocxTags };
299+
module.exports = { sanitizeHtmlChars, titleGenerator, wrapAroundDefaultDocxTags, wrapAroundLockedContentControls };

packages/markdown-docx/src/ToOOXMLVisitor/index.js

+58-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ const {
2626
STRONG_RULE,
2727
CODE_PROPERTIES_RULE,
2828
THEMATICBREAK_RULE,
29+
CLAUSE_RULE,
2930
} = require('./rules');
30-
const { wrapAroundDefaultDocxTags } = require('./helpers');
31+
const { wrapAroundDefaultDocxTags, wrapAroundLockedContentControls } = require('./helpers');
3132
const { TRANSFORMED_NODES } = require('../constants');
3233

3334
/**
@@ -122,8 +123,62 @@ class ToOOXMLVisitor {
122123
this.tags = [...this.tags, VARIABLE_RULE(title, tag, value, type)];
123124
} else if (this.getClass(subNode) === TRANSFORMED_NODES.softbreak) {
124125
this.tags = [...this.tags, SOFTBREAK_RULE()];
125-
} else if(this.getClass(subNode) === TRANSFORMED_NODES.thematicBreak){
126+
} else if (this.getClass(subNode) === TRANSFORMED_NODES.thematicBreak) {
126127
this.globalOOXML += THEMATICBREAK_RULE();
128+
} else if (this.getClass(subNode) === TRANSFORMED_NODES.clause) {
129+
let clauseOOXML = '';
130+
if (subNode.nodes) {
131+
for (const deepNode of subNode.nodes) {
132+
if (this.getClass(deepNode) === TRANSFORMED_NODES.paragraph) {
133+
this.traverseNodes(deepNode.nodes, properties);
134+
let ooxml = '';
135+
for (let xmlTag of this.tags) {
136+
ooxml += xmlTag;
137+
}
138+
ooxml = PARAGRAPH_RULE(ooxml);
139+
clauseOOXML += ooxml;
140+
141+
// Clear all the tags as all nodes of paragraph have been traversed.
142+
this.tags = [];
143+
} else if (this.getClass(deepNode) === TRANSFORMED_NODES.heading) {
144+
this.traverseNodes(deepNode.nodes, properties);
145+
let ooxml = '';
146+
for (let xmlTag of this.tags) {
147+
let headingPropertiesTag = '';
148+
headingPropertiesTag = HEADING_PROPERTIES_RULE(deepNode.level);
149+
ooxml += headingPropertiesTag;
150+
ooxml += xmlTag;
151+
}
152+
153+
// in DOCX heading is a paragraph with some styling tags present
154+
ooxml = PARAGRAPH_RULE(ooxml);
155+
clauseOOXML += ooxml;
156+
157+
this.tags = [];
158+
} else {
159+
let newProperties = [...properties, deepNode.$class];
160+
this.traverseNodes(deepNode.nodes, newProperties);
161+
}
162+
}
163+
const tag = subNode.name;
164+
const type = subNode.elementType;
165+
if (Object.prototype.hasOwnProperty.call(this.counter, tag)) {
166+
this.counter = {
167+
...this.counter,
168+
[tag]: {
169+
...this.counter[tag],
170+
count: ++this.counter[tag].count,
171+
},
172+
};
173+
} else {
174+
this.counter[tag] = {
175+
count: 1,
176+
type,
177+
};
178+
}
179+
const title = `${tag.toUpperCase()[0]}${tag.substring(1)}${this.counter[tag].count}`;
180+
this.globalOOXML += CLAUSE_RULE(title, tag, type, clauseOOXML);
181+
}
127182
} else {
128183
if (subNode.nodes) {
129184
if (this.getClass(subNode) === TRANSFORMED_NODES.paragraph) {
@@ -170,6 +225,7 @@ class ToOOXMLVisitor {
170225
*/
171226
toOOXML(ciceromark) {
172227
this.traverseNodes(ciceromark, []);
228+
this.globalOOXML = wrapAroundLockedContentControls(this.globalOOXML);
173229
this.globalOOXML = wrapAroundDefaultDocxTags(this.globalOOXML);
174230

175231
return this.globalOOXML;

packages/markdown-docx/src/ToOOXMLVisitor/rules.js

+26
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,31 @@ const THEMATICBREAK_RULE = () => {
186186
`;
187187
};
188188

189+
/**
190+
* Inserts a clause object in OOXML syntax
191+
*
192+
* @param {string} title Title of the clause
193+
* @param {string} tag Tag of the clause
194+
* @param {string} type Type of the clause
195+
* @param {string} content Content of the clause
196+
* @returns {string} OOXML for the clause
197+
*/
198+
const CLAUSE_RULE = (title, tag, type, content) => {
199+
return `
200+
<w:sdt>
201+
<w:sdtPr>
202+
<w:lock w:val="contentLocked"/>
203+
<w15:color w:val="99CCFF"/>
204+
<w:alias w:val="${titleGenerator(title, type)}"/>
205+
<w:tag w:val="${tag}"/>
206+
</w:sdtPr>
207+
<w:sdtContent>
208+
${content}
209+
</w:sdtContent>
210+
</w:sdt>
211+
`;
212+
};
213+
189214
module.exports = {
190215
TEXT_RULE,
191216
EMPHASIS_RULE,
@@ -198,4 +223,5 @@ module.exports = {
198223
STRONG_RULE,
199224
CODE_PROPERTIES_RULE,
200225
THEMATICBREAK_RULE,
226+
CLAUSE_RULE,
201227
};

packages/markdown-docx/src/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const TRANSFORMED_NODES = {
3030
text: `${NS_PREFIX_CommonMarkModel}Text`,
3131
thematicBreak: `${NS_PREFIX_CommonMarkModel}ThematicBreak`,
3232
variable: `${NS_PREFIX_CiceroMarkModel}Variable`,
33+
clause:`${NS_PREFIX_CiceroMarkModel}Clause`
3334
};
3435

3536
module.exports = { TRANSFORMED_NODES };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"$class":"org.accordproject.commonmark.Document","xmlns":"http://commonmark.org/xml/1.0","nodes":[{"$class":"org.accordproject.commonmark.Heading","level":"1","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Heading"}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"And below is a "},{"$class":"org.accordproject.commonmark.Strong","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"clause"}]},{"$class":"org.accordproject.commonmark.Text","text":"."}]},{"$class":"org.accordproject.ciceromark.Clause","name":"deliveryClause","elementType":"org.accordproject.acceptanceofdelivery.AcceptanceOfDeliveryClause","nodes":[{"$class":"org.accordproject.commonmark.Heading","level":"2","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Acceptance of Delivery."}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.ciceromark.Variable","value":"\"Party A\"","name":"shipper","elementType":"org.accordproject.organization.Organization"},{"$class":"org.accordproject.commonmark.Text","text":" will be deemed to have completed its delivery obligations"},{"$class":"org.accordproject.commonmark.Softbreak"},{"$class":"org.accordproject.commonmark.Text","text":"if in "},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Party B\"","name":"receiver","elementType":"org.accordproject.organization.Organization"},{"$class":"org.accordproject.commonmark.Text","text":"'s opinion, the "},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Widgets\"","name":"deliverable","elementType":"String"},{"$class":"org.accordproject.commonmark.Text","text":" satisfies the"},{"$class":"org.accordproject.commonmark.Softbreak"},{"$class":"org.accordproject.commonmark.Text","text":"Acceptance Criteria, and "},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Party B\"","name":"receiver","elementType":"org.accordproject.organization.Organization"},{"$class":"org.accordproject.commonmark.Text","text":" notifies "},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Party A\"","name":"shipper","elementType":"org.accordproject.organization.Organization"},{"$class":"org.accordproject.commonmark.Text","text":" in writing"},{"$class":"org.accordproject.commonmark.Softbreak"},{"$class":"org.accordproject.commonmark.Text","text":"that it is accepting the "},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Widgets\"","name":"deliverable","elementType":"String"},{"$class":"org.accordproject.commonmark.Text","text":"."}]},{"$class":"org.accordproject.commonmark.Heading","level":"2","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Inspection and Notice."}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.ciceromark.Variable","value":"\"Party B\"","name":"receiver","elementType":"org.accordproject.organization.Organization"},{"$class":"org.accordproject.commonmark.Text","text":" will have "},{"$class":"org.accordproject.ciceromark.Variable","value":"10","name":"businessDays","elementType":"Long"},{"$class":"org.accordproject.commonmark.Text","text":" Business Days to inspect and"},{"$class":"org.accordproject.commonmark.Softbreak"},{"$class":"org.accordproject.commonmark.Text","text":"evaluate the "},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Widgets\"","name":"deliverable","elementType":"String"},{"$class":"org.accordproject.commonmark.Text","text":" on the delivery date before notifying"},{"$class":"org.accordproject.commonmark.Softbreak"},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Party A\"","name":"shipper","elementType":"org.accordproject.organization.Organization"},{"$class":"org.accordproject.commonmark.Text","text":" that it is either accepting or rejecting the"},{"$class":"org.accordproject.commonmark.Softbreak"},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Widgets\"","name":"deliverable","elementType":"String"},{"$class":"org.accordproject.commonmark.Text","text":"."}]},{"$class":"org.accordproject.commonmark.Heading","level":"2","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Acceptance Criteria."}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"The \"Acceptance Criteria\" are the specifications the "},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Widgets\"","name":"deliverable","elementType":"String"},{"$class":"org.accordproject.commonmark.Softbreak"},{"$class":"org.accordproject.commonmark.Text","text":"must meet for the "},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Party A\"","name":"shipper","elementType":"org.accordproject.organization.Organization"},{"$class":"org.accordproject.commonmark.Text","text":" to comply with its requirements and"},{"$class":"org.accordproject.commonmark.Softbreak"},{"$class":"org.accordproject.commonmark.Text","text":"obligations under this agreement, detailed in "},{"$class":"org.accordproject.ciceromark.Variable","value":"\"Attachment X\"","name":"attachment","elementType":"String"},{"$class":"org.accordproject.commonmark.Text","text":", attached"},{"$class":"org.accordproject.commonmark.Softbreak"},{"$class":"org.accordproject.commonmark.Text","text":"to this agreement."}]}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"More text"}]}]}

0 commit comments

Comments
 (0)