Files
Reactive-Resume/patches/@react-pdf__layout@5.2.0.patch
T
Amruth Pillai ea2beb8450 fix(pdf): keep list markers with their first text fragment (#3443)
* fix(pdf): keep list markers with their first text fragment

* fix(pdf): preserve page breaks while rewinding list companions

* fix(pdf): consume oversized list marker presence hints

* test(pdf): allow cold startup for pagination process guard

* fix(pdf): key list presence spacer
2026-09-05 09:51:09 -07:00

71 lines
4.6 KiB
Diff

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),