Skip to content

feat(api): addPageReference(anchor) — native "see page N" cross-reference#236

Merged
DemchaAV merged 1 commit into
developfrom
feat/toc
Jun 25, 2026
Merged

feat(api): addPageReference(anchor) — native "see page N" cross-reference#236
DemchaAV merged 1 commit into
developfrom
feat/toc

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

Printing "see page N" (or a table-of-contents number) required a manual two-pass: lay the document out into a throwaway session, read pageIndex(), then re-render the whole document with the number substituted. addPageReference(anchor) makes it a single authoring pass — the engine resolves the number from the laid-out document and renders it.

This is the engine foundation split out of the table-of-contents work: the page-reference primitive + the two-pass resolve (a follow-up assembles addTableOfContents on top).

What changed

  • addPageReference(anchor) on flows, and on rows (the number column of a TOC entry) → PageReferenceNode, a text leaf that prints the 1-based page its anchor(...) lands on.
  • Two-pass resolve, to a fixed point. A document containing a page reference is compiled twice (pass 1 resolves anchor→page; pass 2 renders the numbers), then re-resolved until the pages stop moving (capped) — so a reference whose own width re-wraps a neighbour and shifts a page is corrected, not left stale. The reference reserves only its glyph width (a single text line) in both passes, so its footprint does not shift the pages it reports; the canonical TOC layout converges in one recompile.
  • Backend-neutral & non-invasive. The resolved page flows to the leaf through a default accessor on the layout context (resolvedPage(anchor), empty by default), so existing node definitions are untouched. Documents without a page reference compile once and are byte-identical.
  • pageIndex() remains for programmatic access.

Lane: shared-engine layout (two-pass + the context accessor, both @Internal/@Beta SPI) + canonical DSL/node (addPageReference, PageReferenceNode). Additive → japicmp-safe.

Verification

  • ./mvnw test -pl .green, 0 visual baselines changed.
  • PageReferenceTest: a forward reference (on page 1, target on page 2) prints the resolved page — asserted via PDFTextStripper to equal pageIndex().pageNumberOf; multiple references each report their own final page; an unresolved anchor renders without throwing.
  • A 2-lens cold review (two-pass determinism + API/architecture) drove a fix: the resolve now converges to a fixed point instead of assuming two passes suffice.
  • PageReferenceExample rewritten from the manual two-pass to the native one-pass form (a dot-leader "Appendix ···· 2" entry), with a committed preview.

Next: addTableOfContents(...) assembles these references + dotted leaders (line().fill()) + auto/weight columns into a clickable, page-numbered TOC.

.margin(DocumentInsets.of(24))
.create()) {
session.pageFlow(page -> {
page.addRow(r -> r.gap(4).columns(com.demcha.compose.document.style.DocumentRowColumn.auto(),
.margin(DocumentInsets.of(24))
.create()) {
session.pageFlow(page -> {
page.addRow(r -> r.gap(4).addParagraph("Appendix")
session.pageFlow(page -> {
page.addRow(r -> r.gap(4).addParagraph("Appendix")
.addPageReference("appendix", DocumentTextStyle.DEFAULT, TextAlign.RIGHT));
page.addRow(r -> r.gap(4).addParagraph("Glossary")
…ence

A "see page N" cross-reference needed a manual two-pass: lay the document
out, read pageIndex(), then re-render with the number substituted.
addPageReference(anchor) does it in one authoring pass — the engine
resolves the number from the laid-out document and renders it.

A document that contains a page reference is compiled in two passes (the
first resolves every anchor's page, the next renders the references), then
re-resolved to a fixed point so a reference whose own width re-wraps a
neighbour and shifts a page is corrected rather than left stale; the
resolve is capped. The reference reserves only its glyph width, so its
footprint does not shift the pages it reports. Documents without a page
reference compile once and are byte-identical. PageReferenceNode is a text
leaf; the resolved page flows to it through a default layout-context
accessor, so existing node definitions are untouched.

Available on flows and inside rows (the number column of a
table-of-contents row); pageIndex() remains for programmatic access.

Tests: PageReferenceTest covers a forward reference printing the resolved
page (via PDFTextStripper, equal to pageIndex().pageNumberOf), multiple
references each reporting their final page, and an unresolved anchor
rendering without throwing. Example: PageReferenceExample rewritten to the
native one-pass form (a dot-leader entry). Full suite green, no visual
baselines changed.
@DemchaAV DemchaAV merged commit 3cf16cd into develop Jun 25, 2026
11 checks passed
@DemchaAV DemchaAV deleted the feat/toc branch June 25, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants