easy-template-x
Advanced tools
Comparing version 0.12.0 to 1.0.0
# Change Log | ||
## [1.0.0 - 2020-08-09](https://github.com/alonrbar/easy-template-x/tree/v1.0.0) | ||
**Stable release** - from now on breaking changes to the public API (the public | ||
interface of `TemplateHandler`) will introduce a new major release. | ||
### Fixed | ||
- Initial content types parsing. | ||
- Bug in paragraph loops (#36). | ||
## [0.12.0 - 2020-08-01](https://github.com/alonrbar/easy-template-x/tree/v0.12.0) | ||
@@ -4,0 +14,0 @@ |
@@ -17,2 +17,3 @@ import { XmlGeneralNode, XmlNode, XmlParser, XmlTextNode } from '../xml'; | ||
splitTextNode(textNode: XmlTextNode, splitIndex: number, addBefore: boolean): XmlTextNode; | ||
splitParagraphByTextNode(paragraph: XmlNode, textNode: XmlTextNode, removeTextNode: boolean): [XmlNode, XmlNode]; | ||
joinTextNodesRange(from: XmlTextNode, to: XmlTextNode): void; | ||
@@ -22,2 +23,4 @@ joinParagraphs(first: XmlNode, second: XmlNode): void; | ||
isTextNode(node: XmlNode): boolean; | ||
isRunNode(node: XmlNode): boolean; | ||
isRunPropertiesNode(node: XmlNode): boolean; | ||
isTableCellNode(node: XmlNode): boolean; | ||
@@ -32,2 +35,4 @@ isParagraphNode(node: XmlNode): boolean; | ||
containingTableRowNode(node: XmlNode): XmlNode; | ||
isEmptyTextNode(node: XmlNode): boolean; | ||
isEmptyRun(node: XmlNode): boolean; | ||
} |
@@ -40,3 +40,3 @@ import { IMap } from '../types'; | ||
removeSiblings(from: XmlNode, to: XmlNode): XmlNode[]; | ||
splitByChild(root: XmlNode, markerNode: XmlNode, removeMarkerNode: boolean): [XmlNode, XmlNode]; | ||
splitByChild(parent: XmlNode, child: XmlNode, removeChild: boolean): [XmlNode, XmlNode]; | ||
findParent(node: XmlNode, predicate: (node: XmlNode) => boolean): XmlNode; | ||
@@ -43,0 +43,0 @@ findParentByName(node: XmlNode, nodeName: string): XmlNode; |
{ | ||
"name": "easy-template-x", | ||
"version": "0.12.0", | ||
"version": "1.0.0", | ||
"description": "Generate docx documents from templates, in Node or in the browser.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -87,5 +87,5 @@ import { MimeType, MimeTypeHelper } from '../mimeType'; | ||
this.contentTypes[contentTypeAttribute]; | ||
this.contentTypes[contentTypeAttribute] = true; | ||
} | ||
} | ||
} |
@@ -109,2 +109,74 @@ import { XmlGeneralNode, XmlNode, XmlParser, XmlTextNode } from '../xml'; | ||
/** | ||
* Split the paragraph around the specified text node. | ||
* | ||
* @returns Two paragraphs - `left` and `right`. If the `removeTextNode` argument is | ||
* `false` then the original text node is the first text node of `right`. | ||
*/ | ||
public splitParagraphByTextNode(paragraph: XmlNode, textNode: XmlTextNode, removeTextNode: boolean): [XmlNode, XmlNode] { | ||
// input validation | ||
const containingParagraph = this.containingParagraphNode(textNode); | ||
if (containingParagraph != paragraph) | ||
throw new Error(`Node '${nameof(textNode)}' is not a descendant of '${nameof(paragraph)}'.`); | ||
const runNode = this.containingRunNode(textNode); | ||
const wordTextNode = this.containingTextNode(textNode); | ||
// create run clone | ||
const leftRun = XmlNode.cloneNode(runNode, false); | ||
const rightRun = runNode; | ||
XmlNode.insertBefore(leftRun, rightRun); | ||
// copy props from original run node (preserve style) | ||
const runProps = rightRun.childNodes.find(node => node.nodeName === DocxParser.RUN_PROPERTIES_NODE); | ||
if (runProps) { | ||
const leftRunProps = XmlNode.cloneNode(runProps, true); | ||
XmlNode.appendChild(leftRun, leftRunProps); | ||
} | ||
// move nodes from 'right' to 'left' | ||
const firstRunChildIndex = (runProps ? 1 : 0); | ||
let curChild = rightRun.childNodes[firstRunChildIndex]; | ||
while (curChild != wordTextNode) { | ||
XmlNode.remove(curChild); | ||
XmlNode.appendChild(leftRun, curChild); | ||
curChild = rightRun.childNodes[firstRunChildIndex]; | ||
} | ||
// remove text node | ||
if (removeTextNode) { | ||
XmlNode.removeChild(rightRun, firstRunChildIndex); | ||
} | ||
// create paragraph clone | ||
const leftPara = XmlNode.cloneNode(containingParagraph, false); | ||
const rightPara = containingParagraph; | ||
XmlNode.insertBefore(leftPara, rightPara); | ||
// copy props from original paragraph (preserve style) | ||
const paragraphProps = rightPara.childNodes.find(node => node.nodeName === DocxParser.PARAGRAPH_PROPERTIES_NODE); | ||
if (paragraphProps) { | ||
const leftParagraphProps = XmlNode.cloneNode(paragraphProps, true); | ||
XmlNode.appendChild(leftPara, leftParagraphProps); | ||
} | ||
// move nodes from 'right' to 'left' | ||
const firstParaChildIndex = (paragraphProps ? 1 : 0); | ||
curChild = rightPara.childNodes[firstParaChildIndex]; | ||
while (curChild != rightRun) { | ||
XmlNode.remove(curChild); | ||
XmlNode.appendChild(leftPara, curChild); | ||
curChild = rightPara.childNodes[firstParaChildIndex]; | ||
} | ||
// clean paragraphs - remove empty runs | ||
if (this.isEmptyRun(leftRun)) | ||
XmlNode.remove(leftRun); | ||
if (this.isEmptyRun(rightRun)) | ||
XmlNode.remove(rightRun); | ||
return [leftPara, rightPara]; | ||
} | ||
/** | ||
* Move all text between the 'from' and 'to' nodes to the 'from' node. | ||
@@ -216,2 +288,10 @@ */ | ||
public isRunNode(node: XmlNode): boolean { | ||
return node.nodeName === DocxParser.RUN_NODE; | ||
} | ||
public isRunPropertiesNode(node: XmlNode): boolean { | ||
return node.nodeName === DocxParser.RUN_PROPERTIES_NODE; | ||
} | ||
public isTableCellNode(node: XmlNode): boolean { | ||
@@ -294,2 +374,41 @@ return node.nodeName === DocxParser.TABLE_CELL_NODE; | ||
} | ||
// | ||
// advanced node queries | ||
// | ||
public isEmptyTextNode(node: XmlNode): boolean { | ||
if (!this.isTextNode(node)) | ||
throw new Error(`Text node expected but '${node.nodeName}' received.`); | ||
if (!node.childNodes?.length) | ||
return true; | ||
const xmlTextNode = node.childNodes[0]; | ||
if (!XmlNode.isTextNode(xmlTextNode)) | ||
throw new Error("Invalid XML structure. 'w:t' node should contain a single text node only."); | ||
if (!xmlTextNode.textContent) | ||
return true; | ||
return false; | ||
} | ||
public isEmptyRun(node: XmlNode): boolean { | ||
if (!this.isRunNode(node)) | ||
throw new Error(`Run node expected but '${node.nodeName}' received.`); | ||
for (const child of (node.childNodes ?? [])) { | ||
if (this.isRunPropertiesNode(child)) | ||
continue; | ||
if (this.isTextNode(child) && this.isEmptyTextNode(child)) | ||
continue; | ||
return false; | ||
} | ||
return true; | ||
} | ||
} |
@@ -24,24 +24,21 @@ import { Tag } from '../../../compilation'; | ||
const areSame = (firstParagraph === lastParagraph); | ||
const parent = firstParagraph.parentNode; | ||
const firstParagraphIndex = parent.childNodes.indexOf(firstParagraph); | ||
const lastParagraphIndex = areSame ? firstParagraphIndex : parent.childNodes.indexOf(lastParagraph); | ||
// split first paragraphs | ||
let splitResult = XmlNode.splitByChild(firstParagraph, openTag.xmlTextNode, true); | ||
// split first paragraph | ||
let splitResult = this.utilities.docxParser.splitParagraphByTextNode(firstParagraph, openTag.xmlTextNode, true); | ||
firstParagraph = splitResult[0]; | ||
const firstParagraphSplit = splitResult[1]; | ||
let afterFirstParagraph = splitResult[1]; | ||
if (areSame) | ||
lastParagraph = firstParagraphSplit; | ||
lastParagraph = afterFirstParagraph; | ||
// split last paragraph | ||
splitResult = XmlNode.splitByChild(lastParagraph, closeTag.xmlTextNode, true); | ||
const lastParagraphSplit = splitResult[0]; | ||
splitResult = this.utilities.docxParser.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode, true); | ||
const beforeLastParagraph = splitResult[0]; | ||
lastParagraph = splitResult[1]; | ||
if (areSame) | ||
afterFirstParagraph = beforeLastParagraph; | ||
// fix references | ||
XmlNode.removeChild(parent, firstParagraphIndex + 1); | ||
// disconnect splitted paragraph from their parents | ||
XmlNode.remove(afterFirstParagraph); | ||
if (!areSame) | ||
XmlNode.removeChild(parent, lastParagraphIndex); | ||
firstParagraphSplit.parentNode = null; | ||
lastParagraphSplit.parentNode = null; | ||
XmlNode.remove(beforeLastParagraph); | ||
@@ -51,7 +48,6 @@ // extract all paragraphs in between | ||
if (areSame) { | ||
this.utilities.docxParser.joinParagraphs(firstParagraphSplit, lastParagraphSplit); | ||
middleParagraphs = [firstParagraphSplit]; | ||
middleParagraphs = [afterFirstParagraph]; | ||
} else { | ||
const inBetween = XmlNode.removeSiblings(firstParagraph, lastParagraph); | ||
middleParagraphs = [firstParagraphSplit].concat(inBetween).concat(lastParagraphSplit); | ||
middleParagraphs = [afterFirstParagraph].concat(inBetween).concat(beforeLastParagraph); | ||
} | ||
@@ -58,0 +54,0 @@ |
@@ -384,39 +384,42 @@ import { MissingArgumentError } from '../errors'; | ||
/** | ||
* Split the original node into two sibling nodes. | ||
* Returns both nodes. | ||
* Split the original node into two sibling nodes. Returns both nodes. | ||
* | ||
* @param root The node to split | ||
* @param markerNode The node that marks the split position. | ||
* @param parent The node to split | ||
* @param child The node that marks the split position. | ||
* @param removeChild Should this method remove the child while splitting. | ||
* | ||
* @returns Two nodes - `left` and `right`. If the `removeChild` argument is | ||
* `false` then the original child node is the first child of `right`. | ||
*/ | ||
splitByChild(root: XmlNode, markerNode: XmlNode, removeMarkerNode: boolean): [XmlNode, XmlNode] { | ||
splitByChild(parent: XmlNode, child: XmlNode, removeChild: boolean): [XmlNode, XmlNode] { | ||
// find the split path | ||
const path = getDescendantPath(root, markerNode); | ||
if (child.parentNode != parent) | ||
throw new Error(`Node '${nameof(child)}' is not a direct child of '${nameof(parent)}'.`); | ||
// split | ||
const split = XmlNode.cloneNode(root, false); | ||
const childIndex = path[0] + 1; | ||
while (childIndex < root.childNodes.length) { | ||
const curChild = root.childNodes[childIndex]; | ||
XmlNode.remove(curChild); | ||
XmlNode.appendChild(split, curChild); | ||
// create childless clone 'left' | ||
const left = XmlNode.cloneNode(parent, false); | ||
if (parent.parentNode) { | ||
XmlNode.insertBefore(left, parent); | ||
} | ||
const right = parent; | ||
if (root.parentNode) { | ||
XmlNode.insertAfter(split, root); | ||
// move nodes from 'right' to 'left' | ||
let curChild = right.childNodes[0]; | ||
while (curChild != child) { | ||
XmlNode.remove(curChild); | ||
XmlNode.appendChild(left, curChild); | ||
curChild = right.childNodes[0]; | ||
} | ||
// remove marker node | ||
if (removeMarkerNode && root.childNodes.length) { | ||
XmlNode.removeChild(root, root.childNodes.length - 1); | ||
// remove child | ||
if (removeChild) { | ||
XmlNode.removeChild(right, 0); | ||
} | ||
return [root, split]; | ||
return [left, right]; | ||
}, | ||
findParent(node: XmlNode, predicate: (node: XmlNode) => boolean): XmlNode { | ||
if (!node) | ||
return null; | ||
while (node.parentNode) { | ||
while (node) { | ||
@@ -564,20 +567,2 @@ if (predicate(node)) | ||
function getDescendantPath(root: XmlNode, descendant: XmlNode): number[] { | ||
const path: number[] = []; | ||
let node = descendant; | ||
while (node !== root) { | ||
const parent = node.parentNode; | ||
if (!parent) | ||
throw new Error(`Argument ${nameof(descendant)} is not a descendant of ${nameof(root)}`); | ||
const curChildIndex = parent.childNodes.indexOf(node); | ||
path.push(curChildIndex); | ||
node = parent; | ||
} | ||
return path.reverse(); | ||
} | ||
function recursiveRemoveEmptyTextNodes(node: XmlNode): XmlNode { | ||
@@ -584,0 +569,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
384749
8981
0