From 8eb15ad97ce6eb220f1d80fcd1e08393ad395cea Mon Sep 17 00:00:00 2001 From: David Bruant Date: Sat, 19 Jul 2025 14:07:10 +0200 Subject: [PATCH] fillOdtElementTemplate now takes an array of nodes as argument for the situation of calling it on the middleContent of a block with several elements so they're evaluated together (#15) * reproducing bug * reducing test case * fillOdtElementTemplate now takes an array of nodes as argument for the situation of calling it on the middleContent of a block with several elements so they're evaluated together --- .../odf/templating/fillOdtElementTemplate.js | 347 ++++++++++-------- tests/fill-odt-template/each.js | 29 ++ ...without-common-ancestor-for-inner-each.odt | Bin 0 -> 12571 bytes 3 files changed, 213 insertions(+), 163 deletions(-) create mode 100644 tests/fixtures/nested-each-without-common-ancestor-for-inner-each.odt diff --git a/scripts/odf/templating/fillOdtElementTemplate.js b/scripts/odf/templating/fillOdtElementTemplate.js index 7502f13..51203ba 100644 --- a/scripts/odf/templating/fillOdtElementTemplate.js +++ b/scripts/odf/templating/fillOdtElementTemplate.js @@ -14,7 +14,7 @@ class TemplateDOMBranch{ /** @type {Node} */ #leafNode - // ancestors with this.#ancestors[0] === this.#startNode and this.#ancestors.at(-1) === this.#leafNode + // ancestors with this.#ancestors[0] === this.#branchBaseNode and this.#ancestors.at(-1) === this.#leafNode /** @type {Node[]} */ #ancestors @@ -235,9 +235,12 @@ class TemplateBlock{ } //console.log('[fillBlockContentTemplate] after startChild') - for(const content of this.#middleContent){ - fillOdtElementTemplate(content, compartement) - } + + // if content consists of several parts of an {#each}{/each} + // when arriving to the {/each}, it will be alone (and imbalanced) + // and will trigger an error + fillOdtElementTemplate(Array.from(this.#middleContent), compartement) + //console.log('[fillBlockContentTemplate] after middleContent') const endChild = this.endBranch.at(1) @@ -554,16 +557,24 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, c const IF = ifStartMarkerRegex.source const EACH = eachStartMarkerRegex.source +/** @typedef {Element | DocumentFragment | Document} RootElementArgument */ + + /** * - * @param {Element | DocumentFragment | Document} rootElement + * @param {RootElementArgument | RootElementArgument[]} rootElements * @param {Compartment} compartment * @returns {void} */ -export default function fillOdtElementTemplate(rootElement, compartment) { - //console.log('[fillTemplatedOdtElement]', rootElement.nodeType, rootElement.nodeName, rootElement.textContent) - //console.log('[fillTemplatedOdtElement]', rootElement.documentElement && rootElement.documentElement.textContent) +export default function fillOdtElementTemplate(rootElements, compartment) { + if(!Array.isArray(rootElements)){ + rootElements = [rootElements] + } + + //console.log('[fillTemplatedOdtElement]', rootElements.length, rootElements[0].nodeType, rootElements[0].nodeName, rootElements[0].textContent) + //console.log('[fillTemplatedOdtElement]', rootElement.documentElement && rootElement.documentElement.textContent) + let currentlyOpenBlocks = [] /** @type {Node | undefined} */ @@ -585,187 +596,197 @@ export default function fillOdtElementTemplate(rootElement, compartment) { // Traverse "in document order" - // @ts-ignore - traverse(rootElement, currentNode => { - //console.log('currentlyOpenBlocks', currentlyOpenBlocks) - - const insideAnOpenBlock = currentlyOpenBlocks.length >= 1 + for(const rootElement of rootElements){ - if(currentNode.nodeType === Node.TEXT_NODE) { - const text = currentNode.textContent || '' + // @ts-ignore + traverse(rootElement, currentNode => { + //console.log('currentlyOpenBlocks', currentlyOpenBlocks) - /** - * looking for {#each x as y} - */ - const eachStartMatch = text.match(eachStartMarkerRegex); + const insideAnOpenBlock = currentlyOpenBlocks.length >= 1 - if(eachStartMatch) { - //console.log('startMatch', startMatch) + if(currentNode.nodeType === Node.TEXT_NODE) { + const text = currentNode.textContent || '' - currentlyOpenBlocks.push(EACH) + /** + * looking for {#each x as y} + */ + const eachStartMatch = text.match(eachStartMarkerRegex); - if(insideAnOpenBlock) { - // do nothing - } - else { - let [_, _iterableExpression, _itemExpression] = eachStartMatch + if(eachStartMatch) { + //console.log('startMatch', startMatch) - eachBlockIterableExpression = _iterableExpression - eachBlockItemExpression = _itemExpression - eachOpeningMarkerNode = currentNode - } - } + currentlyOpenBlocks.push(EACH) - - /** - * Looking for {/each} - */ - const isEachClosingBlock = text.includes(eachClosingMarker) - - if(isEachClosingBlock) { - - //console.log('isEachClosingBlock', isEachClosingBlock) - - if(!eachOpeningMarkerNode) - throw new Error(`{/each} found without corresponding opening {#each x as y}`) - - if(currentlyOpenBlocks.at(-1) !== EACH) - throw new Error(`{/each} found while the last opened block was not an opening {#each x as y}`) - - if(currentlyOpenBlocks.length === 1) { - eachClosingMarkerNode = currentNode - - // found an {#each} and its corresponding {/each} - // execute replacement loop - fillEachBlock(eachOpeningMarkerNode, eachBlockIterableExpression, eachBlockItemExpression, eachClosingMarkerNode, compartment) - - eachOpeningMarkerNode = undefined - eachBlockIterableExpression = undefined - eachBlockItemExpression = undefined - eachClosingMarkerNode = undefined - } - else { - // ignore because it will be treated as part of the outer {#each} - } - - currentlyOpenBlocks.pop() - } - - - /** - * Looking for {#if ...} - */ - const ifStartMatch = text.match(ifStartMarkerRegex); - - if(ifStartMatch) { - currentlyOpenBlocks.push(IF) - - if(insideAnOpenBlock) { - // do nothing because the marker is too deep - } - else { - let [_, _ifBlockConditionExpression] = ifStartMatch - - ifBlockConditionExpression = _ifBlockConditionExpression - ifOpeningMarkerNode = currentNode - } - } - - - /** - * Looking for {:else} - */ - const hasElseMarker = text.includes(elseMarker); - - if(hasElseMarker) { - if(!insideAnOpenBlock) - throw new Error('{:else} without a corresponding {#if}') - - if(currentlyOpenBlocks.length === 1) { - if(currentlyOpenBlocks[0] === IF) { - ifElseMarkerNode = currentNode + if(insideAnOpenBlock) { + // do nothing + } + else { + let [_, _iterableExpression, _itemExpression] = eachStartMatch + + eachBlockIterableExpression = _iterableExpression + eachBlockItemExpression = _itemExpression + eachOpeningMarkerNode = currentNode } - else - throw new Error('{:else} inside an {#each} but without a corresponding {#if}') } - else { - // do nothing because the marker is too deep - } - } - /** - * Looking for {/if} - */ - const ifClosingMarker = text.includes(closingIfMarker); + /** + * Looking for {/each} + */ + const isEachClosingBlock = text.includes(eachClosingMarker) - if(ifClosingMarker) { - if(!insideAnOpenBlock) - throw new Error('{/if} without a corresponding {#if}') + if(isEachClosingBlock) { - if(currentlyOpenBlocks.length === 1) { - if(currentlyOpenBlocks[0] === IF) { - ifClosingMarkerNode = currentNode + //console.log('isEachClosingBlock', isEachClosingBlock) - // found an {#if} and its corresponding {/if} + if(!eachOpeningMarkerNode){ + throw new Error(`{/each} found without corresponding opening {#each x as y}`) + } + + if(currentlyOpenBlocks.at(-1) !== EACH) + throw new Error(`{/each} found while the last opened block was not an opening {#each x as y}`) + + if(currentlyOpenBlocks.length === 1) { + eachClosingMarkerNode = currentNode + + // found an {#each} and its corresponding {/each} // execute replacement loop - fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment) + //console.log('start of fillEachBlock') - ifOpeningMarkerNode = undefined - ifElseMarkerNode = undefined - ifClosingMarkerNode = undefined - ifBlockConditionExpression = undefined + fillEachBlock(eachOpeningMarkerNode, eachBlockIterableExpression, eachBlockItemExpression, eachClosingMarkerNode, compartment) + + //console.log('end of fillEachBlock') + + eachOpeningMarkerNode = undefined + eachBlockIterableExpression = undefined + eachBlockItemExpression = undefined + eachClosingMarkerNode = undefined + } + else { + // ignore because it will be treated as part of the outer {#each} + } + + //console.log('popping currentlyOpenBlocks') + currentlyOpenBlocks.pop() + } + + + /** + * Looking for {#if ...} + */ + const ifStartMatch = text.match(ifStartMarkerRegex); + + if(ifStartMatch) { + currentlyOpenBlocks.push(IF) + + if(insideAnOpenBlock) { + // do nothing because the marker is too deep + } + else { + let [_, _ifBlockConditionExpression] = ifStartMatch + + ifBlockConditionExpression = _ifBlockConditionExpression + ifOpeningMarkerNode = currentNode + } + } + + + /** + * Looking for {:else} + */ + const hasElseMarker = text.includes(elseMarker); + + if(hasElseMarker) { + if(!insideAnOpenBlock) + throw new Error('{:else} without a corresponding {#if}') + + if(currentlyOpenBlocks.length === 1) { + if(currentlyOpenBlocks[0] === IF) { + ifElseMarkerNode = currentNode + } + else + throw new Error('{:else} inside an {#each} but without a corresponding {#if}') + } + else { + // do nothing because the marker is too deep + } + } + + + /** + * Looking for {/if} + */ + const ifClosingMarker = text.includes(closingIfMarker); + + if(ifClosingMarker) { + if(!insideAnOpenBlock) + throw new Error('{/if} without a corresponding {#if}') + + if(currentlyOpenBlocks.length === 1) { + if(currentlyOpenBlocks[0] === IF) { + ifClosingMarkerNode = currentNode + + // found an {#if} and its corresponding {/if} + // execute replacement loop + fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment) + + ifOpeningMarkerNode = undefined + ifElseMarkerNode = undefined + ifClosingMarkerNode = undefined + ifBlockConditionExpression = undefined + } + else + throw new Error('{/if} inside an {#each} but without a corresponding {#if}') + } + else { + // do nothing because the marker is too deep + } + + currentlyOpenBlocks.pop() + } + + + /** + * Looking for variables for substitutions + */ + if(!insideAnOpenBlock) { + // @ts-ignore + if(currentNode.data) { + // @ts-ignore + const placesToFill = findPlacesToFillInString(currentNode.data, compartment) + + if(placesToFill) { + const newText = placesToFill.fill() + // @ts-ignore + const newTextNode = currentNode.ownerDocument?.createTextNode(newText) + // @ts-ignore + currentNode.parentNode?.replaceChild(newTextNode, currentNode) + } } - else - throw new Error('{/if} inside an {#each} but without a corresponding {#if}') } else { - // do nothing because the marker is too deep + // ignore because it will be treated as part of the outer {#each} block } - - currentlyOpenBlocks.pop() } - - /** - * Looking for variables for substitutions - */ - if(!insideAnOpenBlock) { - // @ts-ignore - if(currentNode.data) { + if(currentNode.nodeType === Node.ATTRIBUTE_NODE) { + // Looking for variables for substitutions + if(!insideAnOpenBlock) { // @ts-ignore - const placesToFill = findPlacesToFillInString(currentNode.data, compartment) - - if(placesToFill) { - const newText = placesToFill.fill() + if(currentNode.value) { // @ts-ignore - const newTextNode = currentNode.ownerDocument?.createTextNode(newText) - // @ts-ignore - currentNode.parentNode?.replaceChild(newTextNode, currentNode) + const placesToFill = findPlacesToFillInString(currentNode.value, compartment) + if(placesToFill) { + // @ts-ignore + currentNode.value = placesToFill.fill() + } } } - } - else { - // ignore because it will be treated as part of the outer {#each} block - } - } - - if(currentNode.nodeType === Node.ATTRIBUTE_NODE) { - // Looking for variables for substitutions - if(!insideAnOpenBlock) { - // @ts-ignore - if(currentNode.value) { - // @ts-ignore - const placesToFill = findPlacesToFillInString(currentNode.value, compartment) - if(placesToFill) { - // @ts-ignore - currentNode.value = placesToFill.fill() - } + else { + // ignore because it will be treated as part of the {#each} block } } - else { - // ignore because it will be treated as part of the {#each} block - } - } - }) + }) + + } } diff --git a/tests/fill-odt-template/each.js b/tests/fill-odt-template/each.js index 3233b66..7af3327 100644 --- a/tests/fill-odt-template/each.js +++ b/tests/fill-odt-template/each.js @@ -344,3 +344,32 @@ Année `.trim()) }); + + +test('nested each without common ancestor for inner each', async t => { + const templatePath = join(import.meta.dirname, '../fixtures/nested-each-without-common-ancestor-for-inner-each.odt') + const templateContent = `{#each liste_espèces_par_impact as élément} +{#each élément.liste_espèces as espèce} +{/each} +{/each} +` + + const data = { + liste_espèces_par_impact: [ + {} + ] + } + + const odtTemplate = await getOdtTemplate(templatePath) + + const templateTextContent = await getOdtTextContent(odtTemplate) + + t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template') + + const odtResult = await fillOdtTemplate(odtTemplate, data) + + const odtResultTextContent = await getOdtTextContent(odtResult) + t.deepEqual(odtResultTextContent, ``) + + +}); \ No newline at end of file diff --git a/tests/fixtures/nested-each-without-common-ancestor-for-inner-each.odt b/tests/fixtures/nested-each-without-common-ancestor-for-inner-each.odt new file mode 100644 index 0000000000000000000000000000000000000000..8f0ca01ab97683fe5831c39c064708cd97507a54 GIT binary patch literal 12571 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 LH0Wf|KOg-sm3oIL literal 0 HcmV?d00001