after-text in #each block (#7)

* Text 'text after {/each} in same text node' failing

* some tests passing

* tests passing

* unused var

* styling
This commit is contained in:
David Bruant 2025-05-09 18:48:34 +02:00 committed by GitHub
parent 6ad0bab069
commit 5400c963a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 240 additions and 9 deletions

View File

@ -85,6 +85,8 @@ function findPlacesToFillInString(str, compartment) {
* @returns {{startChild: Node, endChild:Node, content: DocumentFragment}} * @returns {{startChild: Node, endChild:Node, content: DocumentFragment}}
*/ */
function extractBlockContent(blockStartNode, blockEndNode) { function extractBlockContent(blockStartNode, blockEndNode) {
//console.log('[extractBlockContent] blockEndNode', blockEndNode.textContent)
// find common ancestor of blockStartNode and blockEndNode // find common ancestor of blockStartNode and blockEndNode
let commonAncestor let commonAncestor
@ -118,7 +120,10 @@ function extractBlockContent(blockStartNode, blockEndNode) {
const startChild = startAncestryToCommonAncestor.at(-1) const startChild = startAncestryToCommonAncestor.at(-1)
const endChild = endAncestryToCommonAncestor.at(-1) const endChild = endAncestryToCommonAncestor.at(-1)
//console.log('[extractBlockContent] endChild', endChild.textContent)
// Extract DOM content in a documentFragment // Extract DOM content in a documentFragment
/** @type {DocumentFragment} */
const contentFragment = blockStartNode.ownerDocument.createDocumentFragment() const contentFragment = blockStartNode.ownerDocument.createDocumentFragment()
/** @type {Element[]} */ /** @type {Element[]} */
@ -135,6 +140,8 @@ function extractBlockContent(blockStartNode, blockEndNode) {
contentFragment.appendChild(sibling) contentFragment.appendChild(sibling)
} }
//console.log('extractBlockContent contentFragment', contentFragment.textContent)
return { return {
startChild, startChild,
endChild, endChild,
@ -231,8 +238,6 @@ function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode,
*/ */
function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, compartment) { function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, compartment) {
//console.log('fillEachBlock', iterableExpression, itemExpression) //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) const {startChild, endChild, content: repeatedFragment} = extractBlockContent(startNode, endNode)
@ -244,9 +249,13 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, c
iterable = [] iterable = []
} }
let firstItemFirstChild
let lastItemLastChild
// create each loop result // create each loop result
// using a for-of loop to accept all iterable values // using a for-of loop to accept all iterable values
for(const item of iterable) { for(const item of iterable) {
/** @type {DocumentFragment} */ /** @type {DocumentFragment} */
// @ts-ignore // @ts-ignore
const itemFragment = repeatedFragment.cloneNode(true) const itemFragment = repeatedFragment.cloneNode(true)
@ -262,17 +271,67 @@ function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, c
insideCompartment 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) 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 // remove block marker elements
startChild.parentNode.removeChild(startChild) startChild.parentNode.removeChild(startChild)
endChild.parentNode.removeChild(endChild) endChild.parentNode.removeChild(endChild)
} }
const IF = 'IF' const IF = ifStartMarkerRegex.source
const EACH = 'EACH' const EACH = eachStartMarkerRegex.source
/** /**
* *

View File

@ -1,3 +1,5 @@
//@ts-check
import {traverse, Node} from "../../DOMUtils.js"; import {traverse, Node} from "../../DOMUtils.js";
import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, variableRegex} from './markers.js' import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex, variableRegex} from './markers.js'
@ -39,7 +41,7 @@ function findAllMatches(text, pattern) {
* *
* @param {Node} node1 * @param {Node} node1
* @param {Node} node2 * @param {Node} node2
* @returns {Node | undefined} * @returns {Node}
*/ */
function findCommonAncestor(node1, node2) { function findCommonAncestor(node1, node2) {
const ancestors1 = getAncestors(node1); 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) newStartNode.parentNode?.removeChild(newStartNode)
commonAncestor.insertBefore(newStartNode, commonAncestorStartChild.nextSibling) 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) endNode.parentNode?.removeChild(endNode)
commonAncestor.insertBefore(endNode, commonAncestorEndChild) 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 // then, replace all nodes between (new)startNode and (new)endNode with a single textNode in commonAncestor
replaceBetweenNodesWithText(newStartNode, endNode, positionedMarker.marker) replaceBetweenNodesWithText(newStartNode, endNode, positionedMarker.marker)
//console.log('commonAncestor after replaceBetweenNodesWithText', commonAncestor.textContent )
// After consolidation, break as the DOM structure has changed // After consolidation, break as the DOM structure has changed
// and containerTextNodesInTreeOrder needs to be refreshed // and containerTextNodesInTreeOrder needs to be refreshed
consolidatedMarkers.push(positionedMarker) 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 * isolate markers which are in Text nodes with other texts
* *
* @param {Document} document * @param {Document} document
* @returns {Map<Node, MarkerType>}
*/ */
function isolateMarkers(document){ function isolateMarkerText(document){
/** @type {ReturnType<isolateMarkerText>} */
const markerNodes = new Map()
traverse(document, currentNode => { traverse(document, currentNode => {
//console.log('isolateMarkers', currentNode.nodeName, currentNode.textContent)
if(currentNode.nodeType === Node.TEXT_NODE) { if(currentNode.nodeType === Node.TEXT_NODE) {
// find all marker starts and ends and split textNode // find all marker starts and ends and split textNode
let remainingText = currentNode.textContent || '' let remainingText = currentNode.textContent || ''
@ -330,6 +354,8 @@ function isolateMarkers(document){
while(remainingText.length >= 1) { while(remainingText.length >= 1) {
let matchText; let matchText;
let matchIndex; let matchIndex;
/** @type {MarkerType} */
let markerType;
// looking for a block marker // looking for a block marker
for(const marker of [ifStartMarkerRegex, elseMarker, closingIfMarker, eachStartMarkerRegex, eachClosingMarker]) { for(const marker of [ifStartMarkerRegex, elseMarker, closingIfMarker, eachStartMarkerRegex, eachClosingMarker]) {
@ -339,6 +365,7 @@ function isolateMarkers(document){
if(index !== -1) { if(index !== -1) {
matchText = marker matchText = marker
matchIndex = index matchIndex = index
markerType = marker
// found the first match // found the first match
break; // get out of loop break; // get out of loop
@ -351,6 +378,7 @@ function isolateMarkers(document){
if(match) { if(match) {
matchText = match[0] matchText = match[0]
matchIndex = match.index matchIndex = match.index
markerType = marker.source
// found the first match // found the first match
break; // get out of loop break; // get out of loop
@ -373,11 +401,21 @@ function isolateMarkers(document){
// per spec, currentNode now contains before-match and match text // per spec, currentNode now contains before-match and match text
/** @type {Node} */
let matchTextNode
// @ts-ignore // @ts-ignore
if(matchIndex > 0) { if(matchIndex > 0) {
// @ts-ignore // @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) { if(afterMatchTextNode) {
currentNode = afterMatchTextNode currentNode = afterMatchTextNode
@ -397,8 +435,100 @@ function isolateMarkers(document){
// skip // 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<Node, MarkerType>} 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 * 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: * 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){ export default function prepareTemplateDOMTree(document){
consolidateMarkers(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)
} }

View File

@ -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 => { test('template filling of a table', async t => {
const templatePath = join(import.meta.dirname, '../fixtures/tableau-simple.odt') const templatePath = join(import.meta.dirname, '../fixtures/tableau-simple.odt')
const templateContent = `Évolution énergie en kWh par personne en France const templateContent = `Évolution énergie en kWh par personne en France

Binary file not shown.