Created
November 23, 2023 20:23
-
-
Save ezze/c6f51a16a35f333137bc0411bf0c91ea to your computer and use it in GitHub Desktop.
Possible easy-template-x fix with multiple conditions nested in a table
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { PluginUtilities, Tag, XmlNode } from 'easy-template-x'; | |
import { ILoopStrategy, SplitBeforeResult } from './types'; | |
export class LoopListStrategy implements ILoopStrategy { | |
private utilities: PluginUtilities | undefined; | |
public setUtilities(utilities: PluginUtilities): void { | |
this.utilities = utilities; | |
} | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |
public isApplicable(openTag: Tag, closeTag: Tag): boolean { | |
if (this.utilities === undefined) { | |
throw new TypeError('Plugin utilities are not set'); | |
} | |
const containingParagraph = this.utilities.docxParser.containingParagraphNode(openTag.xmlTextNode); | |
return this.utilities.docxParser.isListParagraph(containingParagraph); | |
} | |
public splitBefore(openTag: Tag, closeTag: Tag): SplitBeforeResult { | |
if (this.utilities === undefined) { | |
throw new TypeError('Plugin utilities are not set'); | |
} | |
const { docxParser } = this.utilities; | |
const firstParagraph = docxParser.containingParagraphNode(openTag.xmlTextNode); | |
const lastParagraph = docxParser.containingParagraphNode(closeTag.xmlTextNode); | |
const paragraphs = XmlNode.siblingsInRange(firstParagraph, lastParagraph); | |
// remove the loop tags | |
XmlNode.remove(openTag.xmlTextNode); | |
XmlNode.remove(closeTag.xmlTextNode); | |
return { | |
firstNode: firstParagraph, | |
nodes: paragraphs, | |
lastNode: lastParagraph | |
}; | |
} | |
public mergeBack( | |
paragraphGroups: Array<Array<XmlNode>>, | |
firstParagraph: XmlNode, | |
lastParagraph: XmlNode, | |
paragraphs: Array<XmlNode> | |
): void { | |
for (let i = 0; i < paragraphGroups.length; i += 1) { | |
const curParagraphsGroup = paragraphGroups[i]; | |
for (let j = 0; j < curParagraphsGroup.length; j += 1) { | |
const paragraph = curParagraphsGroup[j]; | |
XmlNode.insertBefore(paragraph, lastParagraph); | |
} | |
} | |
// remove the old paragraphs | |
paragraphs.forEach((paragraph) => XmlNode.remove(paragraph)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { PluginUtilities, Tag, XmlNode } from 'easy-template-x'; | |
import { ILoopStrategy, SplitBeforeResult } from './types'; | |
export class LoopParagraphStrategy implements ILoopStrategy { | |
private utilities: PluginUtilities | undefined; | |
public setUtilities(utilities: PluginUtilities): void { | |
this.utilities = utilities; | |
} | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |
public isApplicable(openTag: Tag, closeTag: Tag): boolean { | |
return true; | |
} | |
public splitBefore(openTag: Tag, closeTag: Tag): SplitBeforeResult { | |
if (this.utilities === undefined) { | |
throw new TypeError('Plugin utilities are not set'); | |
} | |
const { docxParser } = this.utilities; | |
// gather some info | |
let firstParagraph = docxParser.containingParagraphNode(openTag.xmlTextNode); | |
let lastParagraph = docxParser.containingParagraphNode(closeTag.xmlTextNode); | |
const areSame = firstParagraph === lastParagraph; | |
// split first paragraph | |
let splitResult = docxParser.splitParagraphByTextNode(firstParagraph, openTag.xmlTextNode, true); | |
[firstParagraph] = splitResult; | |
let afterFirstParagraph = splitResult[1]; | |
if (areSame) lastParagraph = afterFirstParagraph; | |
// split last paragraph | |
splitResult = docxParser.splitParagraphByTextNode(lastParagraph, closeTag.xmlTextNode, true); | |
const beforeLastParagraph = splitResult[0]; | |
[, lastParagraph] = splitResult; | |
if (areSame) afterFirstParagraph = beforeLastParagraph; | |
// disconnect split paragraphs from their parents | |
XmlNode.remove(afterFirstParagraph); | |
if (!areSame) XmlNode.remove(beforeLastParagraph); | |
// extract all paragraphs in between | |
let middleParagraphs: Array<XmlNode>; | |
if (areSame) { | |
middleParagraphs = [afterFirstParagraph]; | |
} else { | |
const inBetween = XmlNode.removeSiblings(firstParagraph, lastParagraph); | |
middleParagraphs = [afterFirstParagraph].concat(inBetween).concat(beforeLastParagraph); | |
} | |
return { | |
firstNode: firstParagraph, | |
nodes: middleParagraphs, | |
lastNode: lastParagraph | |
}; | |
} | |
public mergeBack(middleParagraphs: Array<Array<XmlNode>>, firstParagraph: XmlNode, lastParagraph: XmlNode): void { | |
if (this.utilities === undefined) { | |
throw new TypeError('Plugin utilities are not set'); | |
} | |
const { docxParser } = this.utilities; | |
let mergeTo = firstParagraph; | |
for (let i = 0; i < middleParagraphs.length; i += 1) { | |
const curParagraphsGroup = middleParagraphs[i]; | |
if (curParagraphsGroup.length === 0) { | |
continue; | |
} | |
// merge first paragraphs | |
docxParser.joinParagraphs(mergeTo, curParagraphsGroup[0]); | |
// add middle and last paragraphs to the original document | |
for (let j = 1; j < curParagraphsGroup.length; j += 1) { | |
XmlNode.insertBefore(curParagraphsGroup[j], lastParagraph); | |
mergeTo = curParagraphsGroup[j]; | |
} | |
} | |
// merge last paragraph | |
docxParser.joinParagraphs(mergeTo, lastParagraph); | |
// remove the old last paragraph (was merged into the new one) | |
XmlNode.remove(lastParagraph); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-await-in-loop */ | |
import { | |
PluginUtilities, | |
ScopeData, | |
Tag, | |
TemplateContext, | |
TemplateData, | |
TemplatePlugin, | |
XmlNode | |
} from 'easy-template-x'; | |
import { LoopListStrategy } from './loop-list-strategy'; | |
import { LoopParagraphStrategy } from './loop-paragraph-strategy'; | |
import { LoopTableStrategy } from './loop-table-strategy'; | |
import { ILoopStrategy } from './types'; | |
export const LOOP_CONTENT_TYPE = 'loop'; | |
export class LoopPlugin extends TemplatePlugin { | |
public readonly contentType = LOOP_CONTENT_TYPE; | |
protected readonly loopStrategies: Array<ILoopStrategy> = [ | |
new LoopTableStrategy(), | |
new LoopListStrategy(), | |
new LoopParagraphStrategy() // the default strategy | |
]; | |
protected getDefaultStrategy(): ILoopStrategy { | |
return this.loopStrategies[this.loopStrategies.length - 1]; | |
} | |
public setUtilities(utilities: PluginUtilities): void { | |
this.utilities = utilities; | |
this.loopStrategies.forEach((strategy) => strategy.setUtilities(utilities)); | |
} | |
public async containerTagReplacements(tags: Array<Tag>, data: ScopeData, context: TemplateContext): Promise<void> { | |
const openTag = tags[0]; | |
const closeTag = tags[tags.length - 1]; | |
const value = data.getScopeData<Array<TemplateData>>(); | |
if (!Array.isArray(value)) { | |
await this.compileCondition(openTag, closeTag, data, context, !!value); | |
} else { | |
await this.compileLoop(openTag, closeTag, data, context, value.length); | |
} | |
} | |
protected async compileCondition( | |
openTag: Tag, | |
closeTag: Tag, | |
data: ScopeData, | |
context: TemplateContext, | |
passed: boolean | |
): Promise<void> { | |
const { compiler } = this.utilities; | |
const strategy = this.getDefaultStrategy(); | |
const { firstNode, lastNode, nodes } = strategy.splitBefore(openTag, closeTag); | |
const compiledNodes: Array<XmlNode> = []; | |
if (passed) { | |
const dummyRootNode = XmlNode.createGeneralNode('dummyRootNode'); | |
nodes.forEach((node) => XmlNode.appendChild(dummyRootNode, node)); | |
await compiler.compile(dummyRootNode, data, context); | |
while (dummyRootNode.childNodes && dummyRootNode.childNodes.length > 0) { | |
const node = XmlNode.removeChild(dummyRootNode, 0); | |
compiledNodes.push(node); | |
} | |
} | |
strategy.mergeBack([compiledNodes], firstNode, lastNode, nodes); | |
} | |
protected async compileLoop( | |
openTag: Tag, | |
closeTag: Tag, | |
data: ScopeData, | |
context: TemplateContext, | |
count: number | |
): Promise<void> { | |
// Select the suitable strategy | |
const loopStrategy = this.loopStrategies.find((strategy) => strategy.isApplicable(openTag, closeTag)); | |
if (!loopStrategy) { | |
throw new Error(`No loop strategy found for tag '${openTag.rawText}'`); | |
} | |
// prepare to loop | |
const { firstNode, lastNode, nodes } = loopStrategy.splitBefore(openTag, closeTag); | |
// repeat (loop) the content | |
const nodeGroups = this.cloneNodeGroups(nodes, count); | |
// recursive compilation | |
// (this step can be optimized in the future if we'll keep track of the | |
// path to each token and use that to create new tokens instead of | |
// search through the text again) | |
const compiledNodes = await this.compileLoopGroups(nodeGroups, data, context); | |
// merge back to the document | |
loopStrategy.mergeBack(compiledNodes, firstNode, lastNode, nodes); | |
} | |
protected async compileLoopGroups( | |
nodeGroups: Array<Array<XmlNode>>, | |
data: ScopeData, | |
context: TemplateContext | |
): Promise<Array<Array<XmlNode>>> { | |
const compiledNodeGroups: Array<Array<XmlNode>> = []; | |
// compile each node group with its relevant data | |
for (let i = 0; i < nodeGroups.length; i += 1) { | |
// create dummy root node | |
const curNodes = nodeGroups[i]; | |
const dummyRootNode = XmlNode.createGeneralNode('dummyRootNode'); | |
curNodes.forEach((node) => XmlNode.appendChild(dummyRootNode, node)); | |
// compile the new root | |
data.pathPush(i); | |
await this.utilities.compiler.compile(dummyRootNode, data, context); | |
data.pathPop(); | |
// disconnect from dummy root | |
const curResult: Array<XmlNode> = []; | |
while (dummyRootNode.childNodes && dummyRootNode.childNodes.length) { | |
const child = XmlNode.removeChild(dummyRootNode, 0); | |
curResult.push(child); | |
} | |
compiledNodeGroups.push(curResult); | |
} | |
return compiledNodeGroups; | |
} | |
protected cloneNodeGroups(nodes: Array<XmlNode>, times: number): Array<Array<XmlNode>> { | |
if (nodes.length === 0 || times === 0) { | |
return []; | |
} | |
const resultNodes: Array<Array<XmlNode>> = []; | |
for (let i = 0; i < times; i += 1) { | |
const clonedNodes = nodes.map((node) => XmlNode.cloneNode(node, true)); | |
resultNodes.push(clonedNodes); | |
} | |
return resultNodes; | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { PluginUtilities, Tag, XmlNode } from 'easy-template-x'; | |
import { ILoopStrategy, SplitBeforeResult } from './types'; | |
export class LoopTableStrategy implements ILoopStrategy { | |
private utilities: PluginUtilities | undefined; | |
public setUtilities(utilities: PluginUtilities): void { | |
this.utilities = utilities; | |
} | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |
public isApplicable(openTag: Tag, closeTag: Tag): boolean { | |
if (this.utilities === undefined) { | |
throw new TypeError('Plugin utilities are not set'); | |
} | |
const containingParagraph = this.utilities.docxParser.containingParagraphNode(openTag.xmlTextNode); | |
if (!containingParagraph.parentNode) return false; | |
return this.utilities.docxParser.isTableCellNode(containingParagraph.parentNode); | |
} | |
public splitBefore(openTag: Tag, closeTag: Tag): SplitBeforeResult { | |
if (this.utilities === undefined) { | |
throw new TypeError('Plugin utilities are not set'); | |
} | |
const { docxParser } = this.utilities; | |
const firstRow = docxParser.containingTableRowNode(openTag.xmlTextNode); | |
const lastRow = docxParser.containingTableRowNode(closeTag.xmlTextNode); | |
const rows = XmlNode.siblingsInRange(firstRow, lastRow); | |
// remove the loop tags | |
XmlNode.remove(openTag.xmlTextNode); | |
XmlNode.remove(closeTag.xmlTextNode); | |
return { | |
firstNode: firstRow, | |
lastNode: lastRow, | |
nodes: rows | |
}; | |
} | |
public mergeBack(rowGroups: Array<Array<XmlNode>>, firstRow: XmlNode, lastRow: XmlNode, rows: Array<XmlNode>): void { | |
for (let i = 0; i < rowGroups.length; i += 1) { | |
const curRowsGroup = rowGroups[i]; | |
for (let j = 0; j < curRowsGroup.length; j += 1) { | |
const row = curRowsGroup[j]; | |
XmlNode.insertBefore(row, lastRow); | |
} | |
} | |
// remove the old rows | |
rows.forEach((row) => XmlNode.remove(row)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { PluginUtilities, Tag, XmlNode } from 'easy-template-x'; | |
export interface ILoopStrategy { | |
setUtilities(utilities: PluginUtilities): void; | |
isApplicable(openTag: Tag, closeTag: Tag): boolean; | |
splitBefore(openTag: Tag, closeTag: Tag): SplitBeforeResult; | |
mergeBack(compiledNodes: Array<Array<XmlNode>>, firstNode: XmlNode, lastNode: XmlNode, nodes: Array<XmlNode>): void; | |
} | |
export interface SplitBeforeResult { | |
firstNode: XmlNode; | |
lastNode: XmlNode; | |
nodes: Array<XmlNode>; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment