diff --git a/lib/index.js b/lib/index.js index 18c2618f4630b3c1296c8c3cac317b8d7666bbe8..79e1898aadcbbfc78a206e235d868cb178683a07 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3163,7 +3163,10 @@ const splitNodes = (height, contentArea, nodes) => { if (shouldSplit) { const [currentChild, nextChild] = split(child, height, contentArea); // All children are moved to the next page, it doesn't make sense to show the parent on the current page - if (child.children.length > 0 && currentChild.children.length === 0) { + // Tagged list companions can intentionally rewind all their children. + // Keep their empty fragment and already-consumed break state instead + // of restoring the unsplit row (which can exceed a whole page). + if (child.children.length > 0 && currentChild.children.length === 0 && !child.props['data-resume-list-item']) { // But if the current page is empty then we can just include the parent on the current page if (currentChildren.length === 0) { currentChildren.push(child, ...futureFixedNodes); @@ -3196,7 +3199,52 @@ const splitChildren = (height, contentArea, node) => { }; const splitView = (node, height, contentArea) => { const [currentNode, nextNode] = splitNode(node, height); - const [currentChilds, nextChildren] = splitChildren(height, contentArea, node); + let [currentChilds, nextChildren] = splitChildren(height, contentArea, node); + // Reactive Resume opts list rows into companion pagination. Use the text + // fragments after orphan/widow splitting, not estimated font line heights. + // Untagged rows and ordinary absolutely positioned nodes are unaffected. + if (node.props['data-resume-list-item']) { + const hasText = (child) => child && (child.type === P.Text + ? child.lines?.length > 0 + : child.children?.some(hasText)); + const content = (children) => children.find((child) => child.props['data-resume-list-content']); + const markerIndex = node.children.findIndex((child) => child.props['data-resume-list-marker']); + const contentIndex = node.children.findIndex((child) => child.props['data-resume-list-content']); + const hasMarker = (children) => children.some((child) => child.props['data-resume-list-marker']); + const deferredMarker = nextChildren.find((child) => child.props['data-resume-list-marker']); + // Presence is advisory once the whole row moves to a fresh page. Consume + // the hint on continuation so an impossible window cannot rewind the + // same unsplit content indefinitely (including content-first rows). + const continuedMarkerProps = (marker) => ({ + ...(deferredMarker?.props || marker.props), + minPresenceAhead: 0, + }); + if (markerIndex !== -1 && !hasMarker(currentChilds) && hasMarker(nextChildren) && hasText(content(currentChilds))) { + // A presence hint or page break can defer a marker after content. + // Defer unsplit content while retaining the marker's consumed flags. + currentChilds = currentChilds.filter((child) => !child.props['data-resume-list-content']); + nextChildren = node.children + .filter((child) => child.props['data-resume-list-content'] || child.props['data-resume-list-marker']) + .map((child) => child.props['data-resume-list-marker'] + ? { ...child, props: continuedMarkerProps(child) } + : child); + } else if (markerIndex !== -1 && !hasText(content(currentChilds)) && hasText(content(nextChildren))) { + // The first text did not fit. Preserve the authored marker/content + // order when moving the marker to the next fragment of this row. + const marker = node.children[markerIndex]; + // Preserve break: false after splitNodes has consumed an explicit + // page break; restoring the original props would replay it. + const nextMarker = { + ...marker, + props: continuedMarkerProps(marker), + box: { ...marker.box, top: 0 }, + }; + currentChilds = currentChilds.filter((child) => !child.props['data-resume-list-marker']); + nextChildren = nextChildren.filter((child) => !child.props['data-resume-list-marker']); + if (markerIndex < contentIndex) nextChildren.unshift(nextMarker); + else nextChildren.push(nextMarker); + } + } return [ assingChildren(currentChilds, currentNode), assingChildren(nextChildren, nextNode),