Skip to content

feat: make generated heading ids optional - #46

Merged
brendt merged 1 commit into
tempestphp:mainfrom
RobinDev:feat/optional-heading-ids
Sep 15, 2026
Merged

brendt merged 1 commit into
tempestphp:mainfrom
RobinDev:feat/optional-heading-ids

Conversation

@RobinDev

@RobinDev RobinDev commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Part of #13 — your first example there is $markdown->getRule(HeadingRule::class)->showAnchors(true);, and this is the rule half of it.

Every heading gets an id slugged from its content, with no way to opt out. HeadingRule now takes generateIds:

new Parser(rules: [new HeadingRule(generateIds: false)])->parse('## A heading');
// <h2>A heading</h2>

An id written explicitly is kept either way, so ## A heading ## custom-id still renders <h2 id="custom-id">. Default is true, nothing changes for anyone who doesn't ask.

A public promoted property rather than a showAnchors() setter: it reads the same at construction (new HeadingRule(generateIds: false)) and after the fact ($markdown->getRule(HeadingRule::class)->generateIds = false), so it works whichever way #13 lands. Happy to rename it to your showAnchors if you prefer that vocabulary — I picked generateIds only because the rule emits an id attribute rather than an anchor element.

One gap, and it is #18's

Configuring the rule reaches the document, but not a heading nested inside another token, because DivToken still hard-codes new HeadingRule():

$markdown = new Markdown()
    ->removeRules(HeadingRule::class)
    ->prependRules(new HeadingRule(generateIds: false));

$markdown->parse('## A heading')->html;
// <h2>A heading</h2>                                  ← configured

$markdown->parse(":::alert\n## A heading\n:::")->html;
// <div class="alert"><h2 id="a-heading">A heading</h2></div>   ← not configured

#45 closes that, by resolving a token's default rules through the configured set. This PR doesn't depend on it and is mergeable on its own; the two together make the option hold everywhere.

Why I care

I maintain an integration that adds heading ids later in its own render chain, from the whole page rather than from one block, so the parser has to leave them out. Today that means stripping the attribute back out of the HTML with a regex.

Measured on a personal dataset of real-world Markdown: automatic ids are the single biggest reason a document does not already render identically through plain Tempest — ahead of every correctness bug reported in #37 combined. This one option moves the share of documents that match from roughly three in five to roughly three in four.

composer test (331 tests), lint and analyse are clean.

@github-actions

Copy link
Copy Markdown

Benchmark Results

Comparison of feat/optional-heading-ids against main (5e06bea775768b2af19579ec592d17d625bac4ef).

Open to see the benchmark results

No benchmark changes above ±5%.

Generated by phpbench against commit 66906a1

@brendt
brendt merged commit 119bba8 into tempestphp:main Sep 15, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants