You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 4ec4cf8
Browse filesBrowse the repository at this point in the historyBrowse files
✨ NEW: docutils heading-anchor slug preset, and a versioned anchor contract (#1168)
Finishes the job #1158's slug module started: the anchor behaviour
becomes a formal, versioned contract that alternative implementations
and tooling can pin against, plus a third slug preset for mixed-format
projects.
## A `docutils` slug preset
`myst_heading_slug_func = "docutils"` produces slugs **byte-identical to
`docutils.nodes.make_id`** — the id style reStructuredText headings
receive — so a Sphinx project mixing `.rst` and `.md` sources can choose
one uniform anchor rule.
- The algorithm and its translate tables are ported into the pure,
stdlib-only `myst_parser.slugs` (no docutils import there; provenance
noted — docutils is public domain), keeping the module's
replicate-from-one-file property.
- Fidelity is pinned **by CI, not by care**: the corpus plus adversarial
vectors (digraphs, the full 33-entry translate table, combining marks,
compatibility/ligature forms, mathematical alphanumerics, Turkish
dotless-i, whitespace runs) are asserted byte-equal against the real
`make_id`. Review additionally fuzzed **210k random strings and swept
every codepoint U+0020–U+2FFF** with zero divergences. Drift policy
(documented): if a future docutils changes `make_id`, the preset and
corpus stay byte-stable — they are the contract — and the divergence
gets documented.
- The `myst-anchors` CLI picks the preset up automatically (`--slug-func
docutils`), with renderer/CLI cross-check tests.
## Empty slugs mean no anchor
`make_id` returns `""` for *any* title without Latin letters (`Привет`,
`2.0`, `!!!`) — far more often than the github preset. Previously
(unreleased, since #1158) an empty slug was stored, and a *second*
empty-slugging heading received a literal `-1` HTML anchor. Now an empty
slug means the heading simply gets **no anchor** — not stored, no dedup
participation, no id emitted — for all presets and custom functions. Two
behavioural nuances from review, both strictly-better and test-covered:
- `<span id="-1">` anchors no longer appear for duplicate
punctuation-only headings;
- `[](#)` (an empty fragment) no longer silently resolves to a
punctuation-only heading — it now warns as an unresolved reference.
## The versioned contract
- `tests/fixtures/slugs.json` now carries `version: 1` and a policy
statement: append-only within a version; version bump on any semantic
change to an existing vector or algorithm. New `docutils` vectors were
generated against the real `make_id` at authoring time.
- A new normative docs subsection, *The anchor contract*
(`docs/syntax/optional.md`), states in one place: slug generation
(presets + corpus as the machine-readable half), fixed-base
deduplication (`x, x-1, x-2`; docutils' own `idN` dedup deliberately not
emulated), the empty-slug rule, explicit-id priority, HTML id emission
(`id_prefix` bypassed), and the drift policy. The `myst-anchors` CLI
preview's one divergence (it cannot apply the renderer-only empty-slug
rule) is documented alongside the CLI example.
## Review
Two implementation agents + a combined adversarial/behaviour review with
old-vs-new probes against master: github/gitlab documents render
**byte-identically** (docutils publish, sphinx HTML, searchindex,
warnings) except the intended empty-slug fix; docutils-preset end-to-end
resolution (same-doc, cross-doc, dedup-slug, explicit-target priority,
slug==primary-id coincidence) verified with zero warnings. One
pre-existing bug surfaced by review (topmatter clobbers validated config
values, breaking *all* preset strings in frontmatter since #1158) is
tracked separately in #1167 rather than scope-creeping here.
Full suite: 1245 passed. No changes to the `github`/`gitlab` slug
outputs; no new configuration surface beyond the preset name.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01GByoptRecR2GAwj5qrLz6C)_
Copy file name to clipboardExpand all lines: docs/syntax/optional.md
+39-2Lines changed: 39 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -539,13 +539,15 @@ The slug generation function is selected with the `myst_heading_slug_func` optio
539
539
The recommended form is a named preset:
540
540
541
541
```python
542
-
myst_heading_slug_func ="github"# the default; also available: "gitlab"
542
+
myst_heading_slug_func ="github"# the default; also available: "gitlab", "docutils"
543
543
```
544
544
545
545
The `"gitlab"` preset follows [GitLab's algorithm](https://docs.gitlab.com/ee/user/markdown.html#heading-ids-and-links) (which, unlike GitHub, strips surrounding whitespace, squeezes repeated hyphens and prefixes digit-only slugs with `anchor-`).
546
546
547
+
The `"docutils"` preset produces slugs byte-compatible with `docutils.nodes.make_id` — the `id` style reStructuredText headings receive — for a uniform anchor style across mixed rST + Markdown projects; its slugs are ASCII-only, so a non-Latin title produces no anchor (see the anchor contract below).
548
+
547
549
:::{versionchanged} 5.2.0
548
-
`myst_heading_slug_func` now accepts the preset names `"github"` (default)and `"gitlab"`.
550
+
`myst_heading_slug_func` now accepts the preset names `"github"` (default), `"gitlab"`and `"docutils"`.
549
551
550
552
As a legacy form, it can still be set to an import path string
551
553
(e.g. `myst_heading_slug_func = "mypackage.mymodule.slugify"`, added in 0.19.0)
Copy file name to clipboardExpand all lines: tests/fixtures/slugs.json
+72Lines changed: 72 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -1,4 +1,6 @@
1
1
{
2
+
"version": 1,
3
+
"description": "Normative conformance corpus for MyST heading-anchor slugs. 'slugify' rows pin SLUG_PRESETS[preset](input) == expected, byte-for-byte; 'unique' rows pin unique_slug(slug, existing) == expected. The set is append-only within a version; any semantic change to an existing vector or algorithm requires a version bump. An empty expected slug means the heading receives no anchor.",
0 commit comments