diff --git a/docs/reference/enhancements.md b/docs/reference/enhancements.md index 8825350..984640d 100644 --- a/docs/reference/enhancements.md +++ b/docs/reference/enhancements.md @@ -144,7 +144,7 @@ The ID is `Introduction`, not `Introduction1` or `Introduction[^1]`. ## CSS-Safe Heading IDs -**Related:** [php-collective/djot-php#92](https://github.com/php-collective/djot-php/pull/92) +**Related:** [php-collective/djot-php#92](https://github.com/php-collective/djot-php/pull/92), [jgm/djot#391](https://github.com/jgm/djot/issues/391) **Status:** Implemented in djot-php @@ -217,6 +217,22 @@ You can always override with an explicit ID attribute: Explicit IDs are used as-is without normalization. +### Spec Alignment + +The djot spec's wording on auto-ID generation is being clarified in [jgm/djot#391](https://github.com/jgm/djot/issues/391). djot-php's normalization aligns with the proposed direction in most respects and deliberately deviates in two places — both motivated by producing valid CSS identifiers for `querySelector()` consumers. + +| Aspect | djot.js / djoths (proposed spec) | djot-php | +|--------|---------------------------------|----------| +| Mid-word punctuation (`A+B=C`) | replace with `-` → `A-B-C` | replace with `-` → `A-B-C` | +| Non-ASCII letters (`Über uns`) | preserve → `Über-uns` | preserve → `Über-uns` | +| Consecutive punctuation (`foo...bar`) | collapse to single `-` → `foo-bar` | collapse to single `-` → `foo-bar` | +| Apostrophe (`That's all`) | preserve → `That's-all` | replace with `-` → `That-s-all` | +| Double quote / `;` / `:` | preserve | replace with `-` | +| Leading digit (`2024 recap`) | unspecified | prefix with `h-` → `h-2024-recap` | +| Empty result (`!!!`) | unspecified | fallback → `heading` | + +The apostrophe / quote / semicolon / colon deviation is deliberate: these characters are not valid in unescaped CSS identifiers, so preserving them per the spec would force every JS consumer to round-trip through `CSS.escape()` before doing a selector lookup. The leading-digit and empty-result behaviors fill in spec gaps that other implementations handle inconsistently. + --- ## Symbol Parsing in Time Formats diff --git a/tests/TestCase/Extension/HeadingReferenceExtensionTest.php b/tests/TestCase/Extension/HeadingReferenceExtensionTest.php index ef5ec00..15cfcc6 100644 --- a/tests/TestCase/Extension/HeadingReferenceExtensionTest.php +++ b/tests/TestCase/Extension/HeadingReferenceExtensionTest.php @@ -251,7 +251,7 @@ public function testHeadingWithNoTextIsIgnored(): void public function testUserAuthoredLinkWithMatchingPlaceholderIsNotRewritten(): void { - $extension = new class('heading-ref') extends HeadingReferenceExtension { + $extension = new class ('heading-ref') extends HeadingReferenceExtension { protected function generatePlaceholderPrefix(): string { return 'collision-placeholder-'; diff --git a/tests/TestCase/Renderer/HeadingIdTrackerTest.php b/tests/TestCase/Renderer/HeadingIdTrackerTest.php index c028f26..6476323 100644 --- a/tests/TestCase/Renderer/HeadingIdTrackerTest.php +++ b/tests/TestCase/Renderer/HeadingIdTrackerTest.php @@ -163,6 +163,26 @@ public function testNormalizeId(): void $this->assertSame('h-1-Introduction', $this->tracker->normalizeId('1. Introduction')); } + /** + * Pins behaviour discussed in jgm/djot#391 (spec wording on auto-ID generation). + * + * djot-php sides with djot.js / djoths on remove-vs-replace (mid-word punctuation + * becomes `-`), and deliberately deviates on apostrophes / quotes / `;` / `:` by + * also replacing them, so generated IDs are valid CSS identifiers and safe to use + * with `querySelector()`. + */ + public function testNormalizeIdSpecAlignmentEdgeCases(): void + { + $this->assertSame('A-B-C', $this->tracker->normalizeId('A+B=C')); + $this->assertSame('Emphasis-strong', $this->tracker->normalizeId('Emphasis/strong')); + $this->assertSame('That-s-all', $this->tracker->normalizeId("That's all")); + $this->assertSame('foo-bar', $this->tracker->normalizeId('foo...bar')); + $this->assertSame('Uber-uns', $this->tracker->normalizeId('Uber uns')); + $this->assertSame('Über-uns', $this->tracker->normalizeId('Über uns')); + $this->assertSame('h-2024-recap', $this->tracker->normalizeId('2024 recap')); + $this->assertSame('heading', $this->tracker->normalizeId('!!!')); + } + public function testGetPlainText(): void { $heading = new Heading(2);