Skip to content

Commit

Permalink
feat(markdown-docx): add clause transformer - #397 (#432)
Browse files Browse the repository at this point in the history
Signed-off-by: k-kumar-01 <[email protected]>
  • Loading branch information
K-Kumar-01 authored Aug 4, 2021
1 parent 1c569bf commit c26a49c
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid wp14">
<w:body>

<w:sdt>
<w:sdtPr>
<w:lock w:val="contentLocked" />
<w:alias w:val="Template"/>
</w:sdtPr>
<w:sdtContent>

<w:p>

<w:pPr>
Expand Down Expand Up @@ -510,6 +517,9 @@
</w:r>
</w:p>

</w:sdtContent>
</w:sdt>

<w:p/>
</w:body>
</w:document>
Expand Down
76 changes: 64 additions & 12 deletions packages/markdown-docx/src/ToCiceroMarkVisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,25 @@ class ToCiceroMarkVisitor {
}
}

/**
* Gets the node type based on the color property.
*
* @param {Array} properties the variable elements
* @returns {string} the type of the node
*/
getNodeType(properties) {
let nodeType = TRANSFORMED_NODES.variable;
for (const property of properties) {
if (property.name === 'w15:color') {
// eg. "Shipper1 | org.accordproject.organization.Organization"
if (property.attributes['w:val'] === '99CCFF') {
nodeType = TRANSFORMED_NODES.clause;
}
}
}
return nodeType;
}

/**
* Checks if the node is a thematic break or not
*
Expand Down Expand Up @@ -202,9 +221,11 @@ class ToCiceroMarkVisitor {
/**
* Generates all nodes present in a block element( paragraph, heading ).
*
* @param {object} rootNode Block node like paragraph, heading, etc.
* @param {object} rootNode Block node like paragraph, heading, etc.
* @param {boolean} returnConstructedNode return the constructed node if true else appends it to nodes array
* @returns {*} Node if returnConstructedNode else None
*/
generateNodes(rootNode) {
generateNodes(rootNode, returnConstructedNode = false) {
if (this.JSONXML.length > 0) {
let constructedNode;
constructedNode = this.constructCiceroMarkNodeJSON(this.JSONXML[0]);
Expand Down Expand Up @@ -250,7 +271,11 @@ class ToCiceroMarkVisitor {
}
}
this.JSONXML = [];
this.nodes = [...this.nodes, rootNode];
if (returnConstructedNode) {
return rootNode;
} else {
this.nodes = [...this.nodes, rootNode];
}
}
}

