Fix/code block language label - #363
Closed
sonthicse wants to merge 2 commits into
Closed
Conversation
getCodeLanguage() matched `language-shellsession` and then discarded it, because `shellsession` is not in the hardcoded CODE_LANGUAGES set, leaving the fence with no language at all. The whitelist can also produce a wrong language: once the value is dropped, the ancestor walk keeps going and picks up whatever a wrapper declares. On the Docusaurus code-block docs two blocks were tagged `markdown` while holding CSS and a diff. `language-`/`lang-`/`syntax-`/`brush-` prefixes are declarations by the page (CommonMark, Prism, highlight.js), so the value is now validated by shape rather than by membership, and a closed list no longer has to keep up with every highlighter. Ambiguous patterns (`code-X`, `X-snippet`, bare class names) still check the whitelist — they match layout classes too easily. Shape alone is not enough on an arbitrary ancestor, where `language-switcher` or `syntax-highlighter` is ordinary markup rather than a declaration, so it applies only to the matched block and to pre/code elements; ancestors above that keep going through the whitelist. Validation allows at most one hyphen or underscore, which keeps multi-word layout classes out — `language-in-header-enabled` and `syntax-highlighter-line-number` both occur in this repo's fixtures. Tokens meaning "no language" and BCP 47 subtags are rejected, so neither `lang-en` nor `lang-zh-hans` becomes a fence tag, and Prism's diff-highlight `language-diff-<lang>` resolves to `diff`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Language labels and copy buttons were only removed by class name (`header`, `toolbar`, `titlebar`). Sites built on utility-first CSS name classes after appearance rather than role, so a label reading `class="float-end opacity-40 absolute top-0"` matched nothing and its text was extracted into the code content — a reader copying the block pastes the language name as the first command. When a <pre> already contains a <code>, the code lives in that <code>; short div/span siblings of it are chrome. That holds regardless of how the classes are named, and requiring the <code> to exist leaves blocks that put content directly in the <pre> untouched. Removing text is worse than leaving a stray label, so a sibling is kept whenever it might be code: when it repeats a sibling with the same tag and class, since one element per line is a rendering pattern while a label appears once (Chroma marks its line spans "cl", with no "line" in the name); when it is or contains a line element; when it holds code, a table or an image; and when its text is pure punctuation, as shell prompt markers are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Symptom
A code block on a documentation site rendered like this:
Two separate defects in one block:
shellsessionas the first command.Expected:
Source HTML
Three things matter here, and none of them are specific to one site:
language-shellsessionsits on the<pre>, not the<code>.<span>inside the<pre>but outside the<code>, positioned with utility classes.Cause 1 — the language whitelist rejects explicit declarations
getCodeLanguage()matchedlanguage-shellsessionand extractedshellsession, then dropped it becauseCODE_LANGUAGESdoes not contain that name:class="language-X"is the CommonMark / Prism / highlight.js convention — the page is stating the language outright. Filtering that through a fixed list means every unlisted language needs a library patch.shellsession,liquid,css,diff,console,hclandjsoncare all absent today.Note the asymmetry:
data-lang/data-languagereturn immediately without consulting the whitelist. Only the class branch filters, which looks unintended.The whitelist does not merely lose languages — it can pick the wrong one. Because the resolver walks up the ancestors when it finds nothing, a rejected value on the element falls through to whatever an ancestor declares. On the Docusaurus code-block docs, two blocks were tagged
markdownwhen their content was CSS and a diff respectively.Cause 2 — chrome is detected by class name
The UI-chrome filter only matches on names:
The label above carries
float-end opacity-40 right-5 absolute top-0 m-2 text-xs. Nothing matches, so it survives andextractStructuredText()folds its text into the code. Guessing at class names cannot work against utility-first CSS, where names describe appearance rather than role.The fix
Two independent commits.
1. Trust explicit declarations, keep checking guesses.
HIGHLIGHTER_PATTERNSis split.language-/lang-/syntax-/brush-state the language, so the value is validated by shape.code-X,X-snippetand bare class names are guesses that match layout classes too easily, so they still consult the whitelist. The whitelist itself is unchanged.Shape alone is not enough on an arbitrary ancestor, where
language-switcherorsyntax-highlighteris ordinary markup rather than a declaration, so shape-based resolution applies only to the matched block and topre/codeelements; ancestors above that keep going through the whitelist. Validation allows at most one hyphen or underscore, which keeps multi-word layout classes out —language-in-header-enabledandsyntax-highlighter-line-numberboth occur in this repo's fixtures. Tokens meaning "no language" and BCP 47 subtags are rejected, so neitherlang-ennorlang-zh-hansbecomes a fence tag, and Prism's diff-highlightlanguage-diff-<lang>resolves todiff.2. Remove chrome by structure, not by name. Once a
<pre>contains a<code>, the code lives in that<code>; shortdiv/spansiblings of it are chrome. This holds no matter how the classes are named, and requiring the<code>to exist leaves blocks that put content directly in the<pre>untouched.Removing text is worse than leaving a stray label, so a sibling is kept whenever it might be code: when it repeats a sibling with the same tag and class, since one element per line is a rendering pattern while a label appears once (Chroma marks its line spans
cl, with nolinein the name); when it is or contains a line element; when it holds code, a table or an image; and when its text is pure punctuation, as shell prompt markers are.Testing
Two fixtures, each failing before its own commit and passing after:
codeblocks--unlisted-language-class— explicit declaration of a language the whitelist does not know, no chrome. Isolates cause 1.codeblocks--tailwind-lang-label— the full case above. Needs both fixes.All 443 pre-existing tests are unchanged, under both linkedom and jsdom. Bundle size grows 0.5 KB gzip.
Spot-checked against live pages, diffing output before and after:
liquid/css; no content changedshellsessionmarkdowntocssanddiffHappy to split this into two PRs if you prefer — either commit stands alone.