Skip to content

feat(api): DocumentSession.pageIndex() — resolve anchors to pages#231

Merged
DemchaAV merged 1 commit into
developfrom
feat/page-index
Jun 25, 2026
Merged

feat(api): DocumentSession.pageIndex() — resolve anchors to pages#231
DemchaAV merged 1 commit into
developfrom
feat/page-index

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

Anchors and internal links shipped in v1.9, but there was no way to read back an anchor's resolved page — so a document couldn't print a "see page N" cross-reference, and a clickable table of contents had no source for its page numbers without a two-pass render-and-reparse hack.

What changed

  • DocumentSession.pageIndex()PageIndex (forAnchor / pageOf 0-based / pageNumberOf 1-based / all / totalPages) over PageReference(anchor, page) — new public types in document.snapshot. Resolves every declared anchor(...) to its final page in one pass.
  • Computed from the layout graph, not from rendered bytes. PageIndexExtractor reads the AnchorMarkerPayload marker fragments every anchored node already emits at its top-left — the same fragments the PDF backend turns into go-to destinations. So pageIndex() is backend-neutral and a duplicate anchor resolves to the same page linkTo(anchor) jumps to (last registration wins).
  • Cached per layout revision alongside layoutSnapshot() (new package-private cache slot). SessionChromeApi and the snapshot format are untouched — no PlacedNode field, no formatVersion bump, no baseline regen (the marker-fragment approach sidesteps all of it).
  • An anchor is a point, so PageReference carries a single page (no start/end).

Lane: canonical public API (read side) + shared-engine (reads existing fragments, no mutation). Purely additive → japicmp-safe.

Verification

  • ./mvnw test -pl .0 visual baselines changed.
  • PageIndexTest: multi-page resolution (intro→1, body→2), duplicate→last-wins, an anchored section split across pages resolving to its start page, zero-anchor and null-argument leniency (no NPE), and revision caching (recompute is correct after a mutation, not just a new instance).
  • PageIndexAnchorAgreementTest: renders to PDF and asserts the linkTo(anchor) go-to destination page equals pageOf(anchor) — pins the backend-neutral guarantee against future divergence.
  • Runnable PageReferenceExample (two-pass "see page N" cross-reference) with a committed preview and an examples README row.

@since 1.9.0 on all new public API. This is the read-side foundation for the upcoming native table-of-contents.

pageIndex() resolves every declared anchor(...) to its final page in a single,
backend-neutral pass over the laid-out document — pageNumberOf("intro") for a
"see page N" cross-reference, forAnchor(...) for the full PageReference. New
public PageReference(anchor, page) and PageIndex (forAnchor / pageOf /
pageNumberOf / all / totalPages) in document.snapshot; PageIndexExtractor reads
the AnchorMarkerPayload marker fragments every anchored node already emits, so
the result is computed from the layout graph (not from rendered bytes) and a
duplicate anchor resolves to the same destination linkTo(anchor) jumps to.
Cached per layout revision alongside layoutSnapshot(); SessionChromeApi and the
snapshot format are untouched (no PlacedNode field, no formatVersion bump).

Verified: ./mvnw test -pl . — 0 baselines changed. PageIndexTest covers
multi-page resolution, duplicate last-wins, a split section resolving to its
start page, zero-anchor and null-argument leniency, and revision caching;
PageIndexAnchorAgreementTest renders to PDF and asserts the go-to destination
page equals pageOf(anchor). A runnable PageReferenceExample (two-pass
cross-reference) ships with a committed preview.
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.

1 participant