From 29a2429c0037d3b1832a2b177837524a81f252ca Mon Sep 17 00:00:00 2001 From: David Bruant Date: Wed, 21 May 2025 15:09:47 +0200 Subject: [PATCH] Bug if 2 (#8) * Adjust variable marker to ignore {:else} * Getting start branch right content and end branch left content when extracting block content * remove open if block after {/if} * look for left/right branch content only up to common ancestor * fix test case to new reality * fix minor bug --- .gitignore | 2 + .../odf/templating/fillOdtElementTemplate.js | 66 +++++++++++++++--- scripts/odf/templating/markers.js | 2 +- .../odf/templating/prepareTemplateDOMTree.js | 7 +- tests/fill-odt-template/each.js | 6 ++ tests/fill-odt-template/if.js | 27 +++++++ ...branch-content-and-two-consecutive-ifs.odt | Bin 0 -> 14836 bytes 7 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/left-branch-content-and-two-consecutive-ifs.odt diff --git a/.gitignore b/.gitignore index 08d407f..5bd2365 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,7 @@ node_modules/ build/* .~lock* +**/*(Copie)* +**/*(Copy)* stats.html \ No newline at end of file diff --git a/scripts/odf/templating/fillOdtElementTemplate.js b/scripts/odf/templating/fillOdtElementTemplate.js index 723398e..a23043c 100644 --- a/scripts/odf/templating/fillOdtElementTemplate.js +++ b/scripts/odf/templating/fillOdtElementTemplate.js @@ -85,6 +85,7 @@ function findPlacesToFillInString(str, compartment) { * @returns {{startChild: Node, endChild:Node, content: DocumentFragment}} */ function extractBlockContent(blockStartNode, blockEndNode) { + //console.log('[extractBlockContent] blockStartNode', blockStartNode.textContent) //console.log('[extractBlockContent] blockEndNode', blockEndNode.textContent) // find common ancestor of blockStartNode and blockEndNode @@ -93,6 +94,7 @@ function extractBlockContent(blockStartNode, blockEndNode) { let startAncestor = blockStartNode let endAncestor = blockEndNode + // ancestries in order of deepest first, closest to root last const startAncestry = new Set([startAncestor]) const endAncestry = new Set([endAncestor]) @@ -117,9 +119,11 @@ function extractBlockContent(blockStartNode, blockEndNode) { const startAncestryToCommonAncestor = [...startAncestry].slice(0, [...startAncestry].indexOf(commonAncestor)) const endAncestryToCommonAncestor = [...endAncestry].slice(0, [...endAncestry].indexOf(commonAncestor)) + // direct children of commonAncestor in the branch or blockStartNode and blockEndNode respectively const startChild = startAncestryToCommonAncestor.at(-1) const endChild = endAncestryToCommonAncestor.at(-1) + //console.log('[extractBlockContent] startChild', startChild.textContent) //console.log('[extractBlockContent] endChild', endChild.textContent) // Extract DOM content in a documentFragment @@ -127,15 +131,57 @@ function extractBlockContent(blockStartNode, blockEndNode) { const contentFragment = blockStartNode.ownerDocument.createDocumentFragment() /** @type {Element[]} */ - const repeatedPatternArray = [] + const blockContent = [] + + // get start branch "right" content + for(const startAncestor of startAncestryToCommonAncestor){ + if(startAncestor === startChild) + break; + + let sibling = startAncestor.nextSibling + + while(sibling) { + blockContent.push(sibling) + sibling = sibling.nextSibling; + } + } + + let sibling = startChild.nextSibling while(sibling !== endChild) { - repeatedPatternArray.push(sibling) + blockContent.push(sibling) sibling = sibling.nextSibling; } - for(const sibling of repeatedPatternArray) { + + // get end branch "left" content + for(const endAncestor of [...endAncestryToCommonAncestor].reverse()){ + if(endAncestor === endChild) + continue; // already taken care of + + let sibling = endAncestor.previousSibling + + const reversedBlockContentContribution = [] + + while(sibling) { + reversedBlockContentContribution.push(sibling) + sibling = sibling.previousSibling; + } + + const blockContentContribution = reversedBlockContentContribution.reverse() + + blockContent.push(...blockContentContribution) + + if(endAncestor === blockEndNode) + break; + } + + + //console.log('blockContent', blockContent.map(n => n.textContent)) + + + for(const sibling of blockContent) { sibling.parentNode?.removeChild(sibling) contentFragment.appendChild(sibling) } @@ -206,7 +252,6 @@ function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, .add(startIfThenChild).add(endIfThenChild) } - if(chosenFragment) { fillOdtElementTemplate( chosenFragment, @@ -340,7 +385,8 @@ const EACH = eachStartMarkerRegex.source * @returns {void} */ export default function fillOdtElementTemplate(rootElement, compartment) { - //console.log('fillTemplatedOdtElement', rootElement.nodeType, rootElement.nodeName) + //console.log('fillTemplatedOdtElement', rootElement.nodeType, rootElement.nodeName, rootElement.textContent) + //console.log('fillTemplatedOdtElement', rootElement.childNodes[0].childNodes.length) let currentlyOpenBlocks = [] @@ -362,9 +408,11 @@ export default function fillOdtElementTemplate(rootElement, compartment) { let ifBlockConditionExpression // Traverse "in document order" + // @ts-ignore traverse(rootElement, currentNode => { - //console.log('currentlyUnclosedBlocks', currentlyUnclosedBlocks) + //console.log('currentlyOpenBlocks', currentlyOpenBlocks) + const insideAnOpenBlock = currentlyOpenBlocks.length >= 1 if(currentNode.nodeType === Node.TEXT_NODE) { @@ -473,9 +521,9 @@ export default function fillOdtElementTemplate(rootElement, compartment) { /** * Looking for {/if} */ - const hasClosingMarker = text.includes(closingIfMarker); + const ifClosingMarker = text.includes(closingIfMarker); - if(hasClosingMarker) { + if(ifClosingMarker) { if(!insideAnOpenBlock) throw new Error('{/if} without a corresponding {#if}') @@ -498,6 +546,8 @@ export default function fillOdtElementTemplate(rootElement, compartment) { else { // do nothing because the marker is too deep } + + currentlyOpenBlocks.pop() } diff --git a/scripts/odf/templating/markers.js b/scripts/odf/templating/markers.js index 1a09f1e..f2cceb5 100644 --- a/scripts/odf/templating/markers.js +++ b/scripts/odf/templating/markers.js @@ -1,5 +1,5 @@ // the regexps below are shared, so they shoudn't have state (no 'g' flag) -export const variableRegex = /\{([^{#\/]+?)\}/ +export const variableRegex = /\{([^{#\/:]+?)\}/ export const ifStartMarkerRegex = /{#if\s+([^}]+?)\s*}/; export const elseMarker = '{:else}' diff --git a/scripts/odf/templating/prepareTemplateDOMTree.js b/scripts/odf/templating/prepareTemplateDOMTree.js index 698e823..8c75d9e 100644 --- a/scripts/odf/templating/prepareTemplateDOMTree.js +++ b/scripts/odf/templating/prepareTemplateDOMTree.js @@ -3,6 +3,7 @@ import {traverse, Node} from "../../DOMUtils.js"; import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, variableRegex} from './markers.js' + /** * * @param {string} text @@ -166,6 +167,7 @@ function consolidateMarkers(document){ ] for(const potentialMarkersContainer of potentialMarkersContainers) { + /** @type {{marker: string, index: number}[]} */ const consolidatedMarkers = [] /** @type {Text[]} */ @@ -244,13 +246,16 @@ function consolidateMarkers(document){ // Check if marker spans multiple nodes if(startNode !== endNode) { + //console.log('startNode !== endNode', startNode.textContent, endNode.textContent) const commonAncestor = findCommonAncestor(startNode, endNode) + /** @type {Node} */ let commonAncestorStartChild = startNode while(commonAncestorStartChild.parentNode !== commonAncestor){ commonAncestorStartChild = commonAncestorStartChild.parentNode } + /** @type {Node} */ let commonAncestorEndChild = endNode while(commonAncestorEndChild.parentNode !== commonAncestor){ commonAncestorEndChild = commonAncestorEndChild.parentNode @@ -322,6 +327,7 @@ function consolidateMarkers(document){ } } } + } /** @@ -551,5 +557,4 @@ export default function prepareTemplateDOMTree(document){ isolateMarkerText(document) // after isolateMarkerText, each marker is in exactly one text node // (markers are separated from text that was before or after in the same text node) - } \ No newline at end of file diff --git a/tests/fill-odt-template/each.js b/tests/fill-odt-template/each.js index f0ede37..515e57b 100644 --- a/tests/fill-odt-template/each.js +++ b/tests/fill-odt-template/each.js @@ -327,16 +327,22 @@ Année Année Énergie par personne + 1970 36252.637 + 1980 43328.78 + 1990 46971.94 + 2000 53147.277 + 2010 48062.32 + 2020 37859.246 `.trim()) diff --git a/tests/fill-odt-template/if.js b/tests/fill-odt-template/if.js index 729f0ca..c934313 100644 --- a/tests/fill-odt-template/if.js +++ b/tests/fill-odt-template/if.js @@ -40,3 +40,30 @@ n est un grand nombre }); + +test('weird bug', async t => { + const templatePath = join(import.meta.dirname, '../fixtures/left-branch-content-and-two-consecutive-ifs.odt') + const templateContent = `Utilisation de sources lumineuses : {#if scientifique.source_lumineuses}Oui{:else}Non{/if} +{#if scientifique.source_lumineuses && scientifique.modalités_source_lumineuses } +Modalités d’utilisation de sources lumineuses : {scientifique.modalités_source_lumineuses} +{/if} +` + + const data = { + scientifique: { + source_lumineuses: false, + //modalités_source_lumineuses: 'lampes torches' + } + } + + + const odtTemplate = await getOdtTemplate(templatePath) + const templateTextContent = await getOdtTextContent(odtTemplate) + t.deepEqual(templateTextContent.trim(), templateContent.trim(), 'reconnaissance du template') + + const odtResult = await fillOdtTemplate(odtTemplate, data) + const odtResultTextContent = await getOdtTextContent(odtResult) + t.deepEqual(odtResultTextContent.trim(), `Utilisation de sources lumineuses : Non`) + +}); + diff --git a/tests/fixtures/left-branch-content-and-two-consecutive-ifs.odt b/tests/fixtures/left-branch-content-and-two-consecutive-ifs.odt new file mode 100644 index 0000000000000000000000000000000000000000..0630676820191f70c5f8c7b54c7485ccdccaba1e GIT binary patch literal 14836 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 literal 0 HcmV?d00001