diff --git a/packages/markdown-cli/test/data/acceptance/omitted-acceptance-of-delivery.xml b/packages/markdown-cli/test/data/acceptance/omitted-acceptance-of-delivery.xml index 9708a0ed..5c61ef8a 100644 --- a/packages/markdown-cli/test/data/acceptance/omitted-acceptance-of-delivery.xml +++ b/packages/markdown-cli/test/data/acceptance/omitted-acceptance-of-delivery.xml @@ -531,6 +531,7 @@ + @@ -684,6 +685,19 @@ + + + + + + + + + + + + + diff --git a/packages/markdown-docx/src/ToCiceroMarkVisitor.js b/packages/markdown-docx/src/ToCiceroMarkVisitor.js index 871cea40..ac5fac88 100644 --- a/packages/markdown-docx/src/ToCiceroMarkVisitor.js +++ b/packages/markdown-docx/src/ToCiceroMarkVisitor.js @@ -31,6 +31,9 @@ class ToCiceroMarkVisitor { // All the nodes generated from given OOXML this.nodes = []; + + // contains the realtionship part of a given OOXML + this.relationshipXML = []; } /** @@ -214,6 +217,15 @@ class ToCiceroMarkVisitor { $class: nodeInformation.properties[nodePropertyIndex], nodes: [ciceroMarkNode], }; + if (nodeInformation.properties[nodePropertyIndex] === TRANSFORMED_NODES.link) { + ciceroMarkNode.title = ''; + for (const relationshipElement of this.relationshipXML) { + if (relationshipElement.attributes.Id === nodeInformation.linkId) { + ciceroMarkNode.destination = relationshipElement.attributes.Target; + break; + } + } + } } return ciceroMarkNode; } @@ -268,6 +280,14 @@ class ToCiceroMarkVisitor { ...rootNode.nodes[rootNodesLength - 1].nodes[subNodeLength - 1].nodes, constructedNode, ]; + } else if (commonPropertiesLength === 3) { + const subNodeLength = rootNode.nodes[rootNodesLength - 1].nodes.length; + const deepSubNodeLength = rootNode.nodes[rootNodesLength - 1].nodes[subNodeLength - 1].nodes.length; + rootNode.nodes[rootNodesLength - 1].nodes[subNodeLength - 1].nodes[deepSubNodeLength - 1].nodes = [ + ...rootNode.nodes[rootNodesLength - 1].nodes[subNodeLength - 1].nodes[deepSubNodeLength - 1] + .nodes, + constructedNode, + ]; } } this.JSONXML = []; @@ -283,12 +303,15 @@ class ToCiceroMarkVisitor { * Traverses for properties and value. * * @param {Array} node Node to be traversed + * @param {string} calledBy Parent node class that called the function * @param {object} nodeInformation Information for the current node - * @param {Boolean} calledByCodeBlock Is function called by codeblock checker * @returns {string} Value in tags */ - fetchFormattingProperties(node, nodeInformation, calledByCodeBlock = false) { + fetchFormattingProperties(node, calledBy = TRANSFORMED_NODES.paragraph, nodeInformation = null) { let ooxmlTagTextValue = ''; + if (calledBy === TRANSFORMED_NODES.link) { + nodeInformation.properties = [...nodeInformation.properties, calledBy]; + } for (const runTimeNodes of node.elements) { if (runTimeNodes.name === 'w:rPr') { let colorCodePresent = false; @@ -317,7 +340,7 @@ class ToCiceroMarkVisitor { nodeInformation.nodeType = TRANSFORMED_NODES.code; } } else if (runTimeNodes.name === 'w:t') { - if (calledByCodeBlock) { + if (calledBy === TRANSFORMED_NODES.codeBlock) { ooxmlTagTextValue += runTimeNodes.elements ? runTimeNodes.elements[0].text : ''; } else { ooxmlTagTextValue = runTimeNodes.elements ? runTimeNodes.elements[0].text : ' '; @@ -337,11 +360,18 @@ class ToCiceroMarkVisitor { /** * Traverses the JSON object of XML elements in DFS approach. * - * @param {object} node Node object to be traversed + * @param {object} node Node object to be traversed * @param {string} parent Parent node name + * @param {syring} id Relation Id for link in OOXML * @returns {*} GeneratedNode if parent is of type clause else none */ - traverseElements(node, parent = '') { + traverseElements(node, parent = TRANSFORMED_NODES.paragraph, id = undefined) { + /** + * The parent argument is useful in cases where parent is a clause or link. + * If parent argument is not present, then everything would have been treated + * as pargraphs and transformation would be faulty. + */ + // Contains node present in a codeblock or blockquote, etc. let blockNodes = []; for (const subNode of node) { @@ -362,7 +392,7 @@ class ToCiceroMarkVisitor { let text = ''; for (const codeBlockSubNode of subNode.elements) { if (codeBlockSubNode.name === 'w:r') { - text = this.fetchFormattingProperties(codeBlockSubNode, undefined, true); + text = this.fetchFormattingProperties(codeBlockSubNode, TRANSFORMED_NODES.codeBlock); } } const codeBlockNode = { @@ -428,7 +458,10 @@ class ToCiceroMarkVisitor { } if (variableSubNodes.name === 'w:sdtContent') { if (nodeInformation.nodeType === TRANSFORMED_NODES.clause) { - const nodes = this.traverseElements(variableSubNodes.elements, TRANSFORMED_NODES.clause); + const nodes = this.traverseElements( + variableSubNodes.elements, + TRANSFORMED_NODES.clause + ); const clauseNode = { $class: TRANSFORMED_NODES.clause, elementType: nodeInformation.elementType, @@ -439,16 +472,25 @@ class ToCiceroMarkVisitor { } else { for (const variableContentNodes of variableSubNodes.elements) { if (variableContentNodes.name === 'w:r') { - this.fetchFormattingProperties(variableContentNodes, nodeInformation); + this.fetchFormattingProperties( + variableContentNodes, + TRANSFORMED_NODES.paragraph, + nodeInformation + ); } } } } } } + } else if (subNode.name === 'w:hyperlink') { + this.traverseElements(subNode.elements, TRANSFORMED_NODES.link, subNode.attributes['r:id']); } else if (subNode.name === 'w:r') { let nodeInformation = { properties: [], value: '' }; - this.fetchFormattingProperties(subNode, nodeInformation); + if (parent === TRANSFORMED_NODES.link) { + nodeInformation.linkId = id; + } + this.fetchFormattingProperties(subNode, parent, nodeInformation); } } return blockNodes; @@ -474,7 +516,9 @@ class ToCiceroMarkVisitor { if (node.attributes['pkg:name'] === pkgName) { // Gets the document node documentNode = node.elements[0].elements[0]; - break; + } + if (node.attributes['pkg:name'] === '/word/_rels/document.xml.rels') { + this.relationshipXML = node.elements[0].elements[0].elements; } } diff --git a/packages/markdown-docx/src/ToOOXMLVisitor/helpers.js b/packages/markdown-docx/src/ToOOXMLVisitor/helpers.js index ace57bd4..2462ec64 100644 --- a/packages/markdown-docx/src/ToOOXMLVisitor/helpers.js +++ b/packages/markdown-docx/src/ToOOXMLVisitor/helpers.js @@ -58,10 +58,26 @@ function wrapAroundLockedContentControls(ooxml) { /** * Wraps OOXML in docx headers. * - * @param {string} ooxml OOXML to be wrapped + * @param {string} ooxml OOXML to be wrapped + * @param {Array} relationships Relationship tags * @returns {string} OOXML wraped in docx headers */ -function wrapAroundDefaultDocxTags(ooxml) { +function wrapAroundDefaultDocxTags(ooxml, relationships) { + + const LINK_STYLE_SPEC = ` + + + + + + + + + + + + `; + const HEADING_STYLE_SPEC = ` @@ -211,17 +227,25 @@ function wrapAroundDefaultDocxTags(ooxml) { + ${LINK_STYLE_SPEC} `; + let relationshipOOXML = ''; + relationships.forEach( + ({ id, destination }) => + (relationshipOOXML += ``) + ); + const RELATIONSHIP_SPEC = ` + ${relationshipOOXML} diff --git a/packages/markdown-docx/src/ToOOXMLVisitor/index.js b/packages/markdown-docx/src/ToOOXMLVisitor/index.js index fbdd2b6c..28311bff 100644 --- a/packages/markdown-docx/src/ToOOXMLVisitor/index.js +++ b/packages/markdown-docx/src/ToOOXMLVisitor/index.js @@ -29,9 +29,11 @@ const { CODEBLOCK_PROPERTIES_RULE, CODEBLOCK_FONTPROPERTIES_RULE, CLAUSE_RULE, + LINK_RULE, + LINK_PROPERTY_RULE, } = require('./rules'); const { wrapAroundDefaultDocxTags, wrapAroundLockedContentControls } = require('./helpers'); -const { TRANSFORMED_NODES } = require('../constants'); +const { TRANSFORMED_NODES, RELATIONSHIP_OFFSET } = require('../constants'); /** * Transforms the ciceromark to OOXML @@ -47,6 +49,8 @@ class ToOOXMLVisitor { this.counter = {}; // OOXML tags for a given block node(heading, paragraph, etc.) this.tags = []; + // Relationship tags for links in a document + this.relationships = []; } /** @@ -59,6 +63,45 @@ class ToOOXMLVisitor { return node.$class; } + /** + * Generates the OOXML for text and code ciceromark nodes. + * + * @param {string} value Text value of the node + * @param {Array} nodeProperties Properties of the node + * @param {boolean} calledByCode Is function called by code node or not + * @returns {string} Generated OOXML + */ + generateTextOrCodeOOXML(value, nodeProperties, calledByCode = false) { + let propertyTag = ''; + if (calledByCode) { + propertyTag = CODE_PROPERTIES_RULE(); + } + let isLinkPropertyPresent = false; + for (const property of nodeProperties) { + if (property === TRANSFORMED_NODES.emphasize) { + propertyTag += EMPHASIS_RULE(); + } else if (property === TRANSFORMED_NODES.strong) { + propertyTag += STRONG_RULE(); + } else if (property === TRANSFORMED_NODES.link) { + isLinkPropertyPresent = true; + propertyTag += LINK_PROPERTY_RULE(); + } + } + if (propertyTag) { + propertyTag = TEXT_STYLES_RULE(propertyTag); + } + + let textValueTag = TEXT_RULE(value); + + let tag = TEXT_WRAPPER_RULE(propertyTag, textValueTag); + + if (isLinkPropertyPresent) { + let relationshipId = 'rId' + (this.relationships.length + RELATIONSHIP_OFFSET).toString(); + tag = LINK_RULE(tag, relationshipId); + } + return tag; + } + /** * Traverses CiceroMark nodes in a DFS approach * @@ -71,36 +114,10 @@ class ToOOXMLVisitor { } else { for (let subNode of node) { if (this.getClass(subNode) === TRANSFORMED_NODES.text) { - let propertyTag = ''; - for (let property of properties) { - if (property === TRANSFORMED_NODES.emphasize) { - propertyTag += EMPHASIS_RULE(); - } else if (property === TRANSFORMED_NODES.strong) { - propertyTag += STRONG_RULE(); - } - } - if (propertyTag) { - propertyTag = TEXT_STYLES_RULE(propertyTag); - } - - let textValueTag = TEXT_RULE(subNode.text); - - let tag = TEXT_WRAPPER_RULE(propertyTag, textValueTag); + const tag = this.generateTextOrCodeOOXML(subNode.text, properties); this.tags = [...this.tags, tag]; } else if (this.getClass(subNode) === TRANSFORMED_NODES.code) { - let propertyTag = CODE_PROPERTIES_RULE(); - for (let property of properties) { - if (property === TRANSFORMED_NODES.emphasize) { - propertyTag += EMPHASIS_RULE(); - } else if (property === TRANSFORMED_NODES.strong) { - propertyTag += STRONG_RULE(); - } - } - propertyTag = TEXT_STYLES_RULE(propertyTag); - - let textValueTag = TEXT_RULE(subNode.text); - - let tag = TEXT_WRAPPER_RULE(propertyTag, textValueTag); + const tag = this.generateTextOrCodeOOXML(subNode.text, properties, true); this.tags = [...this.tags, tag]; } else if (this.getClass(subNode) === TRANSFORMED_NODES.codeBlock) { let ooxml = CODEBLOCK_PROPERTIES_RULE(); @@ -224,6 +241,15 @@ class ToOOXMLVisitor { this.globalOOXML += ooxml; this.tags = []; } else { + if (this.getClass(subNode) === TRANSFORMED_NODES.link) { + this.relationships = [ + ...this.relationships, + { + id: this.relationships.length + RELATIONSHIP_OFFSET + 1, + destination: subNode.destination, + }, + ]; + } let newProperties = [...properties, subNode.$class]; this.traverseNodes(subNode.nodes, newProperties); } @@ -242,7 +268,7 @@ class ToOOXMLVisitor { toOOXML(ciceromark) { this.traverseNodes(ciceromark, []); this.globalOOXML = wrapAroundLockedContentControls(this.globalOOXML); - this.globalOOXML = wrapAroundDefaultDocxTags(this.globalOOXML); + this.globalOOXML = wrapAroundDefaultDocxTags(this.globalOOXML, this.relationships); return this.globalOOXML; } diff --git a/packages/markdown-docx/src/ToOOXMLVisitor/rules.js b/packages/markdown-docx/src/ToOOXMLVisitor/rules.js index 49ae6fae..6620bb4a 100644 --- a/packages/markdown-docx/src/ToOOXMLVisitor/rules.js +++ b/packages/markdown-docx/src/ToOOXMLVisitor/rules.js @@ -229,6 +229,25 @@ const CLAUSE_RULE = (title, tag, type, content) => { `; }; +const LINK_PROPERTY_RULE = () => { + return ''; +}; + +/** + * Inserts a link node in OOXML syntax. + * + * @param {string} value Value to be rendered in the link + * @param {string} relationshipId Specifies the ID of the relationship in the relationships part for an external link rId5 + * @returns {string} Link OOXML + */ +const LINK_RULE = (value, relationshipId) => { + return ` + + ${value} + + `; +}; + module.exports = { TEXT_RULE, EMPHASIS_RULE, @@ -244,4 +263,6 @@ module.exports = { CODEBLOCK_FONTPROPERTIES_RULE, THEMATICBREAK_RULE, CLAUSE_RULE, + LINK_PROPERTY_RULE, + LINK_RULE, }; diff --git a/packages/markdown-docx/src/constants.js b/packages/markdown-docx/src/constants.js index 3853561a..844ee40a 100644 --- a/packages/markdown-docx/src/constants.js +++ b/packages/markdown-docx/src/constants.js @@ -25,13 +25,20 @@ const TRANSFORMED_NODES = { emphasize: `${NS_PREFIX_CommonMarkModel}Emph`, heading: `${NS_PREFIX_CommonMarkModel}Heading`, item: `${NS_PREFIX_CommonMarkModel}Item`, + link: `${NS_PREFIX_CommonMarkModel}Link`, paragraph: `${NS_PREFIX_CommonMarkModel}Paragraph`, softbreak: `${NS_PREFIX_CommonMarkModel}Softbreak`, strong: `${NS_PREFIX_CommonMarkModel}Strong`, text: `${NS_PREFIX_CommonMarkModel}Text`, thematicBreak: `${NS_PREFIX_CommonMarkModel}ThematicBreak`, variable: `${NS_PREFIX_CiceroMarkModel}Variable`, - clause:`${NS_PREFIX_CiceroMarkModel}Clause` + clause: `${NS_PREFIX_CiceroMarkModel}Clause`, }; -module.exports = { TRANSFORMED_NODES }; +// Two relationships for numbering and style are already present +// and since we need to accommodate for link styles as well, we need a unique ID +// to represent them. Hence, 2 is added to offset the enumeration of `rId`. +// Used in './ToOOXMLVisitor/index.js' +const RELATIONSHIP_OFFSET = 2; + +module.exports = { TRANSFORMED_NODES, RELATIONSHIP_OFFSET }; diff --git a/packages/markdown-docx/test/data/ciceroMark/link-variants.json b/packages/markdown-docx/test/data/ciceroMark/link-variants.json new file mode 100644 index 00000000..5f55fd2b --- /dev/null +++ b/packages/markdown-docx/test/data/ciceroMark/link-variants.json @@ -0,0 +1 @@ +{"$class":"org.accordproject.commonmark.Document","xmlns":"http://commonmark.org/xml/1.0","nodes":[{"$class":"org.accordproject.commonmark.Heading","level":"2","nodes":[{"$class":"org.accordproject.commonmark.Link","destination":"https://google.com","title":"","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Heading Link"}]}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.commonmark.Link","destination":"https://github.com","title":"","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Simple Link"}]}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.commonmark.Link","destination":"https://twitter.com","title":"","nodes":[{"$class":"org.accordproject.commonmark.Emph","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Emph Link"}]}]}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.commonmark.Link","destination":"https://templatemark-dingus.netlify.app/","title":"","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"_"},{"$class":"org.accordproject.commonmark.Emph","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Strong Link"}]}]}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.commonmark.Link","destination":"https://facebook.com","title":"","nodes":[{"$class":"org.accordproject.commonmark.Emph","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Nested "},{"$class":"org.accordproject.commonmark.Strong","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Link"}]}]}]}]},{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.commonmark.Link","destination":"https://google.com","title":"","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Diffrent"}]},{"$class":"org.accordproject.commonmark.Text","text":" "},{"$class":"org.accordproject.commonmark.Link","destination":"https://github.com","title":"","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Links"}]}]}]} \ No newline at end of file diff --git a/packages/markdown-docx/test/data/ciceroMark/link.json b/packages/markdown-docx/test/data/ciceroMark/link.json new file mode 100644 index 00000000..183b3bca --- /dev/null +++ b/packages/markdown-docx/test/data/ciceroMark/link.json @@ -0,0 +1 @@ +{"$class":"org.accordproject.commonmark.Document","xmlns":"http://commonmark.org/xml/1.0","nodes":[{"$class":"org.accordproject.commonmark.Paragraph","nodes":[{"$class":"org.accordproject.commonmark.Link","destination":"https://google.com","title":"","nodes":[{"$class":"org.accordproject.commonmark.Strong","nodes":[{"$class":"org.accordproject.commonmark.Emph","nodes":[{"$class":"org.accordproject.commonmark.Text","text":"Check "},{"$class":"org.accordproject.commonmark.Code","text":"link"}]}]}]}]}]} \ No newline at end of file diff --git a/packages/markdown-transform/test/data/acceptance/omitted-acceptance-of-delivery.xml b/packages/markdown-transform/test/data/acceptance/omitted-acceptance-of-delivery.xml index 9708a0ed..5c61ef8a 100644 --- a/packages/markdown-transform/test/data/acceptance/omitted-acceptance-of-delivery.xml +++ b/packages/markdown-transform/test/data/acceptance/omitted-acceptance-of-delivery.xml @@ -531,6 +531,7 @@ + @@ -684,6 +685,19 @@ + + + + + + + + + + + + +