Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
1bed2d0
feat(layout): footnote-aware body pagination (SD-3049/3050/3051)
tupizz May 11, 2026
b3b89b6
feat(footnote): honor w:numFmt / w:numStart + customMarkFollows (SD-2…
tupizz May 11, 2026
cb1ca5a
docs(footnote): sd-2656 plan + implementation report
tupizz May 11, 2026
cc4eac4
Merge branch 'main' into tadeu/sd-2656-feature-footnote-rendering-fid…
tupizz May 14, 2026
dce9811
fix(footnote): close review gaps in SD-2656 (demand recharge, endnote…
tupizz May 15, 2026
7957529
fix(footnote): list demand + customMark suppresses body marker (SD-2656)
tupizz May 15, 2026
7548481
Merge branch 'main' into tadeu/sd-2656-feature-footnote-rendering-fid…
tupizz May 15, 2026
64f2059
fix(footnote): dedupe block demand by footnote id (SD-2656)
tupizz May 15, 2026
88f6a3f
fix(footnote): charge block demand once, on anchor page (SD-2656)
tupizz May 15, 2026
b873f5f
Merge branch 'main' into tadeu/sd-2656-feature-footnote-rendering-fid…
tupizz May 18, 2026
5beba69
fix(footnote): flip separator widths to match ECMA-376 (SD-2985)
tupizz May 18, 2026
bbd7eda
fix(footnote): customMark refs do not consume an ordinal (SD-2986/SD-…
tupizz May 18, 2026
e2fb857
fix(endnote): suppress body marker for customMark refs (parity with f…
tupizz May 18, 2026
0f8435d
feat(footnote): honor section-level w:footnotePr + numRestart=eachSec…
tupizz May 18, 2026
57c4046
feat(footnote): numRestart=eachPage counter math (helper) (SD-2986)
tupizz May 18, 2026
bf68627
Merge branch 'tadeu/sd-2656-feature-footnote-rendering-fidelity' of g…
tupizz May 18, 2026
181022b
feat(footnote): classify imported separator + continuationNotice cont…
tupizz May 18, 2026
f739373
feat(footnote): read + plumb w:pos placement attribute (SD-2986)
tupizz May 18, 2026
dd941d3
Merge branch 'main' into tadeu/sd-2656-feature-footnote-rendering-fid…
tupizz May 18, 2026
9d91802
fix(footnote): marker is plain superscript + gap before body (SD-2656)
tupizz May 18, 2026
314f1d6
Merge branch 'tadeu/sd-2656-feature-footnote-rendering-fidelity' of g…
tupizz May 18, 2026
6a38dd8
Merge remote-tracking branch 'origin/main' into tadeu/sd-2656-feature…
tupizz May 20, 2026
7548d28
feat(layout-engine): range-aware footnote demand + bodyMaxY-anchored …
tupizz May 20, 2026
e138776
chore: remove internal SD-2656 planning docs from branch
tupizz May 20, 2026
7b5a3e8
Merge branch 'main' into tadeu/sd-2656-feature-footnote-rendering-fid…
tupizz May 20, 2026
31d2173
fix(footnote): bottom-anchor band painting to match Word convention (…
tupizz May 21, 2026
4a3daaf
Merge branch 'main' of github.com:superdoc-dev/superdoc into tadeu/sd…
tupizz May 21, 2026
5ed53ee
fix(footnote): address PR review comments (SD-2656)
tupizz May 21, 2026
a743c9a
feat(footnote): split-aware pagination + minimum-start demand model (…
tupizz May 21, 2026
854a012
feat(footnote): ordered-cluster rule for anchor placement (SD-2656)
tupizz May 22, 2026
71362b9
Revert "feat(footnote): ordered-cluster rule for anchor placement (SD…
tupizz May 22, 2026
d7819fe
Revert "feat(footnote): split-aware pagination + minimum-start demand…
tupizz May 22, 2026
941f1a7
feat(footnote): ordered-cluster pagination + caps marker rendering (S…
tupizz May 22, 2026
57ac386
feat(footnote): phase 0 page ledger + invariant diagnostics (SD-2656)
tupizz May 22, 2026
2311521
feat(footnote): phase 1 body acceptance uses ordered minimum (SD-2656)
tupizz May 22, 2026
ad9c2e9
feat(footnote): phase 3 bounded continuation draining (SD-2656)
tupizz May 22, 2026
768e6c4
feat(footnote): phase 4 reserve shrink reclaims dead reserve (SD-2656)
tupizz May 22, 2026
3e18d51
chore(footnote): refresh analyzer diff outputs after phase 4 (SD-2656)
tupizz May 22, 2026
533cdc2
feat(footnote): preferred-reserve and last-anchor-lines telemetry (SD…
tupizz May 22, 2026
4e92dd4
refactor(footnote): clarify preferred reserve scoring
tupizz May 22, 2026
9d2b213
chore(footnote): keep IT-923 analyzer and plan doc local-only (SD-2656)
tupizz May 25, 2026
9700b46
chore(footnote): resolve PresentationEditor merge with main (SD-2656)
tupizz May 25, 2026
d4a8353
chore(footnote): merge main and port caps marker to refactored list-m…
tupizz May 27, 2026
6e46bd2
fix(footnote): broaden preferred-reserve candidate filter for partial…
tupizz May 27, 2026
de79693
fix(footnote): allow extra dead-reserve when trial eliminates a split…
tupizz May 27, 2026
4a264d8
fix(footnote): include continuationIn in mandatory and preferred rese…
tupizz May 27, 2026
47a9f9f
chore(footnote): merge main into SD-2656 branch and update note build…
tupizz Jun 1, 2026
4753186
fix(footnote): allow +1 page when trial eliminates a cluster split (S…
tupizz Jun 1, 2026
3ea690c
test(footnote): update parity test import after layout-adapter rename
tupizz Jun 1, 2026
2cc68fa
fix(footnote): three correctness issues found in code review (SD-2656)
tupizz Jun 1, 2026
a1e1f9a
feat(footnote): absorb one-line footnote widows by bumping reserve (S…
tupizz Jun 1, 2026
763801c
feat(footnote): reserve full footnote demand at body slice time (SD-2…
tupizz Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions packages/layout-engine/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1927,6 +1927,64 @@ export type Measure =
| ColumnBreakMeasure;

/** A rendered page containing positioned fragments. Page numbers are 1-indexed. */
/**
* SD-2656: per-page footnote planning ledger.
*
* The single source of truth that body pagination, footnote placement, and
* continuation carry must all agree on. Without it the three subsystems read
* different numbers (body reserves X, planner paints Y, carry-forward thinks
* Z) and the resulting drift compounds across the document.
*
* Mandatory invariants checked by `tools/sd-2656-footnote-analyzer`:
* 1. `actualBandHeight <= appliedBodyReserve` (band fits)
* 2. `mandatorySlices` always equals `full(non-last) + firstLine(last)` of
* the page's anchored cluster (rule).
* 3. `continuationIn[P]` matches `continuationOut[P-1]` (carry parity).
* 4. `deadReserve = appliedBodyReserve - actualBandHeight` is small (drift
* fuel above ~30 px is a planning bug).
*/
export type FootnoteContinuationEntry = {
Comment thread
tupizz marked this conversation as resolved.
/** Footnote id (OOXML id, not the Word visible number). */
id: string;
/** How many ranges remain to render. */
remainingRangeCount: number;
/** Total height of the remaining ranges. */
remainingHeightPx: number;
};

export type FootnotePageLedger = {
pageIndex: number;
/** Ordered footnote ids whose body refs are anchored on this page. */
anchorIds: string[];
/** Slices required by the rule: full of non-last + firstLine of last. */
mandatorySliceIds: string[];
/** Slices for content drained from prior pages. */
continuationSliceIds: string[];
/** Slices for last-anchor content beyond firstLine (rendered only if there
* is leftover space after mandatory + continuation). */
extendedSliceIds: string[];
/** Continuations arriving from page-1. */
continuationIn: FootnoteContinuationEntry[];
/** Continuations deferred to page+1. */
continuationOut: FootnoteContinuationEntry[];
/** Mandatory-reserve px: mandatorySlices height + overhead. */
mandatoryReservePx: number;
/** SD-2656 Phase 7: Word-like "preferred" reserve px. Body slicer is allowed
* to reserve this much when doing so does not cause cluster spill or
* continuation overflow. = full(non-last) + asMuchAsFits(last) + overhead. */
preferredReservePx: number;
/** Total painted band height in px, including separator + gaps. */
actualBandHeightPx: number;
/** Body's applied reserve (i.e. `page.footnoteReserved`) for this page. */
appliedBodyReservePx: number;
/** appliedBodyReservePx - actualBandHeightPx — wasted body area. */
deadReservePx: number;
/** Number of measured lines actually rendered for the LAST anchor on this
* page (0 if there is no cluster anchor). Used to flag "mandatory-only"
* pages where Word would have rendered more. */
lastAnchorRenderedLines: number;
};

export type Page = {
number: number;
fragments: Fragment[];
Expand All @@ -1937,6 +1995,12 @@ export type Page = {
* decoration boxes anchored to the real bottom margin while the body shrinks.
*/
footnoteReserved?: number;
/**
* SD-2656: page-level footnote planning ledger. Populated by the layout
* bridge when footnotes are present. Read by the diagnostic toolkit and
* (in later phases) by body pagination itself.
*/
footnoteLedger?: FootnotePageLedger;
numberText?: string;
size?: { w: number; h: number };
orientation?: 'portrait' | 'landscape';
Expand Down
8 changes: 8 additions & 0 deletions packages/layout-engine/contracts/src/resolved-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,14 @@ export type ResolvedListMarkerItem = {
italic?: boolean;
color?: string;
letterSpacing?: number;
/**
* SD-2656: caps marks from the level rPr ( w:caps / w:smallCaps ). When
* `allCaps` is true the painter applies CSS text-transform: uppercase to
* the marker text — matching Word's legal/contract list rendering
* ("FIRST:", "SECOND:", "THIRD:") for `ordinalText` numbering.
*/
allCaps?: boolean;
smallCaps?: boolean;
};
/** Optional DOCX source evidence for list-marker observations. */
sourceAnchor?: SourceAnchor;
Expand Down
Loading
Loading