Add slug='github' for GitHub-compatible heading ids - #43
Draft
PiotrCzapla wants to merge 1 commit into
Draft
Conversation
`to_html` derives heading ids by Pandoc's rules, which differ from GitHub's in ways that break links written against a GitHub-rendered page. Measured against GitHub's own table-of-contents fragments for 296 public wiki pages, the default rules disagree on 539 of 2210 anchors. Add an opt-in `slug='github'` deriving ids by GitHub's rules instead, for documents that already live on GitHub, where an anchor is a published address. The default is unchanged. Three corners drive the divergence, each confirmed against GitHub's rendering rather than inferred: - Only U+0020 becomes `-`. Other whitespace is dropped, so a heading split across source lines slugs as one word. - Text is neither trimmed nor collapsed, so a heading opening with an image keeps the space it left, and a doubled space gives `--`. - There is no `section` fallback, so an all-digit heading such as a date keeps its digits instead of collapsing to `section`, `section-1`, and so on, which renumber whenever a heading is inserted above them. The character rule uses Unicode general categories (letter, number, mark) rather than a copy of `github-slugger`'s generated regex, which reproduces all 3648 corpus anchors exactly. U+200D is kept because that regex omits it, which keeps emoji sequences intact. End to end, 2285 of 2290 anchors now match GitHub. The 5 remaining are one page whose `## Dustin [@DHowett]` headings hit the separate collision between `[@name]` shortcut reference links and cross-reference syntax; the slug rule is exact given the right heading text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
What this adds
An opt-in
slug='github'onto_html, and a matching--slug githubonmd2html, deriving heading ids by GitHub's rules instead of the dialect's Pandoc-style ones. The default is unchanged.Why
An anchor on a document that lives on GitHub is a published address. A wiki page's own table of contents, a
READMEsection link, and any link anyone wrote against the rendered page all point at GitHub's slug. Rendering that same Markdown throughmdhtmlgives a different id, and every one of those links breaks.I measured the gap rather than assuming it. I fetched GitHub's own
_tocfragments for 296 public wiki pages across 18 repositories (gollum, opencv, matplotlib, numpy, scipy, sympy, qBittorrent, rclone, microsoft/terminal and others) and compared them againstto_htmloutput page by page. On the 2210 headings where both sides agreed on the heading count, the default rules disagreed on 539 anchors.The causes, by volume:
section,section-1,section-2Reverse Proxy is not working.ends in-working.--page-file-dirloses its dashesThe first row is the worst of them, and it is not merely a wrong link. OpenCV's meeting-note pages are date headings top to bottom, so one page gets 79 ids named
section-1throughsection-79. Inserting a heading anywhere renumbers every id below it.Design choices
A separate
slug=parameter, not anauto_idsvariant.auto_idsanswers whether ids get minted at all, and callers rendering several fragments into one page already passauto_ids=False. The rule for minting is a different question, and the codebase already spells mode choices as string parameters (refs,math,number_headings).slug='github'composes withauto_ids=Falsethe obvious way, which is to mint nothing.Unicode general categories, not a port of
github-slugger's regex. That library removes punctuation via a generated character class thousands of characters long. Copying it into Rust would be unreviewable and would rot. Keeping letters, numbers and marks, plus-,_and space, reproduces all 3648 corpus anchors exactly, so the categories are what the regex is actually expressing.I recommend reviewing this by running the check rather than reading the class. A worked hazard: the real regex begins at U+2000, which renders identically to an ASCII space. My first attempt at transcribing it produced the range "space through U+206F", which deletes every letter and silently emitted empty anchors for the whole corpus. The category rule has no such failure mode.
U+200Dis kept.github-slugger's class happens to omit the zero width joiner while removing its neighbours, which keeps emoji sequences intact. This is a quirk rather than a principle, so it is a named constant with a comment. It is the difference between 99.973% and 100% on the corpus.Reading the heading text verbatim, not through
norm_text. GitHub's rule depends on whitespace the dialect's own rule discards, so this mode callsto_textdirectly. Three behaviours follow, and all three are load-bearing:-. A newline is dropped outright, so# Minutes\n*isminutes, notminutes-.#  Wikikeeps the space the image left and yields-wiki.--.These look like bugs and will attract a tidy-up. The comment at
norm_text's call site says why they are not.No
sectionfallback. A heading with nothing left takes the empty id and repeats dedupe to-1,-2. That is GitHub's behaviour, and the fallback is precisely what causes the 368-anchor failure mode above.Validation
End to end, 2285 of 2290 anchors match GitHub across 196 pages.
The 5 misses are all on one page,
microsoft/terminalCore-team-North-Stars, whose headings read## Dustin [@DHowett]with a matching link definition below.[@DHowett]is a CommonMark shortcut reference link, but the parser reads it as a cross-reference, so the heading text becomesDustinand slugs todustin-. Given the text GitHub actually slugs, this mode returnsdustin-dhowett. The slug rule is exact; that page hits a separate defect, noted below.Testing
cargo fmt,cargo check,cargo testandgen_docs(check=True)are clean.pytestis 938 passed with 3 failures that reproduce identically on a cleanmain: two intest_fill.pyfrom an unawaited coroutine, and one from the optionalmath-corepackage being absent.Two tests added to
tests/test_export.py, one per divergence and one for dedup and option handling.Not in this PR
Four issues the same survey turned up, listed so they are not lost:
---or===. The dialect drops them by design, so they render as a paragraph plus<hr>and vanish from the table of contents. This costs 138 headings across 56 pages, and whole pages disappear:matplotlib/MEP9has 9 headings on GitHub and 0 here. This is the largest single divergence and it is a deliberate design decision, so it needs your call rather than a patch.[@name]collides with shortcut reference links, and in the defaultrefs='resolve'modeto_htmlraisesValueErrorand produces nothing for the whole page.<details>and<summary>are outside the raw HTML subset, so they render as visible escaped text. No public wiki in the sample used them, but our ownanswerdotaiwiki uses them 36 times on one page.