Skip to content

Heading ids: lowercase (GitHub-style) default + opt-in ASCII fold - #68

Merged
dereuromark merged 2 commits into
mainfrom
feat/heading-ids-lowercase
Jun 7, 2026
Merged

Heading ids: lowercase (GitHub-style) default + opt-in ASCII fold#68
dereuromark merged 2 commits into
mainfrom
feat/heading-ids-lowercase

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

Implements the carve spec #73 lowercase heading-id decision in carve-php.

What changes

HeadingIdTracker::normalizeId now:

ASCII transliteration was the baked default; it is now opt-in via a new
AsciiHeadingIdsExtension. The extension installs a slug transformer on both the
render-time tracker and the parse-time tracker, so implicit [Heading][] references
also resolve to the folded ids.

Bonus fix

renderHeadingRef rendered an unresolved </#id> as a dangling self-link
(<a href="#id">id</a>). It now renders the literal source text, matching the spec
and carve-js. This was masked before: ASCII-by-default ids meant ASCII refs usually
resolved; once ids keep non-ASCII, the divergence showed in corpus 19.

Scope

  • src/Renderer/HeadingIdTracker.php, src/Parser/BlockParser.php, src/Renderer/HtmlRenderer.php
  • new src/Extension/AsciiHeadingIdsExtension.php (+ test)
  • spec submodule bumped to the Tabs stay tabs by default; opt-in TabNormalizeExtension #73 corpus; 81-list-lazy-continuation promoted to IMPLEMENTED (already byte-identical)
  • stale unit-test expectations updated to the lowercase / non-ASCII rule

Gates green locally: phpunit (2194), phpstan (0), phpcs (0). Part of carve#71.

Aligns the heading-id slug with carve spec #73. normalizeId now applies
the jgm/djot#393 run rule (each maximal run of non-alphanumeric ASCII ->
single '-', trimmed) and then lowercases (Unicode-aware), keeping non-ASCII
characters verbatim. Empty-text and digit-leading slugs use the s / s-
fallback. This makes ids and </#id> / [Heading][] cross-references
case-insensitive with no special lookup logic.

ASCII transliteration is no longer the default. It moves behind a new
AsciiHeadingIdsExtension, which sets a slug transformer on both the
render-time tracker and the parse-time tracker (so implicit [Heading][]
references resolve to the folded ids).

Also fixes renderHeadingRef: an unresolved </#id> now renders as its
literal source text instead of a dangling self-link, matching the spec and
carve-js. This surfaced once ASCII refs stopped matching non-ASCII ids.

Bumps the spec submodule to the #73 lowercase corpus and promotes the new
81-list-lazy-continuation category (already produced byte-identically).
…rcase

# Conflicts:
#	tests/CarveCorpusTest.php
@dereuromark
dereuromark merged commit e4804d7 into main Jun 7, 2026
4 checks passed
@dereuromark
dereuromark deleted the feat/heading-ids-lowercase branch June 7, 2026 12:29
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.

1 participant