diff --git a/src/atsphinx/typst/builders.py b/src/atsphinx/typst/builders.py index 53ff2ed..362cf92 100644 --- a/src/atsphinx/typst/builders.py +++ b/src/atsphinx/typst/builders.py @@ -12,7 +12,7 @@ from sphinx import addnodes from sphinx._cli.util.colour import darkgreen from sphinx.builders import Builder -from sphinx.errors import SphinxError +from sphinx.errors import NoUri, SphinxError from sphinx.util.fileutil import copy_asset, copy_asset_file from sphinx.util.nodes import inline_all_toctrees @@ -40,6 +40,10 @@ def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: # noqa: D107 self._static_dir = Path(self.outdir / "_static") self._images_dir = Path(self.outdir / "_images") self._build_date = date.today() + # Docnames merged into the document currently being written, like + # the LaTeX builder's ``self.docnames``. Populated by + # ``assemble_doctree()`` before it resolves references. + self.docnames: set[str] = set() def init(self): # noqa: D102 super().init() @@ -111,13 +115,29 @@ def assemble_doctree( root_section += toctree root = root.copy() root += root_section - tree = inline_all_toctrees(self, {docname}, docname, root, darkgreen, [docname]) + self.docnames = {docname} + tree = inline_all_toctrees( + self, self.docnames, docname, root, darkgreen, [docname] + ) + tree["docname"] = docname + # Like the LaTeX builder, resolve references right after merging so + # that Sphinx's own domains (:doc:, :ref:, :confval:, intersphinx, + # ...) do the resolution; see get_target_uri()/get_relative_uri() + # for how that maps onto the single merged Typst document. self.env.resolve_references(tree, docname, self) return tree def get_target_uri(self, docname, typ=None): # noqa: D102 - # TODO: Implement it! - return "" + if docname not in self.docnames: + raise NoUri(docname, typ) + # All documents are merged into one, so the docname itself is + # enough; the writer turns it into the appropriate Typst label. + return docname + + def get_relative_uri(self, from_, to, typ=None): # noqa: D102 + # Ignore source path: there's only one output file, so there is no + # relative path to compute (cf. the LaTeX builder). + return self.get_target_uri(to, typ) def copy_assets(self): # noqa: D102 # Copying all theme assets. diff --git a/src/atsphinx/typst/writer.py b/src/atsphinx/typst/writer.py index 624093a..d41c346 100644 --- a/src/atsphinx/typst/writer.py +++ b/src/atsphinx/typst/writer.py @@ -38,6 +38,13 @@ def _typst_local_package_fullname(name: str, version: str | None = None) -> str: return f"@local/{name}:{version}" +def _doc_label(docname: str) -> str: + # All documents are merged into a single Typst file, so every label + # needs a per-document namespace to avoid collisions between e.g. two + # documents that each have a "Installation" section. + return docname.replace("/", ":") + + class TypstTranslator(SphinxTranslator, BaseTypstTranslator): """Custom translator that has converter from dotctree to Typst syntax.""" @@ -65,15 +72,49 @@ def __init__(self, document: nodes.document, builder: Builder) -> None: self.context = { "has_index": False, } + # Track current document for cross-references (like the LaTeX + # writer's ``curfilestack``). The builder merges everything into + # one doctree, so this is what lets us namespace labels per + # document and tell which document a relative reference is in. + self.curfilestack = [document["docname"]] # ------ # visit/departuer methods # ------ + def visit_document(self, node: nodes.document): + super().visit_document(node) + self._write_anchor(_doc_label(self.curfilestack[-1])) + def visit_title(self, node: nodes.title): if isinstance(node.parent, nodes.section) and self._section_level < 1: raise nodes.SkipNode + super().visit_title(node) + def depart_title(self, node: nodes.title): + """Add a label to section titles for cross-referencing.""" + docname = _doc_label(self.curfilestack[-1]) + if isinstance(node.parent, nodes.section): + ids = node.parent.get("ids", []) + else: + ids = [] + + if ids: + # Add the section label AFTER the title text but BEFORE + # newlines. Typst syntax: == Title