From ca5aa1c0bfe43fbd8e72806d271b3409c8caa2bc Mon Sep 17 00:00:00 2001 From: David Bruant Date: Wed, 30 Apr 2025 17:43:37 +0200 Subject: [PATCH] if in a single text node works --- scripts/odf/fillOdtTemplate.js | 60 ++++++++++++++-------- tests/fill-odt-template/each.js | 27 ---------- tests/fill-odt-template/in-text-node.js | 65 ++++++++++++++++++++++++ tests/fixtures/inline-if-nombres.odt | Bin 0 -> 12489 bytes 4 files changed, 104 insertions(+), 48 deletions(-) create mode 100644 tests/fill-odt-template/in-text-node.js create mode 100644 tests/fixtures/inline-if-nombres.odt diff --git a/scripts/odf/fillOdtTemplate.js b/scripts/odf/fillOdtTemplate.js index 9d5c1e5..0c4a744 100644 --- a/scripts/odf/fillOdtTemplate.js +++ b/scripts/odf/fillOdtTemplate.js @@ -293,6 +293,15 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, c const IF = 'IF' const EACH = 'EACH' +// the regexps below are shared, so they shoudn't have state (no 'g' flag) +const ifStartRegex = /{#if\s+([^}]+?)\s*}/; +const elseMarker = '{:else}' +const closingIfMarker = '{/if}' + +const eachStartMarkerRegex = /{#each\s+([^}]+?)\s+as\s+([^}]+?)\s*}/; +const eachClosingBlockString = '{/each}' + + /** * @@ -334,8 +343,7 @@ function fillTemplatedOdtElement(rootElement, compartment){ /** * looking for {#each x as y} */ - const eachStartRegex = /{#each\s+([^}]+?)\s+as\s+([^}]+?)\s*}/; - const eachStartMatch = text.match(eachStartRegex); + const eachStartMatch = text.match(eachStartMarkerRegex); if(eachStartMatch){ //console.log('startMatch', startMatch) @@ -358,7 +366,6 @@ function fillTemplatedOdtElement(rootElement, compartment){ /** * Looking for {/each} */ - const eachClosingBlockString = '{/each}' const isEachClosingBlock = text.includes(eachClosingBlockString) if(isEachClosingBlock){ @@ -394,7 +401,6 @@ function fillTemplatedOdtElement(rootElement, compartment){ /** * Looking for {#if ...} */ - const ifStartRegex = /{#if\s+([^}]+?)\s*}/; const ifStartMatch = text.match(ifStartRegex); if(ifStartMatch){ @@ -415,7 +421,6 @@ function fillTemplatedOdtElement(rootElement, compartment){ /** * Looking for {:else} */ - const elseMarker = '{:else}' const hasElseMarker = text.includes(elseMarker); if(hasElseMarker){ @@ -438,7 +443,6 @@ function fillTemplatedOdtElement(rootElement, compartment){ /** * Looking for {/if} */ - const closingIfMarker = '{/if}' const hasClosingMarker = text.includes(closingIfMarker); if(hasClosingMarker){ @@ -529,28 +533,42 @@ function fillTemplatedOdtDocument(document, compartment){ let remainingText = currentNode.textContent || '' while(remainingText.length >= 1){ - let match; + let matchText; + let matchIndex; - // looking for opening {#each ...} block - const eachBlockOpeningRegex = /{#each\s+([^}]+?)\s+as\s+([^}]+?)\s*}/; - const eachBlockClosingRegex = /{\/each}/; + // looking for a block marker + for(const marker of [ifStartRegex, elseMarker, closingIfMarker, eachStartMarkerRegex, eachClosingBlockString]){ + if(typeof marker === 'string'){ + const index = remainingText.indexOf(marker) + + if(index !== -1){ + matchText = marker + matchIndex = index - for(const regexp of [eachBlockOpeningRegex, eachBlockClosingRegex]){ - let thisMatch = remainingText.match(regexp) + // found the first match + break; // get out of loop + } + } + else{ + // marker is a RegExp + const match = remainingText.match(marker) - // trying to find only the first match in remainingText string - // @ts-ignore - if(thisMatch && (!match || match.index > thisMatch.index)){ - match = thisMatch + if(match){ + matchText = match[0] + matchIndex = match.index + + // found the first match + break; // get out of loop + } } } - if(match){ + if(matchText){ // split 3-way : before-match, match and after-match - if(match[0].length < remainingText.length){ + if(matchText.length < remainingText.length){ // @ts-ignore - let afterMatchTextNode = currentNode.splitText(match.index + match[0].length) + let afterMatchTextNode = currentNode.splitText(matchIndex + matchText.length) if(afterMatchTextNode.textContent && afterMatchTextNode.textContent.length >= 1){ remainingText = afterMatchTextNode.textContent } @@ -561,9 +579,9 @@ function fillTemplatedOdtDocument(document, compartment){ // per spec, currentNode now contains before-match and match text // @ts-ignore - if(match.index > 0){ + if(matchIndex > 0){ // @ts-ignore - currentNode.splitText(match.index) + currentNode.splitText(matchIndex) } if(afterMatchTextNode){ diff --git a/tests/fill-odt-template/each.js b/tests/fill-odt-template/each.js index c39de73..8c9cc43 100644 --- a/tests/fill-odt-template/each.js +++ b/tests/fill-odt-template/each.js @@ -247,33 +247,6 @@ Hiver }); -test('template filling {#each ...}{/each} within a single text node', async t => { - const templatePath = join(import.meta.dirname, '../fixtures/liste-nombres.odt') - const templateContent = `Liste de nombres - -Les nombres : {#each nombres as n}{n} {/each} !! -` - - const data = { - nombres : [1,1,2,3,5,8,13,21] - } - - 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 : 1 1 2 3 5 8 13 21  !! -`) - -}); - - test('template filling of a table', async t => { const templatePath = join(import.meta.dirname, '../fixtures/tableau-simple.odt') const templateContent = `Évolution énergie en kWh par personne en France diff --git a/tests/fill-odt-template/in-text-node.js b/tests/fill-odt-template/in-text-node.js new file mode 100644 index 0000000..1928d3f --- /dev/null +++ b/tests/fill-odt-template/in-text-node.js @@ -0,0 +1,65 @@ +import test from 'ava'; +import {join} from 'node:path'; + +import {getOdtTemplate} from '../../scripts/odf/odtTemplate-forNode.js' + +import {fillOdtTemplate, getOdtTextContent} from '../../exports.js' + + +test('template filling {#if ...}{/if} within a single text node', async t => { + const templatePath = join(import.meta.dirname, '../fixtures/inline-if-nombres.odt') + const templateContent = `Taille de nombre + +Le nombre {n} est {#if n<5}petit{:else}grand{/if}. +` + + const odtTemplate = await getOdtTemplate(templatePath) + + const templateTextContent = await getOdtTextContent(odtTemplate) + t.deepEqual(templateTextContent, templateContent, 'reconnaissance du template') + + const odtResult3 = await fillOdtTemplate(odtTemplate, {n : 3}) + + const odtResult3TextContent = await getOdtTextContent(odtResult3) + t.deepEqual(odtResult3TextContent, `Taille de nombre + +Le nombre 3 est petit. +`) + + const odtResult9 = await fillOdtTemplate(odtTemplate, {n : 9}) + + const odtResult9TextContent = await getOdtTextContent(odtResult9) + t.deepEqual(odtResult9TextContent, `Taille de nombre + +Le nombre 9 est grand. +`) + +}); + + +test('template filling {#each ...}{/each} within a single text node', async t => { + const templatePath = join(import.meta.dirname, '../fixtures/liste-nombres.odt') + const templateContent = `Liste de nombres + +Les nombres : {#each nombres as n}{n} {/each} !! +` + + const data = { + nombres : [1,1,2,3,5,8,13,21] + } + + 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 : 1 1 2 3 5 8 13 21  !! +`) + +}); + diff --git a/tests/fixtures/inline-if-nombres.odt b/tests/fixtures/inline-if-nombres.odt new file mode 100644 index 0000000000000000000000000000000000000000..66559cdf39c994f2ec44d2064c76def089df887a GIT binary patch literal 12489 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