Skip to content

✨ NEW: Add section_ref syntax extension (§1.1 heading references) - #1170

Merged
chrisjsewell merged 20 commits into
masterfrom
claude/mdit-section-reference-plugin-yfvmtz
Nov 10, 2026
Merged

chrisjsewell merged 20 commits into
masterfrom
claude/mdit-section-reference-plugin-yfvmtz

Conversation

@chrisjsewell

@chrisjsewell chrisjsewell commented Jul 16, 2026 •

Copy link
Copy Markdown
Member

Adds an optional syntax extension, section_ref, built on the mdit-py-plugins section_ref plugin (executablebooks/mdit-py-plugins#144).
It turns section-sign references such as §1, §1.1 and §2.3.4 into internal links to the correspondingly numbered heading of the current document:

# Title

See §1 and §1.1 for details.

## First Section

### Nested

This is blocked on mdit-py-plugins 0.7.0 reaching PyPI: pyproject.toml and the mypy pre-commit dependencies pin mdit-py-plugins~=0.7, so CI cannot install until then.
The test suite, pre-commit and a -nW docs 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:

  • The plugin emits a section_ref token whose meta["numbers"] holds the parsed number.
    The renderer turns it into an inline marker carrying those numbers.
  • A ResolveSectionRefs transform 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.
  • A section counts wherever the document tree places it, under its nearest section ancestor:
    inside an only node whatever its condition (so the numbers are the same for every builder), an ifconfig node, or any other container.
  • A resolved marker becomes an internal reference to the section's primary id, with class section-ref, the heading's title as reftitle (a hover tooltip in Sphinx HTML),
    and the parsed number kept as section_numbers.
    An unresolved one stays as text marked with the section-ref class, with a suppressible myst.section_ref warning
    ("Section reference target not found in this document: '§9.9'", giving the number as written).
  • A reference inside a section heading or link text is left unlinked, since a link there would nest inside another link (headings are copied into toctree and contents entries).
    References in other titles (admonitions, tables) and in rubrics (a heading inside a directive body) resolve as usual.
  • An unresolved reference in an {include}d file is reported at that file's line.
  • Image alt text keeps the reference as written (![a §1 b](x.png) gives alt="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, a myst_title_to_header title),
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.

claude added 2 commits July 16, 2026 09:47
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
claude and others added 16 commits July 16, 2026 14:09
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 "![a §1 b](x.png)" 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
&sect; 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 chrisjsewell mentioned this pull request Nov 10, 2026
14 tasks
@chrisjsewell
chrisjsewell marked this pull request as ready for review November 10, 2026 18:00
@chrisjsewell
chrisjsewell merged commit 7d48de0 into master Nov 10, 2026
24 checks passed
@chrisjsewell
chrisjsewell deleted the claude/mdit-section-reference-plugin-yfvmtz branch November 10, 2026 18:00
chrisjsewell added a commit that referenced this pull request Nov 10, 2026
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.

2 participants