Skip to content

Add support for cross-document references#79

Open
imphil wants to merge 1 commit into
atsphinx:mainfrom
imphil:crossref
Open

Add support for cross-document references#79
imphil wants to merge 1 commit into
atsphinx:mainfrom
imphil:crossref

Conversation

@imphil

@imphil imphil commented Jun 22, 2026

Copy link
Copy Markdown

Implement :doc:/:ref:/:confval:/etc. cross-references in the Typst
builder, following the same approach as Sphinx's LaTeX builder: all
documents are merged into a single doctree, and Sphinx's standard
env.resolve_references() runs against that merged tree right after
merging. This lets Sphinx's own domains handle path normalization,
label lookup, title substitution, and intersphinx exactly as they do
for any other builder.

Builder.get_target_uri()/get_relative_uri() map a docname onto the
single merged Typst document, mirroring the LaTeX builder's
"%docname" convention. The writer's job is then only to translate the
refid/refuri that Sphinx resolves into a namespaced Typst label
(docname:id); the namespace keeps ids from colliding across
documents now that they all share one output file. A small
per-document "#metadata(none) " anchor - the same trick HTML
uses for an extra empty anchor per additional id on an element - gives
whole-document :doc: references something to link to even when the
target document has no sections, and covers section/signature ids
beyond the first, since Typst only allows one label per element.

docutils' PropagateTargets transform forwards an explicit label (e.g.
RST's .. _label:) placed directly before a section, a paragraph,
a directive with a signature (.. confval::), or another target,
onto whatever follows it: it clears the target node's own ids and
points its refid at the merged-in id instead. visit_target mirrors
Sphinx's LaTeX writer (visit_target in sphinx/writers/latex.py) to
handle this correctly: walk past any chain of adjacent forwarding
targets to find what the id actually landed on. When that's a
section, visit_title/depart_title already emit a namespaced anchor
for every one of the section's ids (including ones forwarded here), so
visit_target skips it to avoid emitting the label twice. Anything else - a plain paragraph, or a desc node whose own id handling in
depart_desc_signature only looks at its signature's ids, not ids
forwarded onto the parent desc - has no other code path that would
emit the anchor, so visit_target writes it directly.

Includes test coverage for basic cross-references, :ref: to named
targets, relative paths (including bare relative paths resolved from
the referring document's directory), nested documents, edge cases such
as documents without sections and confval-style targets, and explicit
labels placed directly before a plain paragraph or a confval directive.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements cross-reference support for the merged single-file Typst builder. In builders.py, NoUri is imported, a self.docnames set is added to track merged documents, and assemble_doctree now records the current docname, resolves references on the merged tree, while get_target_uri and get_relative_uri replace their stubs with real validation and delegation logic. In writer.py, a _doc_label helper converts document names to Typst label namespaces, curfilestack tracking is added to the translator, and visit_document, depart_title, visit_reference, depart_desc_signature, visit_start_of_file, and _write_anchor are all implemented or updated to emit correctly namespaced Typst #link and #metadata anchors. A new test fixture tree (test-cross-references/) with conf.py, seven RST source files, and seven E2E test functions covers the full cross-reference pipeline including :doc:, :ref:, bare-relative paths, no-sections documents, implicit titles, label syntax validation, and PDF compilation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • atsphinx/typst#47: Adds desc node support and visit_reference/desc_* methods in writer.py, directly overlapping with this PR's changes to visit_reference, depart_desc_signature, and anchor handling in the same translator.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding cross-document reference support to the Typst builder.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Implement :doc:/:ref:/:confval:/etc. cross-references in the Typst
builder, following the same approach as Sphinx's LaTeX builder: all
documents are merged into a single doctree, and Sphinx's standard
env.resolve_references() runs against that merged tree right after
merging. This lets Sphinx's own domains handle path normalization,
label lookup, title substitution, and intersphinx exactly as they do
for any other builder.

Builder.get_target_uri()/get_relative_uri() map a docname onto the
single merged Typst document, mirroring the LaTeX builder's
"%docname" convention. The writer's job is then only to translate the
refid/refuri that Sphinx resolves into a namespaced Typst label
(<docname:id>); the namespace keeps ids from colliding across
documents now that they all share one output file. A small
per-document "#metadata(none) <label>" anchor - the same trick HTML
uses for an extra empty anchor per additional id on an element - gives
whole-document :doc: references something to link to even when the
target document has no sections, and covers section/signature ids
beyond the first, since Typst only allows one label per element.

docutils' PropagateTargets transform forwards an explicit label (e.g.
RST's ``.. _label:``) placed directly before a section, a paragraph,
a directive with a signature (``.. confval::``), or another target,
onto whatever follows it: it clears the target node's own ids and
points its refid at the merged-in id instead. visit_target mirrors
Sphinx's LaTeX writer (visit_target in sphinx/writers/latex.py) to
handle this correctly: walk past any chain of adjacent forwarding
targets to find what the id actually landed on. When that's a
section, visit_title/depart_title already emit a namespaced anchor
for every one of the section's ids (including ones forwarded here), so
visit_target skips it to avoid emitting the label twice. Anything else
- a plain paragraph, or a ``desc`` node whose own id handling in
depart_desc_signature only looks at its signature's ids, not ids
forwarded onto the parent ``desc`` - has no other code path that would
emit the anchor, so visit_target writes it directly.

Includes test coverage for basic cross-references, :ref: to named
targets, relative paths (including bare relative paths resolved from
the referring document's directory), nested documents, edge cases such
as documents without sections and confval-style targets, and explicit
labels placed directly before a plain paragraph or a confval directive.
@imphil

imphil commented Jul 17, 2026

Copy link
Copy Markdown
Author

Updated to fix some problems we saw with real-world documents.

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