diff --git a/scripts/odf/templating/fillOdtElementTemplate.js b/scripts/odf/templating/fillOdtElementTemplate.js index fd6995f..723398e 100644 --- a/scripts/odf/templating/fillOdtElementTemplate.js +++ b/scripts/odf/templating/fillOdtElementTemplate.js @@ -85,6 +85,8 @@ function findPlacesToFillInString(str, compartment) { * @returns {{startChild: Node, endChild:Node, content: DocumentFragment}} */ function extractBlockContent(blockStartNode, blockEndNode) { + //console.log('[extractBlockContent] blockEndNode', blockEndNode.textContent) + // find common ancestor of blockStartNode and blockEndNode let commonAncestor @@ -118,7 +120,10 @@ function extractBlockContent(blockStartNode, blockEndNode) { const startChild = startAncestryToCommonAncestor.at(-1) const endChild = endAncestryToCommonAncestor.at(-1) + //console.log('[extractBlockContent] endChild', endChild.textContent) + // Extract DOM content in a documentFragment + /** @type {DocumentFragment} */ const contentFragment = blockStartNode.ownerDocument.createDocumentFragment() /** @type {Element[]} */ @@ -135,6 +140,8 @@ function extractBlockContent(blockStartNode, blockEndNode) { contentFragment.appendChild(sibling) } + //console.log('extractBlockContent contentFragment', contentFragment.textContent) + return { startChild, endChild, @@ -231,8 +238,6 @@ function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, */ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, compartment) { //console.log('fillEachBlock', iterableExpression, itemExpression) - //console.log('startNode', startNode.nodeType, startNode.nodeName) - //console.log('endNode', endNode.nodeType, endNode.nodeName) const {startChild, endChild, content: repeatedFragment} = extractBlockContent(startNode, endNode) @@ -244,9 +249,13 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, c iterable = [] } + let firstItemFirstChild + let lastItemLastChild + // create each loop result // using a for-of loop to accept all iterable values for(const item of iterable) { + /** @type {DocumentFragment} */ // @ts-ignore const itemFragment = repeatedFragment.cloneNode(true) @@ -262,17 +271,67 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, c insideCompartment ) + if(!firstItemFirstChild){ + firstItemFirstChild = itemFragment.firstChild + } + + // eventually, will be set to the last item's last child + lastItemLastChild = itemFragment.lastChild + endChild.parentNode.insertBefore(itemFragment, endChild) } + // add before-text if any + const startNodePreviousSiblings = [] + let startNodePreviousSibling = startNode.previousSibling + while(startNodePreviousSibling){ + startNodePreviousSiblings.push(startNodePreviousSibling) + startNodePreviousSibling = startNodePreviousSibling.previousSibling + } + + // set the array back to tree order + startNodePreviousSiblings.reverse() + + if(startNodePreviousSiblings.length >= 1){ + let firstItemFirstestDescendant = firstItemFirstChild + while(firstItemFirstestDescendant?.firstChild){ + firstItemFirstestDescendant = firstItemFirstestDescendant.firstChild + } + + for(const beforeFirstNodeElement of startNodePreviousSiblings){ + firstItemFirstestDescendant?.parentNode?.insertBefore(beforeFirstNodeElement, firstItemFirstestDescendant) + } + } + + // add after-text if any + const endNodeNextSiblings = [] + let endNodeNextSibling = endNode.nextSibling + while(endNodeNextSibling){ + endNodeNextSiblings.push(endNodeNextSibling) + endNodeNextSibling = endNodeNextSibling.nextSibling + } + + if(endNodeNextSiblings.length >= 1){ + let lastItemLatestDescendant = lastItemLastChild + while(lastItemLatestDescendant?.lastChild){ + lastItemLatestDescendant = lastItemLatestDescendant.lastChild + } + + for(const afterEndNodeElement of endNodeNextSiblings){ + lastItemLatestDescendant?.parentNode?.appendChild(afterEndNodeElement) + } + } + + // remove block marker elements startChild.parentNode.removeChild(startChild) endChild.parentNode.removeChild(endChild) + } -const IF = 'IF' -const EACH = 'EACH' +const IF = ifStartMarkerRegex.source +const EACH = eachStartMarkerRegex.source /** * diff --git a/scripts/odf/templating/prepareTemplateDOMTree.js b/scripts/odf/templating/prepareTemplateDOMTree.js index b631ca8..698e823 100644 --- a/scripts/odf/templating/prepareTemplateDOMTree.js +++ b/scripts/odf/templating/prepareTemplateDOMTree.js @@ -1,3 +1,5 @@ +//@ts-check + import {traverse, Node} from "../../DOMUtils.js"; import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, variableRegex} from './markers.js' @@ -39,7 +41,7 @@ function findAllMatches(text, pattern) { * * @param {Node} node1 * @param {Node} node2 - * @returns {Node | undefined} + * @returns {Node} */ function findCommonAncestor(node1, node2) { const ancestors1 = getAncestors(node1); @@ -51,7 +53,7 @@ function findCommonAncestor(node1, node2) { } } - return undefined; + throw new Error(`node1 and node2 do not have a common ancestor`) } /** @@ -281,6 +283,8 @@ function consolidateMarkers(document){ newStartNode.parentNode?.removeChild(newStartNode) commonAncestor.insertBefore(newStartNode, commonAncestorStartChild.nextSibling) + + //console.log('commonAncestor after before-text split', commonAncestor.textContent ) } @@ -299,11 +303,15 @@ function consolidateMarkers(document){ endNode.parentNode?.removeChild(endNode) commonAncestor.insertBefore(endNode, commonAncestorEndChild) } + + //console.log('commonAncestor after after-text split', commonAncestor.textContent ) } // then, replace all nodes between (new)startNode and (new)endNode with a single textNode in commonAncestor replaceBetweenNodesWithText(newStartNode, endNode, positionedMarker.marker) + //console.log('commonAncestor after replaceBetweenNodesWithText', commonAncestor.textContent ) + // After consolidation, break as the DOM structure has changed // and containerTextNodesInTreeOrder needs to be refreshed consolidatedMarkers.push(positionedMarker) @@ -316,13 +324,29 @@ function consolidateMarkers(document){ } } +/** + * @typedef {typeof closingIfMarker | typeof eachClosingMarker | typeof eachStartMarkerRegex.source | typeof elseMarker | typeof ifStartMarkerRegex.source | typeof variableRegex.source} MarkerType + */ + +/** + * @typedef {Object} MarkerNode + * @prop {Node} node + * @prop {MarkerType} markerType + */ + /** * isolate markers which are in Text nodes with other texts * * @param {Document} document + * @returns {Map} */ -function isolateMarkers(document){ +function isolateMarkerText(document){ + /** @type {ReturnType} */ + const markerNodes = new Map() + traverse(document, currentNode => { + //console.log('isolateMarkers', currentNode.nodeName, currentNode.textContent) + if(currentNode.nodeType === Node.TEXT_NODE) { // find all marker starts and ends and split textNode let remainingText = currentNode.textContent || '' @@ -330,6 +354,8 @@ function isolateMarkers(document){ while(remainingText.length >= 1) { let matchText; let matchIndex; + /** @type {MarkerType} */ + let markerType; // looking for a block marker for(const marker of [ifStartMarkerRegex, elseMarker, closingIfMarker, eachStartMarkerRegex, eachClosingMarker]) { @@ -339,6 +365,7 @@ function isolateMarkers(document){ if(index !== -1) { matchText = marker matchIndex = index + markerType = marker // found the first match break; // get out of loop @@ -351,6 +378,7 @@ function isolateMarkers(document){ if(match) { matchText = match[0] matchIndex = match.index + markerType = marker.source // found the first match break; // get out of loop @@ -373,11 +401,21 @@ function isolateMarkers(document){ // per spec, currentNode now contains before-match and match text + /** @type {Node} */ + let matchTextNode + // @ts-ignore if(matchIndex > 0) { // @ts-ignore - currentNode.splitText(matchIndex) + matchTextNode = currentNode.splitText(matchIndex) } + else{ + matchTextNode = currentNode + } + + markerNodes.set(matchTextNode, markerType) + + // per spec, currentNode now contains only before-match text if(afterMatchTextNode) { currentNode = afterMatchTextNode @@ -397,8 +435,100 @@ function isolateMarkers(document){ // skip } }) + + return markerNodes } + + +/** + * after isolateMatchingMarkersStructure, matching markers (opening/closing each, if/then/closing if) + * are put in isolated branches within their common ancestors + * + * UNFINISHED - maybe another day if relevant + * + * @param {Document} document + * @param {Map} markerNodes + */ +//function isolateMatchingMarkersStructure(document, markerNodes){ + /** @type {MarkerNode[]} */ +/* let currentlyOpenBlocks = [] + + traverse(document, currentNode => { + + const markerType = markerNodes.get(currentNode) + + if(markerType){ + switch(markerType){ + case eachStartMarkerRegex.source: + case ifStartMarkerRegex.source: { + currentlyOpenBlocks.push({ + node: currentNode, + markerType + }) + break; + } + case eachClosingMarker: { + const lastOpenedBlockMarkerNode = currentlyOpenBlocks.pop() + + if(!lastOpenedBlockMarkerNode) + throw new Error(`{/each} found without corresponding opening {#each x as y}`) + + if(lastOpenedBlockMarkerNode.markerType !== eachStartMarkerRegex.source) + throw new Error(`{/each} found while the last opened block was not an opening {#each x as y} (it was a ${lastOpenedBlockMarkerNode.markerType})`) + + const openingEachNode = lastOpenedBlockMarkerNode.node + const closingEachNode = currentNode + + const commonAncestor = findCommonAncestor(openingEachNode, closingEachNode) + + if(openingEachNode.parentNode !== commonAncestor && openingEachNode.parentNode.childNodes.length >= 2){ + if(openingEachNode.previousSibling){ + // create branch for previousSiblings + let previousSibling = openingEachNode.previousSibling + const previousSiblings = [] + while(previousSibling){ + previousSiblings.push(previousSibling.previousSibling) + previousSibling = previousSibling.previousSibling + } + + // put previous siblings in tree order + previousSiblings.reverse() + + const parent = openingEachNode.parentNode + const parentClone = parent.cloneNode(false) + for(const previousSibling of previousSiblings){ + previousSibling.parentNode.removeChild(previousSibling) + parentClone.appendChild(previousSibling) + } + + let openingEachNodeBranch = openingEachNode.parentNode + let branchForPreviousSiblings = parentClone + + while(openingEachNodeBranch.parentNode !== commonAncestor){ + const newParentClone = openingEachNodeBranch.parentNode.cloneNode(false) + branchForPreviousSiblings.parentNode.removeChild(branchForPreviousSiblings) + newParentClone.appendChild(branchForPreviousSiblings) + } + } + } + + + + + break; + } + + default: + throw new TypeError(`MarkerType not recognized: '${markerType}`) + } + } + + }) + +}*/ + + /** * 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: @@ -415,5 +545,11 @@ function isolateMarkers(document){ */ export default function prepareTemplateDOMTree(document){ consolidateMarkers(document) - isolateMarkers(document) + // after consolidateMarkers, each marker is in at most one text node + // (formatting with markers is removed) + + 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 8c9cc43..f0ede37 100644 --- a/tests/fill-odt-template/each.js +++ b/tests/fill-odt-template/each.js @@ -247,6 +247,42 @@ Hiver }); +test('template filling with text after {/each} in same text node', async t => { + const templatePath = join(import.meta.dirname, '../fixtures/text-after-closing-each.odt') + const templateContent = `Légumes de saison + +{#each légumes as légume} +{légume}, +{/each} en {saison} +` + + const data = { + saison: 'Printemps', + légumes: [ + 'Asperge', + 'Betterave', + 'Blette' + ] + } + + 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, `Légumes de saison + +Asperge, +Betterave, +Blette, en Printemps +`) + +}); + + 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/fixtures/text-after-closing-each.odt b/tests/fixtures/text-after-closing-each.odt new file mode 100644 index 0000000..b452f15 Binary files /dev/null and b/tests/fixtures/text-after-closing-each.odt differ