Repository navigation
✨ NEW: Add section_ref syntax extension (§1.1 heading references) - #1170
Merged
chrisjsewell merged 20 commits intoNov 10, 2026
Merged
Conversation
Wires the (unreleased) mdit-py-plugins section_ref plugin into MyST-Parser as a new optional extension. §1 / §1.1 / §2.3.4 references resolve, via a new ResolveSectionRefs transform, to internal links targeting the correspondingly numbered heading of the current document. Numbering is document-local and structural: with a single top-level heading, its subsections are §1, §2, ...; nested levels dot-separated. Unresolvable references stay as section-ref styled text and emit a suppressible myst.section_ref warning. References inside headings or link text are deliberately left unlinked, since a reference there would nest anchors in the output and in sphinx toctree entries. Requires mdit-py-plugins >= 0.7 (unreleased). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwRkZLzpbHdb3pgmctmn6H
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwRkZLzpbHdb3pgmctmn6H
The section_ref plugin now provides meta["numbers"] as a list of ints, so the marker attribute becomes section_numbers and the transform no longer parses the dotted string itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwRkZLzpbHdb3pgmctmn6H
Resolved §refs now carry the target section's title as reftitle, rendered by Sphinx HTML as a hover tooltip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwRkZLzpbHdb3pgmctmn6H
# Conflicts: # .pre-commit-config.yaml # pyproject.toml
docutils promotes a lone top-level heading to the document title, and a lone child heading of that to the subtitle; Sphinx never does. The single-root heuristic then ran on the promoted tree, so in docutils "# T / See §1 §1.1 / ## A / ### A1" resolved §1 to A1 and left §1.1 unresolved, while Sphinx resolved §1 to A and §1.1 to A1. The numbering now recovers the heading hierarchy from the promoted title and subtitle, so §1 can target the subtitle, and a reference written in the subtitle stays inert, as it does in a Sphinx heading. A parity test runs each case through both parsers.
A section_ref token has no children, so renderInlineAsText dropped it, and "" gave alt="a b". Its content, the reference as written, is now kept.
Say that docutils title/subtitle promotion no longer changes what a reference targets, that §-numbers are not the numbers a :numbered: toctree displays, and that legal citations and references into other specifications (RFC 9110 §8.3) are read as local references, with § as the way to write a literal sign. Unresolved and inert references are "marked with the section-ref class", not styled: no CSS ships with the extension.
Run ResolveSectionRefs at priority 300, after the transforms that settle
each section's first id (PropagateTargets 260, SortIds 261,
PrioritiseExplicitIds 262) and before docutils' DocTitle (320) and
SectSubTitle (350). The doctree is then the headings as written in both
docutils and sphinx, so the code that recovered the numbering from a
promoted title or subtitle is gone, and the sectsubtitle_xform case now
also agrees with sphinx. A link to a heading that docutils later promotes
to the subtitle still lands, since the promotion copies the section ids.
Also:
- only section headings leave a reference inert; admonition, table, topic
and sidebar titles are not copied into navigation, so references there
now resolve (and a typo there now warns);
- the unresolved-reference warning is located at the node, so inside an
{include} it names the included file rather than the host file.
New tests pin both priority bounds (an explicit target's id, and the
promotion cases), the sectsubtitle_xform case, the pending_xref inert
rule, admonition titles and the warning location.
Say that numbers are positional (inserting or moving a heading silently retargets later references; use a (label)= target where that matters), that a reference in an included file resolves against the including document, and that references in admonition or table titles resolve. Reword the docutils promotion paragraph to match resolution before it.
…} count
Move the docutils/Sphinx parity cases into one fixture file,
tests/test_renderers/fixtures/section_ref_resolution.md, which states for
each document which heading every reference resolves to (or that it is
unresolved and warns, or inert and does not), so a second implementation
can port it as data. It covers the documented numbering rules: single and
several top-level headings, skipped levels, includes, eval-rst sections,
rubrics, front matter and title_to_header, numbers as written (leading
zeros, §0, the nine-digit limit), inert contexts, explicit targets, and
headings in an {only} block.
Sections inside an `only` node now count whatever its condition, so the
numbering does not depend on the builder (an {eval-rst} `.. only::` keeps
its sections inside the node).
The unresolved warning now says "in this document", a resolved reference
keeps its section_numbers attribute, and the priority comment no longer
names consumers that never see a resolved reference.
Add a "Numbering rules" subsection stating which text is a reference,
which headings count, how they are numbered and how references resolve,
including includes, eval-rst sections, {only} blocks, title_to_header,
the nine-digit limit, and that a reference resolves against the document
it is written in.
Point to {numref} for citing the number a :numbered: toctree displays,
and move the suppress_warnings hint next to the legal-citation caution.
A section now counts wherever the document tree places it, under its
nearest section ancestor: the numbering descends through any container
element (an `only` node, an `ifconfig` node, ...), not just `only`.
Conformance fixture:
- the {only} cases prove the counting through the headings after the
branch, rather than by linking into a branch the build excludes;
- new Sphinx-only cases: `.. only::` sections at the document root,
nested `only`, `.. ifconfig::` in {eval-rst}, and link text of a link
to another document (replacing its standalone test);
- new case: {include} with :heading-offset:;
- a reference in a rubric heading (inside {note}) resolves;
- targets that share a title carry an occurrence suffix (`A [2]`).
Say that included headings are shifted by :heading-offset:, that sections
placed by other directives count, that docutils has no {only}, that a
reference into an excluded {only} branch points to a missing anchor, and
that a rubric heading is not a section heading. {numref} on a section does
not need numfig.
Add a fixture case, run in both parsers, where {eval-rst} places rST
sections inside a {note} and a list item, so the general container rule
is pinned by data a second engine can run. Put a rubric that shares a
section's title into the duplicate-titles case, so the occurrence suffix
counts sections only. Only a subtitle of a section (or of the document)
counts as a section title in the harness; a sidebar's subtitle does not.
Name ifconfig beside {only} where docutils drops the block and where an
excluded branch leaves a missing anchor, say that a sidebar's :subtitle:
is not a section heading, and that the rubric rule is about Markdown
headings.
chrisjsewell
marked this pull request as ready for review
November 10, 2026 18:00
chrisjsewell
added a commit
that referenced
this pull request
Nov 10, 2026
chrisjsewell
added a commit
that referenced
this pull request
Nov 10, 2026
…ugins 0.7 requirement (#1170)
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.
Adds an optional syntax extension,
section_ref, built on the mdit-py-pluginssection_refplugin (executablebooks/mdit-py-plugins#144).It turns section-sign references such as
§1,§1.1and§2.3.4into internal links to the correspondingly numbered heading of the current document:This is blocked on mdit-py-plugins 0.7.0 reaching PyPI:
pyproject.tomland the mypy pre-commit dependencies pinmdit-py-plugins~=0.7, so CI cannot install until then.The test suite, pre-commit and a
-nWdocs build with the extension enabled all pass locally against mdit-py-plugins from the branch that adds the nine-digit limit per level(the conformance fixture's digit cases need it, so 0.7.0 must include it).
How it works:
section_reftoken whosemeta["numbers"]holds the parsed number.The renderer turns it into an inline marker carrying those numbers.
ResolveSectionRefstransform numbers the document's headings as written:under a single top-level heading its subsections are
§1,§2, ...; otherwise the top-level headings are; deeper levels add dots.It runs at priority 300, after the transforms that settle each section's first id (including
PrioritiseExplicitIds, so(label)=ids win),and before docutils promotes a lone top-level heading to the document title (and a lone subsection to a subtitle), which Sphinx never does.
So every reference resolves to the same heading in both, also with docutils'
--section-subtitles.inside an
onlynode whatever its condition (so the numbers are the same for every builder), anifconfignode, or any other container.section-ref, the heading's title asreftitle(a hover tooltip in Sphinx HTML),and the parsed number kept as
section_numbers.An unresolved one stays as text marked with the
section-refclass, with a suppressiblemyst.section_refwarning("Section reference target not found in this document: '§9.9'", giving the number as written).
References in other titles (admonitions, tables) and in rubrics (a heading inside a directive body) resolve as usual.
{include}d file is reported at that file's line.givesalt="a §1 b").The docs (
docs/syntax/optional.md, "Section references") say that §-numbers are not the numbers a:numbered:toctree displays(and point to
{numref}with a label for citing those),that legal citations (
§242 BGB) and references into other specifications (RFC 9110 §8.3) are read as local references(with
suppress_warnings = ["myst.section_ref"]next to that caution),that
§writes a literal sign,that numbers are positional (inserting a heading retargets later references without a warning, so use a
(label)=target where that matters),and that a reference in an included file resolves against the including document.
A "Numbering rules" subsection states the behaviour exactly: what text is a reference (including the nine-digit limit per level),
which headings count (included headings at their written level shifted by
:heading-offset:,{eval-rst}sections,{only}headings whatever the condition, sections placed by other directives, amyst_title_to_headertitle),how they are numbered, and how references resolve.
The rules are pinned by one conformance fixture,
tests/test_renderers/fixtures/section_ref_resolution.md:each case is a document plus the heading every reference resolves to (or that it is unresolved and warns, or inert and does not),
and one test runs every case through both docutils and Sphinx
(the cases that need
{only},{ifconfig}or a link to another document run in Sphinx only, since docutils has no such directive and cannot resolve such a link).A reference into an
{only}branch that the build excludes points to an anchor that is not in the output; the fixture deliberately does not pin it.The fixture is plain data, so another implementation of the syntax can port it.
Resolution is document-local in this version. A follow-up could resolve against Sphinx's section numbers for
:numbered:toctrees, or across documents.