Skip to content

Instantly share code, notes, and snippets.

@ezze
Created November 23, 2023 20:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ezze/c6f51a16a35f333137bc0411bf0c91ea to your computer and use it in GitHub Desktop.
Save ezze/c6f51a16a35f333137bc0411bf0c91ea to your computer and use it in GitHub Desktop.
Possible easy-template-x fix with multiple conditions nested in a table
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));
}
}
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);
}
}
/* 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;
}
}
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));
}
}
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