From 3793923c796a142ef12364940d6d7955d7598deb Mon Sep 17 00:00:00 2001 From: David Bruant Date: Thu, 8 May 2025 15:09:20 +0200 Subject: [PATCH] Test of 2 formatted markers within same Text node passes --- scripts/odf/templating/fillOdtTemplate.js | 7 +- .../odf/templating/prepareTemplateDOMTree.js | 63 +++++++++++------- tests/fill-odt-template/formatting.js | 31 ++++++++- ...ing-liste-nombres-2-markeurs-formatted.odt | Bin 0 -> 12565 bytes ...tting-liste-nombres-plusieurs-couches.odt} | Bin 5 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 tests/fixtures/formatting-liste-nombres-2-markeurs-formatted.odt rename tests/fixtures/{liste-nombres-avec-formattage.odt => formatting-liste-nombres-plusieurs-couches.odt} (100%) diff --git a/scripts/odf/templating/fillOdtTemplate.js b/scripts/odf/templating/fillOdtTemplate.js index a13f3d9..6ca8047 100644 --- a/scripts/odf/templating/fillOdtTemplate.js +++ b/scripts/odf/templating/fillOdtTemplate.js @@ -1,6 +1,6 @@ import {ZipReader, ZipWriter, BlobReader, BlobWriter, TextReader, Uint8ArrayReader, TextWriter, Uint8ArrayWriter} from '@zip.js/zip.js'; -import {parseXML, serializeToString, Node} from '../../DOMUtils.js' +import {parseXML, serializeToString} from '../../DOMUtils.js' import {makeManifestFile, getManifestFileData} from '../manifest.js'; import prepareTemplateDOMTree from './prepareTemplateDOMTree.js'; @@ -164,8 +164,3 @@ export default async function fillOdtTemplate(odtTemplate, data) { return writer.close(); } - - - - - diff --git a/scripts/odf/templating/prepareTemplateDOMTree.js b/scripts/odf/templating/prepareTemplateDOMTree.js index 995c202..6974b2f 100644 --- a/scripts/odf/templating/prepareTemplateDOMTree.js +++ b/scripts/odf/templating/prepareTemplateDOMTree.js @@ -35,7 +35,6 @@ function findAllMatches(text, pattern) { return results; } - /** * * @param {Node} node1 @@ -72,7 +71,6 @@ function getAncestors(node) { return ancestors; } - /** * text position of a node relative to a text nodes within a container * @@ -268,8 +266,6 @@ function removeNodesBetween(startBranch, endBranch, commonAncestor) { } } - - /** * Consolidate markers which are split among several Text nodes * @@ -277,23 +273,33 @@ function removeNodesBetween(startBranch, endBranch, commonAncestor) { */ function consolidateMarkers(document){ // Perform a first pass to detect templating markers with formatting to remove it - const potentialMarkerContainers = [ + const potentialMarkersContainers = [ ...Array.from(document.getElementsByTagName('text:p')), ...Array.from(document.getElementsByTagName('text:h')) ] - for(const potentialMarkerContainer of potentialMarkerContainers) { - // Check if any template marker is split across multiple text nodes - // Get all text nodes within this container + for(const potentialMarkersContainer of potentialMarkersContainers) { + const consolidatedMarkers = [] + /** @type {Text[]} */ - const containerTextNodesInTreeOrder = []; + let containerTextNodesInTreeOrder = []; + + function refreshContainerTextNodes(){ + containerTextNodesInTreeOrder = [] + + traverse(potentialMarkersContainer, node => { + if(node.nodeType === Node.TEXT_NODE) { + containerTextNodesInTreeOrder.push(/** @type {Text} */(node)) + } + }) + } + + refreshContainerTextNodes() + let fullText = '' - traverse(potentialMarkerContainer, node => { - if(node.nodeType === Node.TEXT_NODE) { - containerTextNodesInTreeOrder.push(/** @type {Text} */(node)) - fullText = fullText + node.textContent - } - }) + for(const node of containerTextNodesInTreeOrder){ + fullText = fullText + node.textContent + } // Check for each template marker const positionedMarkers = [ @@ -307,10 +313,11 @@ function consolidateMarkers(document){ console.log('positionedMarkers', positionedMarkers) // If no markers found, skip this container - if(positionedMarkers.length >= 1) { + while(consolidatedMarkers.length < positionedMarkers.length) { + refreshContainerTextNodes() // For each marker, check if it's contained within a single text node - for(const positionedMarker of positionedMarkers) { + for(const positionedMarker of positionedMarkers.slice(consolidatedMarkers.length)) { console.log('positionedMarker', positionedMarker) let markerStart = -1; @@ -342,9 +349,16 @@ function consolidateMarkers(document){ currentPos = nodeEnd; } + /*if(!startNode){ + throw new Error(`Could not find startNode for marker '${positionedMarker.marker}'`) + }*/ + + /*if(!endNode){ + throw new Error(`Could not find endNode for marker '${positionedMarker.marker}'`) + }*/ + // Check if marker spans multiple nodes if(startNode !== endNode) { - console.log('startNode !== endNode') const commonAncestor = findCommonAncestor(startNode, endNode); // Calculate relative positions within the nodes @@ -364,8 +378,6 @@ function consolidateMarkers(document){ // Find the diverging point in the paths const lowestCommonAncestorChild = findDivergingPoint(pathToStart, pathToEnd); - console.log('lowestCommonAncestorChild', lowestCommonAncestorChild) - if(!lowestCommonAncestorChild) { // Direct parent-child relationship or other simple case // Handle separately @@ -392,6 +404,8 @@ function consolidateMarkers(document){ // Create a new node for the start of the marker const startOfMarkerNode = document.createTextNode(positionedMarker.marker); + console.log('parentOfStartNode', parentOfStartNode) + // Insert after the modified start node if(startNode.nextSibling) { parentOfStartNode.insertBefore(startOfMarkerNode, startNode.nextSibling); @@ -437,15 +451,18 @@ function consolidateMarkers(document){ removeNodesBetween(startBranch, endBranch, commonAncestor); } - // After consolidation, we can break as the DOM structure has changed + // After consolidation, break as the DOM structure has changed + // and containerTextNodesInTreeOrder needs to be refreshed + consolidatedMarkers.push(positionedMarker) break; } + + consolidatedMarkers.push(positionedMarker) } } } } - /** * isolate markers which are in Text nodes with other texts * @@ -529,8 +546,6 @@ function isolateMarkers(document){ }) } - - /** * This function prepares the template DOM tree in a way that makes it easily processed by the template execution * Specifically, after the call to this function, the document is altered to respect the following property: diff --git a/tests/fill-odt-template/formatting.js b/tests/fill-odt-template/formatting.js index a2d5221..64df339 100644 --- a/tests/fill-odt-template/formatting.js +++ b/tests/fill-odt-template/formatting.js @@ -5,8 +5,8 @@ import {getOdtTemplate} from '../../scripts/odf/odtTemplate-forNode.js' import {fillOdtTemplate, getOdtTextContent} from '../../exports.js' -test('template filling {#each ...}{/each} with formating in {#each ...} start marker', async t => { - const templatePath = join(import.meta.dirname, '../fixtures/liste-nombres-avec-formattage.odt') +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 Les nombres : {#each nombres as n}{n} {/each} !! @@ -29,4 +29,31 @@ Les nombres : {#each nombres as n}{n} {/each} !! Les nombres : 1 2 3 5  !! `) +}); + + +test('template filling - both {#each ...} and {/each} within the same Text node are formatted', async t => { + const templatePath = join(import.meta.dirname, '../fixtures/formatting-liste-nombres-2-markeurs-formatted.odt') + const templateContent = `Liste de nombres + +Les nombres : {#each nombres as n}{n} {/each} !! +` + + const data = { + nombres : [2,3,5,8] + } + + 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, `Liste de nombres + +Les nombres : 2 3 5 8  !! +`) + }); \ No newline at end of file diff --git a/tests/fixtures/formatting-liste-nombres-2-markeurs-formatted.odt b/tests/fixtures/formatting-liste-nombres-2-markeurs-formatted.odt new file mode 100644 index 0000000000000000000000000000000000000000..0ff9cceab9bae91c3844a05fcce1d2350c5eacd9 GIT binary patch literal 12565 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 f=Hy>ON%fl