Skip to content

Commit

Permalink
feat(markdown-docx): add link transformer - #397 (#436)
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 9, 2021
1 parent c26a49c commit 12cf1a1
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@
<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"/>

</Relationships>
</pkg:xmlData>
</pkg:part>
Expand Down Expand Up @@ -684,6 +685,19 @@
<w:color w:val="1F4D78" w:themeColor="accent1" w:themeShade="7F" />
</w:rPr>
</w:style>

<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>

</w:styles>
</pkg:xmlData>
</pkg:part>
Expand Down
64 changes: 54 additions & 10 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 @@ -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 <w:t> 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;
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,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) {
Expand All @@ -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 = {
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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;
}
}

Expand Down
28 changes: 26 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,25 @@ 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(
({ id, destination }) =>
(relationshipOOXML += `<Relationship Id="rId${id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="${destination}" TargetMode="External"/>`)
);

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
86 changes: 56 additions & 30 deletions packages/markdown-docx/src/ToOOXMLVisitor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -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
*
Expand All @@ -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();
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
}
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,
};
Loading

0 comments on commit 12cf1a1

Please sign in to comment.