From 0e7c2ba95987933824e1affd224bb3f2bbf58fd6 Mon Sep 17 00:00:00 2001 From: David Bruant Date: Thu, 8 May 2025 21:27:57 +0200 Subject: [PATCH] the tests ae passing --- .../odf/templating/prepareTemplateDOMTree.js | 116 +++++++++--------- tests/fill-odt-template/formatting.js | 38 +++++- .../formatted-start-each-single-paragraph.odt | Bin 0 -> 16394 bytes tools/create-odt-file-from-template.js | 12 +- 4 files changed, 102 insertions(+), 64 deletions(-) create mode 100644 tests/fixtures/formatted-start-each-single-paragraph.odt diff --git a/scripts/odf/templating/prepareTemplateDOMTree.js b/scripts/odf/templating/prepareTemplateDOMTree.js index da3af78..b631ca8 100644 --- a/scripts/odf/templating/prepareTemplateDOMTree.js +++ b/scripts/odf/templating/prepareTemplateDOMTree.js @@ -43,10 +43,10 @@ function findAllMatches(text, pattern) { */ function findCommonAncestor(node1, node2) { const ancestors1 = getAncestors(node1); - const ancestors2 = getAncestors(node2); + const ancestors2 = new Set(getAncestors(node2)); for(const ancestor of ancestors1) { - if(ancestors2.includes(ancestor)) { + if(ancestors2.has(ancestor)) { return ancestor; } } @@ -98,19 +98,14 @@ function getNodeTextPosition(node, containerTextNodes) { /** * remove nodes between startNode and endNode - * but keep startNode and endNode - * - * returns the common ancestor child in start branch - * for the purpose for inserting something between startNode and endNode - * with insertionPoint.parentNode.insertBefore(newBetweenContent, insertionPoint) + * including startNode and endNode * * @param {Node} startNode * @param {Node} endNode - * @returns {Node} + * @param {string} text + * @returns {void} */ -function removeNodesBetween(startNode, endNode) { - let nodesToRemove = new Set(); - +function replaceBetweenNodesWithText(startNode, endNode, text) { // find both ancestry branch const startNodeAncestors = new Set(getAncestors(startNode)) const endNodeAncestors = new Set(getAncestors(endNode)) @@ -118,43 +113,42 @@ function removeNodesBetween(startNode, endNode) { // find common ancestor const commonAncestor = findCommonAncestor(startNode, endNode) - // remove everything "on the right" of start branch - let currentAncestor = startNode - let commonAncestorChildInEndNodeBranch + let remove = false + let toRemove = [] + let commonAncestorChild = commonAncestor.firstChild + let commonAncestorInsertionChild - while(currentAncestor !== commonAncestor){ - let siblingToRemove = currentAncestor.nextSibling - - while(siblingToRemove && !endNodeAncestors.has(siblingToRemove)){ - nodesToRemove.add(siblingToRemove) - siblingToRemove = siblingToRemove.nextSibling - } - if(endNodeAncestors.has(siblingToRemove)){ - commonAncestorChildInEndNodeBranch = siblingToRemove + while(commonAncestorChild){ + if(startNodeAncestors.has(commonAncestorChild)){ + remove = true } - currentAncestor = currentAncestor.parentNode; - } + if(remove){ + toRemove.push(commonAncestorChild) - // remove everything "on the left" of end branch - currentAncestor = endNode - - while(currentAncestor !== commonAncestor){ - let siblingToRemove = currentAncestor.previousSibling - - while(siblingToRemove && !startNodeAncestors.has(siblingToRemove)){ - nodesToRemove.add(siblingToRemove) - siblingToRemove = siblingToRemove.previousSibling + if(endNodeAncestors.has(commonAncestorChild)){ + commonAncestorInsertionChild = commonAncestorChild.nextSibling + break; + } } - - currentAncestor = currentAncestor.parentNode; + commonAncestorChild = commonAncestorChild.nextSibling } - for(const node of nodesToRemove){ - node.parentNode.removeChild(node) + for(const node of toRemove){ + commonAncestor.removeChild(node) + } + + //console.log('replaceBetweenNodesWithText startNode', startNode.textContent) + + const newTextNode = commonAncestor.ownerDocument.createTextNode(text) + + if(commonAncestorInsertionChild){ + commonAncestor.insertBefore(newTextNode, commonAncestorInsertionChild) + } + else{ + commonAncestor.appendChild(newTextNode) } - return commonAncestorChildInEndNodeBranch } /** @@ -202,8 +196,10 @@ function consolidateMarkers(document){ ...findAllMatches(fullText, variableRegex) ]; - /*if(positionedMarkers.length >= 1) - console.log('positionedMarkers', positionedMarkers)*/ + + //if(positionedMarkers.length >= 1) + // console.log('positionedMarkers', positionedMarkers) + while(consolidatedMarkers.length < positionedMarkers.length) { refreshContainerTextNodes() @@ -246,6 +242,18 @@ function consolidateMarkers(document){ // Check if marker spans multiple nodes if(startNode !== endNode) { + const commonAncestor = findCommonAncestor(startNode, endNode) + + let commonAncestorStartChild = startNode + while(commonAncestorStartChild.parentNode !== commonAncestor){ + commonAncestorStartChild = commonAncestorStartChild.parentNode + } + + let commonAncestorEndChild = endNode + while(commonAncestorEndChild.parentNode !== commonAncestor){ + commonAncestorEndChild = commonAncestorEndChild.parentNode + } + // Calculate relative positions within the nodes let startNodeTextContent = startNode.textContent || ''; let endNodeTextContent = endNode.textContent || ''; @@ -256,47 +264,45 @@ function consolidateMarkers(document){ // Calculate the position within the end node let posInEndNode = (positionedMarker.index + positionedMarker.marker.length) - getNodeTextPosition(endNode, containerTextNodesInTreeOrder); - /** @type {Node} */ - let beforeStartNode = startNode + let newStartNode = startNode // if there is before-text, split if(posInStartNode > 0) { // Text exists before the marker - preserve it // set newStartNode to a Text node containing only the marker beginning - const newStartNode = startNode.splitText(posInStartNode) + newStartNode = startNode.splitText(posInStartNode) // startNode/beforeStartNode now contains only non-marker text // then, by definition of .splitText(posInStartNode): posInStartNode = 0 - // remove the marker beginning part from the tree (since the marker will be inserted in full later) + // move the marker beginning part to become a child of commonAncestor newStartNode.parentNode?.removeChild(newStartNode) + + commonAncestor.insertBefore(newStartNode, commonAncestorStartChild.nextSibling) } - /** @type {Node} */ - let afterEndNode // if there is after-text, split if(posInEndNode < endNodeTextContent.length) { // Text exists after the marker - preserve it - // set afterEndNode to a Text node containing only non-marker text - afterEndNode = endNode.splitText(posInEndNode); + endNode.splitText(posInEndNode); // endNode now contains only the end of marker text // then, by definition of .splitText(posInEndNode): posInEndNode = endNodeTextContent.length - // remove the marker ending part from the tree (since the marker will be inserted in full later) - endNode.parentNode?.removeChild(endNode) + // move the marker ending part to become a child of commonAncestor + if(endNode !== commonAncestorEndChild){ + endNode.parentNode?.removeChild(endNode) + commonAncestor.insertBefore(endNode, commonAncestorEndChild) + } } // then, replace all nodes between (new)startNode and (new)endNode with a single textNode in commonAncestor - const insertionPoint = removeNodesBetween(beforeStartNode, afterEndNode) - const markerTextNode = insertionPoint.ownerDocument.createTextNode(positionedMarker.marker) - - insertionPoint.parentNode.insertBefore(markerTextNode, insertionPoint) + replaceBetweenNodesWithText(newStartNode, endNode, positionedMarker.marker) // After consolidation, break as the DOM structure has changed // and containerTextNodesInTreeOrder needs to be refreshed diff --git a/tests/fill-odt-template/formatting.js b/tests/fill-odt-template/formatting.js index fda17fc..4a6aaf1 100644 --- a/tests/fill-odt-template/formatting.js +++ b/tests/fill-odt-template/formatting.js @@ -5,6 +5,7 @@ import {getOdtTemplate} from '../../scripts/odf/odtTemplate-forNode.js' import {fillOdtTemplate, getOdtTextContent} from '../../exports.js' + test('template filling with several layers of formatting in {#each ...} start marker', async t => { const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-plusieurs-couches.odt') const templateContent = `Liste de nombres @@ -86,7 +87,6 @@ Les nombres : 3 5 8 13  !! }); - test('template filling - {/each} and text after partially formatted', async t => { const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-each-end-and-after-formatted.odt') const templateContent = `Liste de nombres @@ -114,8 +114,6 @@ Les nombres : 5 8 13 21  !! }); - - test('template filling - partially formatted variable', async t => { const templatePath = join(import.meta.dirname, '../fixtures/partially-formatted-variable.odt') const templateContent = `Nombre @@ -129,13 +127,41 @@ Voici le nombre : {nombre} !!! const templateTextContent = await getOdtTextContent(odtTemplate) t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template') - +//try{ const odtResult = await fillOdtTemplate(odtTemplate, data) - +//}catch(e){console.error(e)} const odtResultTextContent = await getOdtTextContent(odtResult) t.deepEqual(odtResultTextContent, `Nombre Voici le nombre : 37 !!! `) -}); \ No newline at end of file +}); + + +test('template filling - formatted-start-each-single-paragraph', async t => { + const templatePath = join(import.meta.dirname, '../fixtures/formatted-start-each-single-paragraph.odt') + const templateContent = ` +{#each nombres as n} +{n} +{/each} +` + + const data = {nombres : [37, 38, 39]} + + 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(), ` +37 +38 +39 +`.trim()) + +}); + diff --git a/tests/fixtures/formatted-start-each-single-paragraph.odt b/tests/fixtures/formatted-start-each-single-paragraph.odt new file mode 100644 index 0000000000000000000000000000000000000000..7bc9d0c5c196ccb4f1ea2b15cb6a7dbb9f357da1 GIT binary patch literal 16394 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