Expand Down Expand Up @@ -313,9 +338,12 @@ class ToCiceroMarkVisitor {
* Traverses the JSON object of XML elements in DFS approach.
*
* @param {object} node Node object to be traversed
* @param {object} parent Parent node name
* @param {string} parent Parent node name
* @returns {*} GeneratedNode if parent is of type clause else none
*/
traverseElements(node, parent = '') {
// Contains node present in a codeblock or blockquote, etc.
let blockNodes = [];
for (const subNode of node) {
if (subNode.name === 'w:p') {
if (!subNode.elements) {
Expand Down Expand Up @@ -349,8 +377,12 @@ class ToCiceroMarkVisitor {
const thematicBreakNode = {
$class: TRANSFORMED_NODES.thematicBreak,
};
this.nodes = [...this.nodes, thematicBreakNode];
continue;
if (parent === TRANSFORMED_NODES.clause) {
blockNodes = [...blockNodes, thematicBreakNode];
} else {
this.nodes = [...this.nodes, thematicBreakNode];
continue;
}
}

this.traverseElements(subNode.elements);
Expand All @@ -361,13 +393,21 @@ class ToCiceroMarkVisitor {
level,
nodes: [],
};
this.generateNodes(headingNode);
if (parent === TRANSFORMED_NODES.clause) {
blockNodes = [...blockNodes, this.generateNodes(headingNode, true)];
} else {
this.generateNodes(headingNode);
}
} else {
let paragraphNode = {
$class: TRANSFORMED_NODES.paragraph,
nodes: [],
};
this.generateNodes(paragraphNode);
if (parent === TRANSFORMED_NODES.clause) {
blockNodes = [...blockNodes, this.generateNodes(paragraphNode, true)];
} else {
this.generateNodes(paragraphNode);
}
}
} else if (subNode.name === 'w:sdt') {
// denotes the whole template if parent is body
Expand All @@ -376,7 +416,6 @@ class ToCiceroMarkVisitor {
} else {
let nodeInformation = {
properties: [],
value: '',
nodeType: TRANSFORMED_NODES.variable,
name: null,
elementType: null,
Expand All @@ -385,11 +424,23 @@ class ToCiceroMarkVisitor {
if (variableSubNodes.name === 'w:sdtPr') {
nodeInformation.name = this.getName(variableSubNodes.elements);
nodeInformation.elementType = this.getElementType(variableSubNodes.elements);
nodeInformation.nodeType = this.getNodeType(variableSubNodes.elements);
}
if (variableSubNodes.name === 'w:sdtContent') {
for (const variableContentNodes of variableSubNodes.elements) {
if (variableContentNodes.name === 'w:r') {
this.fetchFormattingProperties(variableContentNodes, nodeInformation);
if (nodeInformation.nodeType === TRANSFORMED_NODES.clause) {
const nodes = this.traverseElements(variableSubNodes.elements, TRANSFORMED_NODES.clause);
const clauseNode = {
$class: TRANSFORMED_NODES.clause,
elementType: nodeInformation.elementType,
name: nodeInformation.name,
nodes,
};
this.nodes = [...this.nodes, clauseNode];
} else {
for (const variableContentNodes of variableSubNodes.elements) {
if (variableContentNodes.name === 'w:r') {
this.fetchFormattingProperties(variableContentNodes, nodeInformation);
}
}
}
}
Expand All @@ -400,6 +451,7 @@ class ToCiceroMarkVisitor {
this.fetchFormattingProperties(subNode, nodeInformation);
}
}
return blockNodes;
}

/**
Expand Down
23 changes: 21 additions & 2 deletions packages/markdown-docx/src/ToOOXMLVisitor/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,33 @@ function titleGenerator(title, type) {
return `${title} | ${type}`;
}

/**
* Wraps the OOXML in locked w:sdt tags to prevent content editing.
*
* @param {string} ooxml OOXML string to be wrapped
* @returns {string} OOXML wrapped in locked content controls
*/
function wrapAroundLockedContentControls(ooxml) {
return `
<w:sdt>
<w:sdtPr>
<w:lock w:val="contentLocked" />
<w:alias w:val="Template"/>
</w:sdtPr>
<w:sdtContent>
${ooxml}
</w:sdtContent>
</w:sdt>
`;
}

/**
* Wraps OOXML in docx headers.
*
* @param {string} ooxml OOXML to be wrapped
* @returns {string} OOXML wraped in docx headers
*/
function wrapAroundDefaultDocxTags(ooxml) {

const HEADING_STYLE_SPEC = `
<pkg:part pkg:name="/word/styles.xml" pkg:contentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml">
<pkg:xmlData>
Expand Down Expand Up @@ -264,4 +283,4 @@ function wrapAroundDefaultDocxTags(ooxml) {
return ooxml;
}

module.exports = { sanitizeHtmlChars, titleGenerator, wrapAroundDefaultDocxTags };
module.exports = { sanitizeHtmlChars, titleGenerator, wrapAroundDefaultDocxTags, wrapAroundLockedContentControls };
58 changes: 57 additions & 1 deletion packages/markdown-docx/src/ToOOXMLVisitor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ const {
THEMATICBREAK_RULE,
CODEBLOCK_PROPERTIES_RULE,
CODEBLOCK_FONTPROPERTIES_RULE,
CLAUSE_RULE,
} = require('./rules');
const { wrapAroundDefaultDocxTags } = require('./helpers');
const { wrapAroundDefaultDocxTags, wrapAroundLockedContentControls } = require('./helpers');
const { TRANSFORMED_NODES } = require('../constants');

/**
Expand Down Expand Up @@ -140,6 +141,60 @@ class ToOOXMLVisitor {
this.tags = [...this.tags, SOFTBREAK_RULE()];
} else if (this.getClass(subNode) === TRANSFORMED_NODES.thematicBreak) {
this.globalOOXML += THEMATICBREAK_RULE();
} else if (this.getClass(subNode) === TRANSFORMED_NODES.clause) {
let clauseOOXML = '';
if (subNode.nodes) {
for (const deepNode of subNode.nodes) {
if (this.getClass(deepNode) === TRANSFORMED_NODES.paragraph) {
this.traverseNodes(deepNode.nodes, properties);
let ooxml = '';
for (let xmlTag of this.tags) {
ooxml += xmlTag;
}
ooxml = PARAGRAPH_RULE(ooxml);
clauseOOXML += ooxml;

// Clear all the tags as all nodes of paragraph have been traversed.
this.tags = [];
} else if (this.getClass(deepNode) === TRANSFORMED_NODES.heading) {
this.traverseNodes(deepNode.nodes, properties);
let ooxml = '';
for (let xmlTag of this.tags) {
let headingPropertiesTag = '';
headingPropertiesTag = HEADING_PROPERTIES_RULE(deepNode.level);
ooxml += headingPropertiesTag;
ooxml += xmlTag;
}

// in DOCX heading is a paragraph with some styling tags present
ooxml = PARAGRAPH_RULE(ooxml);
clauseOOXML += ooxml;

this.tags = [];
} else {
let newProperties = [...properties, deepNode.$class];
this.traverseNodes(deepNode.nodes, newProperties);
}
}
const tag = subNode.name;
const type = subNode.elementType;
if (Object.prototype.hasOwnProperty.call(this.counter, tag)) {
this.counter = {
...this.counter,
[tag]: {
...this.counter[tag],
count: ++this.counter[tag].count,
},
};
} else {
this.counter[tag] = {
count: 1,
type,
};
}
const title = `${tag.toUpperCase()[0]}${tag.substring(1)}${this.counter[tag].count}`;
this.globalOOXML += CLAUSE_RULE(title, tag, type, clauseOOXML);
}
} else {
if (subNode.nodes) {
if (this.getClass(subNode) === TRANSFORMED_NODES.paragraph) {
Expand Down Expand Up @@ -186,6 +241,7 @@ class ToOOXMLVisitor {
*/
toOOXML(ciceromark) {
this.traverseNodes(ciceromark, []);
this.globalOOXML = wrapAroundLockedContentControls(this.globalOOXML);
this.globalOOXML = wrapAroundDefaultDocxTags(this.globalOOXML);

return this.globalOOXML;
Expand Down
26 changes: 26 additions & 0 deletions packages/markdown-docx/src/ToOOXMLVisitor/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,31 @@ const THEMATICBREAK_RULE = () => {
`;
};

/**
* Inserts a clause object in OOXML syntax
*
* @param {string} title Title of the clause
* @param {string} tag Tag of the clause
* @param {string} type Type of the clause
* @param {string} content Content of the clause
* @returns {string} OOXML for the clause
*/
const CLAUSE_RULE = (title, tag, type, content) => {
return `
<w:sdt>
<w:sdtPr>
<w:lock w:val="contentLocked"/>
<w15:color w:val="99CCFF"/>
<w:alias w:val="${titleGenerator(title, type)}"/>
<w:tag w:val="${tag}"/>
</w:sdtPr>
<w:sdtContent>
${content}
</w:sdtContent>
</w:sdt>
`;
};

module.exports = {
TEXT_RULE,
EMPHASIS_RULE,
Expand All @@ -218,4 +243,5 @@ module.exports = {
CODEBLOCK_PROPERTIES_RULE,
CODEBLOCK_FONTPROPERTIES_RULE,
THEMATICBREAK_RULE,
CLAUSE_RULE,
};
1 change: 1 addition & 0 deletions packages/markdown-docx/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const TRANSFORMED_NODES = {
text: `${NS_PREFIX_CommonMarkModel}Text`,
thematicBreak: `${NS_PREFIX_CommonMarkModel}ThematicBreak`,
variable: `${NS_PREFIX_CiceroMarkModel}Variable`,
clause:`${NS_PREFIX_CiceroMarkModel}Clause`
};

module.exports = { TRANSFORMED_NODES };
1 change: 1 addition & 0 deletions packages/markdown-docx/test/data/ciceroMark/clause.json
Original file line number Diff line number Diff line change
@@ -0,0 +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"}]}]}
Loading

0 comments on commit c26a49c

Please sign in to comment.