Emit slug-based heading IDs for anchor link navigation#430
Conversation
Headings now receive text-derived id attributes so standard CommonMark/GFM anchor links like [text](#section) navigate to the corresponding heading in the preview. The visible [TOC] renderer uses the same slugs for its links, keeping them consistent with the new heading ids. Existing golden fixtures and a few inline assertions that assumed <h*> without attributes are updated. Four new tests cover slug generation (basic ASCII, UTF-8 preservation, punctuation handling, and emission independent of the TOC preference). Related to schuyler#429
MPQuickLookRenderer uses its own parallel Hoedown setup, so without this the anchor link fix would only apply to the main preview pane and not to Finder Quick Look previews of the same file. The slugify helper and header renderer are duplicated rather than shared because MacDownCore is a separate Xcode target without access to MacDown's hoedown_html_patch.c. Sharing them would require restructuring the project, which is out of scope here. Related to schuyler#429
schuyler
left a comment
There was a problem hiding this comment.
Thanks for the PR, @falcon-enoc!
Issues
-
HTML entities in slugs: because slugs are derived from the rendered HTML content, a heading like
Q&Abecomesid="qampa"(the literalampleaks in), so the#qaanchor a reader would naturally write silently fails to navigate — the same happens for<,>,",'(e.g.Tips & Tricks→tips-amp-tricks). Since rendered content always escapes a literal&to&, skipping&…;entities the same way the loop already skips<…>tags would resolve it, for example near the top of theslugifyloop:if (c == '&') { // skip an HTML entity like & / < / ' while (i + 1 < content->size && content->data[i + 1] != ';') i++; i++; // consume the ';' continue; }
(The same change would apply to
mp_quicklook_slugify.)
Suggestions
- Duplicate-heading test: a test pinning current behavior for identical headings (e.g.
## Cand## C++both yieldingid="c") would document the slug contract. - TOC round-trip test: since this PR changes both sides, a test asserting a heading's
idequals the matching[TOC]hrefwould lock in the navigation guarantee. - Non-ASCII anchors: UTF-8 bytes are emitted into
id/hrefwithout percent-encoding, which works in modern browsers but isn't canonical.
Generated by Claude Code
Rendered heading content escapes & < > to entities, so the slug loop received "&" and leaked "amp" into ids (e.g. "Q&A" -> id="qampa"), silently breaking anchor navigation. Skip &...; sequences the same way the loop already skips <...> tags, in both slugify and mp_quicklook_slugify. Related to schuyler#430
Adds cases for &/</multiple entities in both the preview and Quick Look renderers, pins the duplicate-heading collapse contract, and locks in TOC href == heading id navigation. Related to schuyler#430
|
Thanks for the thorough review, @schuyler! All points addressed. HTML entities in slugs (Issue) — Fixed. The slug loop now skips Duplicate-heading test (Suggestion 1) — Added TOC round-trip test (Suggestion 2) — Added Non-ASCII anchors (Suggestion 3) — Left UTF-8 as-is on purpose: browsers match the URL fragment against the Also added 3 analogous tests for the Quick Look renderer to keep both code paths in parity. All tests green: 86 passed, 0 failures. |
There was a problem hiding this comment.
Thanks for the PR, @falcon-enoc — and for the thorough write-up in #429!
This is a clean, well-targeted fix: emitting text-derived IDs unconditionally is the right call, the TOC href and heading ID staying in sync is a nice touch, and the new tests around HTML-entity handling cover a genuinely tricky path. Approving.
A handful of minor, non-blocking polish ideas came up during review (a few extra edge-case tests, a now-unused TOC counter, and a comment tweak) — none need to hold this up, so we've logged them on our side in #471 rather than expand the scope here. Much appreciated!
|
Thanks again for this, @falcon-enoc! The one red check here isn't a problem with your code — it's a branch-staleness collision, and the fix is mechanical. What's happening: the only failing test is The fix: merge (or rebase onto) the latest - <h2>Wide Table (many columns, issue #432 regression)</h2>
+ <h2 id="wide-table-many-columns-issue-432-regression">Wide Table (many columns, issue #432 regression)</h2>Regenerating via the suite's (The Generated by Claude Code |
Regenerate tables.html golden so the "Wide Table" heading (added in schuyler#440) carries the slug id emitted by the anchor-id logic, keeping the exact-match rendering test green against current main.
|
Thanks, @schuyler — your diagnosis was spot on. I rebased onto the latest - <h2>Wide Table (many columns, issue #432 regression)</h2>
+ <h2 id="wide-table-many-columns-issue-432-regression">Wide Table (many columns, issue #432 regression)</h2>No other fixtures picked up new headings from Pushed in |
|
@falcon-enoc Thanks for your contribution and especially thanks for your patience with all the back and forth. Branch merged! |
PR #430 copied the slug/heading-id logic into the Quick Look renderer (mp_quicklook_render_header), including the same hoedown_buffer_new( content->size) call. An empty heading (e.g. a bare setext underline) therefore creates a zero-unit buffer that aborts the Quick Look render process the first time it is written to — the same defect just fixed in hoedown_html_patch.c, reachable by previewing a .md file in Finder. Clamp the size hint with a mp_quicklook_new_growable_buffer() helper mirroring new_growable_buffer(), and add a regression test rendering a bare '-' heading through the Quick Look renderer. Related to #479
Summary
idattributes regardless of the "Detect TOC token" preference, enabling standard[text](#section)anchor links in the preview.[TOC]renderer uses the same slugs.hoedown_html_patch.c) and the Quick Look renderer (MPQuickLookRenderer.m) for parity.Tests
xcodebuild test -workspace "MacDown 3000.xcworkspace" -scheme MacDown -destination 'platform=macOS'— 875 / 875 passing locally.MPMarkdownRenderingTests.mfor slug behavior.id="..."additions only).Related to #429