fix(plugin-markdown): keep code-span content out of extractToc's HTML strip (objectui#7658) - #7668
Merged
Conversation
… strip (objectui#7658) `stripInline()` unwrapped an inline code span and then ran the raw-HTML rule over the result, so tag-shaped text that was INSIDE backticks was deleted: `objectui add <component>` slugged to `objectui-add` while `rehype-slug` put `objectui-add-component` on the rendered heading, and the TOC's `#id` named an anchor that does not exist. The same sequencing let the emphasis rules eat the underscores out of `a_b_c` and the link rule rewrite `[x](y)`. Code spans are now lifted into an opaque slot before any other inline rule and restored verbatim at the end. The raw-HTML rule is unchanged and still strips genuine markup, which is what the renderer does (`remark-rehype` runs without `allowDangerousHtml`, dropping raw html nodes and keeping the text they wrapped — measured, not assumed). The slot stays ordinary text to the link rule, so a code-span link label still collapses to its label. Pinned against the real render pipeline: the new test renders each heading through `MarkdownImpl` and compares `extractToc`'s id to the `id` attribute `rehype-slug` actually emitted, over the three heading shapes this repo's own docs carry (7 live headings across cli.mdx, runner.mdx, packages/cli/README.md). 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
|
os-sam
marked this pull request as ready for review
September 4, 2026 16:21
os-sam
deleted the
claude/issue-7658-extracttoc-inline-code-stripping
branch
September 4, 2026 16:36
This was referenced Sep 4, 2026
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 #7658
The defect
stripInline()inpackages/plugin-markdown/src/toc.tsapplied its rules in sequence: the inline-code rule unwrapped a code span, and the raw-HTML rule that ran next, over that same text deleted tag-shaped text the code span had contained. In the rendered DOM that text is literal —rehype-slugslugs the heading's flattened value and a code span's content is a text value — so the idextractTocproduced named an anchor that does not exist. A TOC entry that renders, is clickable, and silently does nothing.Measured against the real render pipeline, before the fix:
extractTocidrehype-slugputs on the headingobjectui generate TYPE NAME(aliasg)objectui-generate---alias-gobjectui-generate-type-name-alias-gobjectui add COMPONENTobjectui-addobjectui-add-component?api=BASE)serving-metadata-over-http-apiserving-metadata-over-http-apibaseSeven live headings in this repo's own docs:
content/docs/utilities/cli.mdx(3),content/docs/utilities/runner.mdx(1),packages/cli/README.md(3).The same sequencing had two more consequences the card did not name, both found by the same measurement and both repaired here: the emphasis rules ate the underscores out of a code span (
a_b_cslugged asabc), and the link rule rewrote a code span that documents link syntax.The fix
Code spans are lifted into an opaque slot before any other inline rule and restored verbatim at the end, so nothing reaches inside one. This is the shape the card listed as "tokenising code spans before any other inline rule", and the same approach PR #7657 takes for the gate.
Two properties the slot has to hold at once, and both are pinned:
*,_or angle bracket, so those rules cannot see into a code span any more.The raw-HTML rule is unchanged and still strips genuine markup. That was verified rather than assumed:
remark-rehyperuns withoutallowDangerousHtml, so it drops raw html nodes and keeps the text they wrapped — a heading with a bold tag around a word renders asa-bold-tag, which removing the tags reproduces exactly. This settles the second question the card left open ("a heading containing genuine raw HTML flattens to its raw text under mdast, not to nothing"): for this pipeline it flattens to the wrapped text, and the existing rule already matches it.The private-use sentinels are written as escape spellings in the source, so the file carries no raw non-ASCII byte; any sentinel present in the input is dropped first, so no input can forge a slot.
Truth source
scripts/github-slug.mjsfrom PR #7657 is not on this base (checked at8e501cb9, the merge-base) so nothing here depends on it, and neither it norscripts/check-doc-links.mjsis touched. As the brief directed, this package uses the realgithub-sluggerit already depends on.The pin does not re-derive the slug rules at all — re-deriving them would only prove two derivations agree.
packages/plugin-markdown/src/toc-anchor-parity.test.tsxrenders each heading throughMarkdownImpl(the real remark/rehype chain this plugin ships) and comparesextractToc's id to theidattributerehype-slugactually emitted. Reading the rendered id is what proves the anchor exists.Evidence
All runs at
d3ba962a, through the shared verify lock (slotissue-7658); wall-clock figures from it are shared-box readings, not idle-box.Red first. The new pin was run before the repair existed: 5 failed / 3 passed — the three live shapes, the duplicate-suffix case, and the code-span-literal case. Predicted 5 failures before running; observed 5. The three rendered ids in the table above came out of that run, confirming the card's measurements against the real renderer.
Green.
vitest run packages/plugin-markdown/—Test Files 5 passed (5),Tests 39 passed (39). Union atd3ba962aincluding the console docs-portal consumer:Test Files 6 passed (6),Tests 45 passed (45),VITEST_EXIT=0.Non-vacuity (ablation). Predicted in writing before the leg: red with exactly 5 failures / 3 passes, then 8/8 after restore. Observed exactly that.
b0eae89fto1d49f884), anchored grep countsSLOT_OPEN2 to 0 and the old one-line code rule 0 to 1. The leg refuses to run if any of those checks disagrees.Tests 5 failed | 3 passed (8),ABLATED_VITEST_EXIT=1.git checkout HEAD -- ABSOLUTE_PATH(never a bare checkout) under anEXIT INT TERMtrap holding absolute paths fromgit rev-parse --show-toplevel; verified by blob equality back tob0eae89f, emptygit diff HEAD, andSLOT_OPENback to 2../tocand./MarkdownImplby relative path from source, so nodistsits between the mutation and the assertion.Type-check / lint.
pnpm --filter @object-ui/plugin-markdown run type-check—TYPECHECK_EXIT=0. Package lint —PKG_LINT_EXIT=0(7 pre-existing warnings, 0 errors). Repo-wideeslint . --no-inline-config --format json: 4259 files linted, and both changed files report 0 errors and 0 warnings; the 93 errors in that run are pre-existing across 77 other files, none of them touched here. No narrowing to declare — the full population was linted.Gates, exit codes captured by redirect before any pipe, each quoted from the gate's own verdict line:
check:control-bytesOK (6254 tracked text files);check:phantom-depsOK;check:self-importOK;check:esm-specifiersOK;check:vi-mock-specifiersOK;check:vi-mock-inheritOK;check:shell-escape-residueOK;check:published-tsconfig-excludeOK;check:side-effects-arrayOK;check:entry-guardOK;check:doc-fencesOK;check:published-distOK;check:dist-completenessOK.check:readme-exportsfirst reportedPRECONDITION NOT MET— "the population COLLAPSED, this run proves nothing", 28 packages unbuilt. Recorded as NOT MEASURED, not as a pass and not as a red; the workspace was then built (39/39 turbo tasks) and it was re-run: OK, with a lit population — 37 of 40 packages read, 52 keys compared both ways.Changeset gates:
check-changeset-presenceOK — "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)";check-changeset-no-majorOK;check-changeset-overwriteOK.skip-changesetis not applied.Clause 2 determination: no contract review needed
extractToc's signature, itsoptsshape and theTocIteminterface are all byte-identical to before; nothing is added to or withdrawn from the package's exports, and no schema key is added, removed or re-typed. The change is entirely inside a private module-scoped helper, and what moves is the value of an id that was measurably wrong. Consumers that relied on the broken id would have been relying on an anchor that does not exist. So this is a consumer-side bug fix and can land through the merge queue;needs:contract-reviewis not applied to either carrier.The visible
textfield does change for affected headings — it now keeps the code span's content, which is what the heading actually reads. Its only consumer,apps/console/src/pages/DocPage.tsx, renders it as React text (nodangerouslySetInnerHTMLanywhere in that file), so the restored characters are escaped by React and cannot become markup.Adjacent gaps: measured and filed, not repaired here
A corpus sweep was run to check whether this change left anything behind — every file in
content/docsplus everypackages/*/README.mdrendered throughMarkdownImpland compared id-by-id withextractToc. 223 files, 2941 rendered headings (a lit population, not a zero reading); 5 files still diverge, in two classes, neither of them this card's defect class. Per the scope fence they are measured and filed rather than repaired here:extractToclists a heading the renderer never emits, when the heading sits on the line right after a JSX/raw-HTML block with no blank line. 4 live files, allcontent/docs/fields/*.mdx.NON_GRID_ROW_CEILINGslugs asnongridrow_ceilingwhile the anchor readsnon_grid_row_ceiling. 1 live instance,packages/react/README.md:224. That card also records setext headings being invisible toextractToc, with zero live instances measured.Both were dedup-searched first over all 383 open issues in this repo (repo-scoped REST list plus local grep, after a repo-scoped REST read returned HTTP 200), with a lit control: the term
extractTocreturns objectui#7658, so the instrument was demonstrably not dark. Neither card is assigned and neither carries labels; grading is the triage seat's.Left untouched per the scope fence:
scripts/check-doc-links.mjs,scripts/github-slug.mjs,scripts/__tests__/check-doc-links.test.ts(PR #7657's territory), thegetDataConfigproducers, andpackages/types/src/registry.ts/complex.ts.Generated by Claude Code in session
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3(a seat-level id, not a per-change one), dispatched by thedomain:uiPM seat.🤖 Generated with Claude Code
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Generated by Claude Code