Skip to content

Fix introspection-driven features (page numbering, refs, outline)#4

Open
ntodd wants to merge 2 commits into
dannote:masterfrom
ntodd:master
Open

Fix introspection-driven features (page numbering, refs, outline)#4
ntodd wants to merge 2 commits into
dannote:masterfrom
ntodd:master

Conversation

@ntodd
Copy link
Copy Markdown

@ntodd ntodd commented May 17, 2026

This PR was AI-assisted, but I did review the code to the best of my Rust language abilities.

The Engine was wired with EmptyIntrospector and laid out once, so anything that depends on Typst's second-pass resolution silently broke: page numbering rendered the literal pattern on every page, refs resolved to nothing, and the outline was empty.

Two fixes:

  1. world.rs — build the body/styles once, then run the same MAX_ITERS=5 convergence loop the Typst CLI uses (compile_impl in vendor/typst/crates/typst/src/lib.rs). Each iteration feeds the previous PagedDocument's introspector into a fresh engine; the loop exits when comemo::Constraint validates against the new document's introspector.

  2. convert.rslabel/1 was producing Content::empty().labelled(lbl), an empty placeholder carrying the label, so [heading(1, "Foo"), label("foo")] never associated the label with the heading. Mirror Typst's markup eval (typst-eval/src/markup.rs): when a Label node is built, walk backwards through the sequence to the last non-Unlabellable element and attach the label there. Also suppress the auto-parbreak that would otherwise slip between a block and its trailing label.

Testing

New regression test (test/folio_test.exs) compiles two pages with identical bodies separated by a pagebreak with page_numbering enabled, and asserts the SVG strings differ. This isn't perfect, but like other tests, does not require PDF introspection tools.

ntodd added 2 commits May 17, 2026 12:27
The Engine was wired with EmptyIntrospector and laid out exactly once, so
Typst's two-pass introspection — which resolves page counters, refs, the
outline, and any raw_typst calling counter/here/query — could never see
the document's actual positions. Page numbering rendered the literal
pattern on every page, refs found nothing, and the outline was empty.

Build the body and styles once, then run the same MAX_ITERS=5 convergence
loop the Typst CLI uses (compile_impl in vendor/typst/crates/typst/src/lib.rs):
each iteration feeds the previous PagedDocument's introspector into a fresh
engine, stopping when comemo::Constraint validates against the new document's
introspector.
label/1 produced Content::empty().labelled(lbl), an empty placeholder
carrying the label. Refs then couldn't find any numbered element, so
[heading(1, "Foo"), label("foo")] followed by ref("foo") resolved to
nothing.

Mirror Typst's markup eval (vendor/typst/crates/typst-eval/src/markup.rs):
when a Label node is built, walk backwards through the current sequence to
the last non-Unlabellable element and attach the label there. Applied in
both build_content and cc so labels inside nested bodies behave the same.
Also suppress the auto-parbreak that would otherwise sit between a block
and its trailing label and sever the attachment.
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