Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/reference/enhancements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/TestCase/Extension/HeadingReferenceExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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-';
Expand Down
20 changes: 20 additions & 0 deletions tests/TestCase/Renderer/HeadingIdTrackerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading