Skip to content

Commit

Permalink
feat(markdown-docx): add link transformer - #397
Browse files Browse the repository at this point in the history
transformation logic: OOXML<->CiceroMark
rules: link and link property
tests: link and its variants

Signed-off-by: k-kumar-01 <[email protected]>
  • Loading branch information
K-Kumar-01 committed Aug 5, 2021
1 parent c26a49c commit d2f29f8
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 11 deletions.
62 changes: 54 additions & 8 deletions packages/markdown-docx/src/ToCiceroMarkVisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 = [];
Expand All @@ -284,11 +304,14 @@ class ToCiceroMarkVisitor {
*
* @param {Array} node Node to be traversed
* @param {object} nodeInformation Information for the current node
* @param {Boolean} calledByCodeBlock Is function called by codeblock checker
* @param {string} calledBy Parent node class that called the function
* @returns {string} Value in <w:t> tags
*/
fetchFormattingProperties(node, nodeInformation, calledByCodeBlock = false) {
fetchFormattingProperties(node, nodeInformation, calledBy = TRANSFORMED_NODES.paragraph) {
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;
Expand Down Expand Up @@ -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 : ' ';
Expand All @@ -337,11 +360,12 @@ 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 = '', id = undefined) {
// Contains node present in a codeblock or blockquote, etc.
let blockNodes = [];
for (const subNode of node) {
Expand All @@ -362,7 +386,11 @@ 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,
undefined,
TRANSFORMED_NODES.codeBlock
);
}
}
const codeBlockNode = {
Expand Down Expand Up @@ -428,7 +456,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,
Expand All @@ -446,9 +477,18 @@ class ToCiceroMarkVisitor {
}
}
}
} 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,
nodeInformation,
parent === TRANSFORMED_NODES.link ? TRANSFORMED_NODES.link : TRANSFORMED_NODES.paragraph
);
}
}
return blockNodes;
Expand Down Expand Up @@ -478,6 +518,12 @@ class ToCiceroMarkVisitor {
}
}

for (const node of rootNode) {
if (node.attributes['pkg:name'] === '/word/_rels/document.xml.rels') {
this.relationshipXML = node.elements[0].elements[0].elements;
}
}

this.traverseElements(documentNode.elements[0].elements, 'body');

return {
Expand Down
25 changes: 23 additions & 2 deletions packages/markdown-docx/src/ToOOXMLVisitor/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<w:style w:type="character" w:styleId="Hyperlink">
<w:name w:val="Hyperlink"/>
<w:basedOn w:val="DefaultParagraphFont"/>
<w:uiPriority w:val="99"/>
<w:unhideWhenUsed/>
<w:rsid w:val="003C09C6"/>
<w:rPr>
<w:color w:val="0563C1" w:themeColor="hyperlink"/>
<w:u w:val="single"/>
</w:rPr>
</w:style>
`;

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 @@ -211,17 +227,22 @@ function wrapAroundDefaultDocxTags(ooxml) {
<w:color w:val="1F4D78" w:themeColor="accent1" w:themeShade="7F" />
</w:rPr>
</w:style>
${LINK_STYLE_SPEC}
</w:styles>
</pkg:xmlData>
</pkg:part>
`;

let relationshipOOXML = '';
relationships.forEach(relationship => (relationshipOOXML += relationship));

const RELATIONSHIP_SPEC = `
<pkg:part pkg:name="/word/_rels/document.xml.rels" pkg:contentType="application/vnd.openxmlformats-package.relationships+xml" pkg:padding="256">
<pkg:xmlData>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
${relationshipOOXML}
</Relationships>
</pkg:xmlData>
</pkg:part>
Expand Down
37 changes: 36 additions & 1 deletion packages/markdown-docx/src/ToOOXMLVisitor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ 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');
Expand All @@ -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 = [];
}

/**
Expand All @@ -72,11 +76,15 @@ class ToOOXMLVisitor {
for (let subNode of node) {
if (this.getClass(subNode) === TRANSFORMED_NODES.text) {
let propertyTag = '';
let isLinkPropertyPresent = false;
for (let property of properties) {
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) {
Expand All @@ -86,21 +94,40 @@ class ToOOXMLVisitor {
let textValueTag = TEXT_RULE(subNode.text);

let tag = TEXT_WRAPPER_RULE(propertyTag, textValueTag);

if (isLinkPropertyPresent) {
// some rels are already present
// To avoid overwrite ids start our
// rels from 10
let relationShipId = 'rId' + (this.relationShips.length + 10).toString();
tag = LINK_RULE(tag, relationShipId);
}
this.tags = [...this.tags, tag];
} else if (this.getClass(subNode) === TRANSFORMED_NODES.code) {
let propertyTag = CODE_PROPERTIES_RULE();
let isLinkPropertyPresent = false;
for (let property of properties) {
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();
}
}
propertyTag = TEXT_STYLES_RULE(propertyTag);

let textValueTag = TEXT_RULE(subNode.text);

let tag = TEXT_WRAPPER_RULE(propertyTag, textValueTag);
if (isLinkPropertyPresent) {
// some rels are already present
// To avoid overwrite ids start our
// rels from 10
let relationShipId = 'rId' + (this.relationShips.length + 10).toString();
tag = LINK_RULE(tag, relationShipId);
}
this.tags = [...this.tags, tag];
} else if (this.getClass(subNode) === TRANSFORMED_NODES.codeBlock) {
let ooxml = CODEBLOCK_PROPERTIES_RULE();
Expand Down Expand Up @@ -224,6 +251,14 @@ class ToOOXMLVisitor {
this.globalOOXML += ooxml;
this.tags = [];
} else {
if (this.getClass(subNode) === TRANSFORMED_NODES.link) {
const relationshipTag = `<Relationship Id="rId${
this.relationShips.length + 11
}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="${
subNode.destination
}" TargetMode="External"/>`;
this.relationShips = [...this.relationShips, relationshipTag];
}
let newProperties = [...properties, subNode.$class];
this.traverseNodes(subNode.nodes, newProperties);
}
Expand All @@ -242,7 +277,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;
}
Expand Down
21 changes: 21 additions & 0 deletions packages/markdown-docx/src/ToOOXMLVisitor/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,25 @@ const CLAUSE_RULE = (title, tag, type, content) => {
`;
};

const LINK_PROPERTY_RULE = () => {
return '<w:rStyle w:val="Hyperlink"/>';
};

/**
* 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 `
<w:hyperlink r:id="${relationShipId}" w:history="0">
${value}
</w:hyperlink>
`;
};

module.exports = {
TEXT_RULE,
EMPHASIS_RULE,
Expand All @@ -244,4 +263,6 @@ module.exports = {
CODEBLOCK_FONTPROPERTIES_RULE,
THEMATICBREAK_RULE,
CLAUSE_RULE,
LINK_PROPERTY_RULE,
LINK_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 @@ -25,6 +25,7 @@ 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`,
Expand Down
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":"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"}]}]}]}
36 changes: 36 additions & 0 deletions packages/markdown-docx/test/data/ciceroMark/link.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$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"
}
]
}
]
}
]
}
]
}
]
}

0 comments on commit d2f29f8

Please sign in to comment.