html: register element ids as PDF named destinations#422
Open
kuadrosx wants to merge 1 commit into
Open
Conversation
Internal anchor links (<a href="#section">) rendered as clickable link annotations but navigated nowhere: folio never registered an HTML element's id as a PDF named destination, so each /GoTo pointed at a target that was never defined and the /Dests catalog was empty. An element carrying an id is now wrapped in a layout anchor marker that tags its first PlacedBlock with the id. The renderer surfaces these on PageResult.Anchors and buildAllPages registers each as a named destination (offset by manualPageCount so it lands on the right absolute page), feeding the existing /Dests writer and the link-annotation resolver — no caller AddNamedDest needed. The resolver and writer share one destination-array builder so an internal link resolves to a direct /Dest rather than a dangling string /GoTo. Destinations use /XYZ at the target's y with zoom retained — the same shape auto-bookmarks use — so clicking a link and clicking an outline entry to the same target behave identically instead of the link forcing a fit-width zoom. On a duplicate name (two equal ids, or an id colliding with a caller's AddNamedDest) the /Dests writer keeps the first registration, matching the resolver's first-match so both agree. The anchor and bookmark markers expose an unwrap() and the layouter resolves optional interfaces through baseElement(), so decorating an element to record its id no longer masks Clearable, KeepTogether, HeightSettable, or the column layoutable check — previously wrapping any element with an id silently disabled CSS clear, page-break-inside: avoid, flex cross-axis stretch, and column layout on it. This also fixes the same latent masking in the pre-existing bookmark wrapper. Inline targets (<span id>, the <a id>/<a name> idiom) remain out of scope; only block-level elements register a destination.
kuadrosx
force-pushed
the
fix/internal-anchor-destinations
branch
from
July 12, 2026 02:14
1b17d01 to
b408cca
Compare
kuadrosx
marked this pull request as ready for review
July 12, 2026 02:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
<a href="#id">links render as clickable annotations but navigate nowhere: an element'sidis consumed only for CSS selector matching, so nothing registers it as a PDF named destination. The link emits a dangling/GoTo (id)string action pointing at a destination that is never defined — the generated PDF has an empty/Dests.This is the behavior the
## [0.8.0]CHANGELOG entry (#223) describes as fixed ("layout.Anchorelement + auto-registered PDF named destinations"), but the implementing symbols (layout.Anchor,PageResult.Anchors,LinkDestName) are absent from the tree — the entry outlived the code (reverted or never merged).Fix
Re-implements the
id → named destinationpath by mirroring the existingbookmarkAnchormechanism:idattr → wrap element in alayoutanchor marker → marker tags its firstPlacedBlock.AnchorName→ renderer surfacesPageResult.Anchors→ the document layer registers them viaAddNamedDestduring layout → the existing/Destswriter + annotation resolver emit the destination.layout/anchor.go(new) —NewAnchor(inner, name), a thin pass-through wrapper modeled onbookmark.go; delegatesMeasurableso flex / table-cell shrink-to-fit is unaffected.layout—PlacedBlock.AnchorName,PageResult.Anchors(AnchorInfo{Name, Y}), recorded indrawBlockNestedat the block's topy.html/converter_dispatch.go— wrap the produced element when the node carries anid(block-element dispatch, so headings/divs/sections are covered uniformly).document/document.go— register each anchor as aNamedDest(FitHat the targety). Extracts a sharedbuildNamedDestArrayso the annotation resolver honorsFitH/Top(links land on the section, not the page top) and agrees with the/Destswriter.No caller
AddNamedDest()is required — registration is automatic.Tests
layout/anchor_test.go— wrapper unit tests (name on first block, overflow not re-tagged,Measurabledelegation, empty-plan/empty-name safety).html/anchor_dest_test.go— end-to-end:html.Convert → WriteTo, then walks the parsed catalog's/Deststo prove the destination is defined, asserts/FitHand the absence of a dangling/GoTo. Verified red before the fix, green after.gofmt -s,go vet, and the fullgo test ./... -racesuite pass.Scope notes
/Destsdictionary (ISO 32000 §12.3.2.3), which viewers honor. A/Names → /Destsname tree is a separate optional enhancement.ids (e.g.<span id>mid-paragraph) flow through the inline path and are out of scope; real anchor targets are block-level.## [0.8.0]html: emit real PDF link annotations for href="#anchor" with auto-registered destinations #223 CHANGELOG line is left as released history; this change is logged under## Unreleased → Fixed.