fix(plugin-markdown): teach extractToc's emphasis rules CommonMark's flanking rule (objectui#7667) - #7672
Merged
Merged
Conversation
…flanking rule (objectui#7667) `stripInline()` gave `*` and `_` one shared regex, so it did not know that a `_` run inside a word opens nothing. The renderer keeps those underscores, so `### NON_GRID_ROW_CEILING` slugged `nongridrow_ceiling` in the TOC against the `non_grid_row_ceiling` that `rehype-slug` put on the anchor. The underscore form is now its own flanking-aware rule; the asterisk rules are unchanged, because only `_` carries the intraword exemption. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7667
extractToc's ids must be the idsrehype-slugactually puts on the renderedheadings, because that is the only thing a
#idlink can resolve to. Oneshape in this repository's own docs did not qualify.
What was wrong
stripInline()ended with two hand-rolled emphasis rules that gave*and_a single shared regex:
Neither knew CommonMark's flanking rule, under which a
_run inside a wordopens nothing — it is both left- and right-flanking with no adjacent
punctuation, so it may neither open nor close emphasis. The renderer obeys that
and keeps the underscores. The shared rule paired the first two underscores of
### NON_GRID_ROW_CEILINGand ateGRID, then resumed past them and ateROW:extractTocidrehype-slugemits### NON_GRID_ROW_CEILINGnongridrow_ceilingnon_grid_row_ceilingThe TOC entry rendered, was clickable, and silently went nowhere. Live
instance:
packages/react/README.md:224.The fix
The underscore form becomes its own flanking-aware rule; the asterisk rules are
left byte-identical, because only
_carries the intraword exemption —giving
*the same one would breaka*b*c, which the renderer really doesemphasise. That counter-direction is pinned too.
Specialised to
_, CommonMark's can-open / can-close conditions each reduce toone boundary test on either side of the whole delimiter run, which is what
the new regex spells:
A negative lookbehind and lookahead for one more underscore anchor each match
to a whole run, which keeps
x__init__yliteral instead of pairing that run'sinner underscores. A matched pair is
dropped at whatever length it has, because it contributes no characters to the
rendered text however it nests (
___x___rendersx).The one-underscore case is a regression pin, not a repair.
## the snake_case namewas already correct — a lone underscore has nothing to pairwith, which is exactly why this went unnoticed — and it still slugs
the-snake_case-name. It takes two or more underscores in one heading for theold italic rule to find a pair.
Evidence
Truth source throughout is the real render pipeline, never a second derivation
of the flanking rule: each heading is rendered through
MarkdownImplandextractToc's id compared to theidattributerehype-slugactuallyemitted. Every measurement below carries a lit control.
Red first. The new pins were written and run against unmodified
toc.ts.Predicted in writing beforehand: 4 of the 7 new tests red. Observed: exactly
those 4, at exactly the predicted cases.
Corpus sweep, re-run. Same corpus the card measured —
content/docs/**plus every
packages/*/README.md, frontmatter stripped from both sides.Predicted before the baseline run: 223 files / 2941 rendered headings / 5
divergent files. Observed: exactly that. After the fix: 223 / 2941 / 4.
The 4 that remain are all the same, different, already-filed defect — an extra
field-schemaentryextractToclists that the renderer never emits, under aJSX block with no blank line, which shifts every id after it:
content/docs/fields/{date,rich-text,text,textarea}.mdx. That is #7666, filedas needing a decision because
apps/siterenders the same.mdxthroughfumadocs; it is deliberately not addressed here and remains open.
Non-vacuity. The naive shared regexes were reinstated on the committed tree
and the pins re-run. Predicted: the same 4 tests red. Observed: the same 4 red,
11 passing. The mutation was proved on disk by anchored fixed-string counts
(new rule 1 to 0, naive rule 0 to 1) and
git hash-objectmovement off the HEADblob; restore ran from a trap installed before the mutation, using absolute
paths from
git rev-parse --show-toplevelandgit checkout HEAD -- ABSOLUTE_PATH,and was verified three ways — blob equality with the HEAD blob, an empty
git diff HEAD, and the anchored counts back at 1/0. The ablation subjectresolves through a relative source specifier (
import { extractToc } from './toc'),not through the package's
exportsintodist/, so no rebuild gates the leg.Gates, all at the final commit
6a44becf, exit codes captured by redirectbefore any pipe, each quoted from the gate's own verdict line:
pnpm --filter @object-ui/plugin-markdown testTest Files 5 passed (5)·Tests 46 passed (46)pnpm --filter @object-ui/plugin-markdown type-checktsc --noEmit && tsc -p tsconfig.test.jsonpnpm --filter @object-ui/plugin-markdown lint✖ 7 problems (0 errors, 7 warnings)— all pre-existing, inMermaid.tsx/index.tsxpnpm changeset:check✅ All workspace packages are in the changeset fixed group.pnpm check:control-bytes✅ check-control-bytes: OK (scanned 6256 tracked text file(s); skipped 85 binary).Declared narrowing. The repo-wide
pnpm lintfarm is CI's run, notduplicated here. The narrowing excludes nothing, on three readings: the
population came from eslint's own config —
eslint .insidepackages/plugin-markdowncovers the whole package, a superset of the twochanged source files; the count came from
--format json(2 files, 0 errors, 0warnings), and
.changeset/*.mdis outside eslint's configured populationentirely ("File ignored because no matching configuration was supplied"); and
eslint.config.jsdeclares neitherparserOptions.projectnorprojectService, so linting is not type-aware and no untouched file's verdictcan depend on the contents of
toc.ts.Clause-② determination: no
Own determination, same conclusion as the dispatching seat. The criterion is
whether the card changes contract accept/reject behaviour or widens the
published surface, and neither moves:
stripInline(), a module-private helper — onedefinition, one call site, never exported and never re-exported from the
package entry.
extractToc's signature, itsoptsshape and the
TocIteminterface are untouched; the diff contains noexportand nointerfaceline.extractTocaccepted everystring before and accepts every string now. It never validated or rejected.
anything authorable.
@objectstack/specis untouched.What moves is only the value of an id that was measurably wrong. That value
now equals what
rehype-slugemits — which is the contractTocItem.id's owndoc comment already published ("Slug id, identical to what
rehype-slugputson the rendered heading"). The change brings the implementation to its declared
contract rather than changing the contract.
Scope
packages/plugin-markdown/src/toc.tsonly, plus its pins and a changeset.Nothing under
scripts/was touched. #7666 is not addressed here.🤖 Generated with Claude Code
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Generated by Claude Code