fix(webapp): a broken mermaid fence says which line, and why (BEA-135) - #150
Open
ssowonny wants to merge 2 commits into
Open
fix(webapp): a broken mermaid fence says which line, and why (BEA-135)#150ssowonny wants to merge 2 commits into
ssowonny wants to merge 2 commits into
Conversation
A fence that doesn't parse showed "Couldn't render this diagram." and nothing else — no line, no parser output, no way to fix it. The diagnostic already existed: mermaid throws a parse error carrying the line number, the offending source, a caret column and the expected tokens, and the catch discarded it one line from where it was needed. Bind it and print it under the existing note, in a sibling element so .mermaid-err's text stays exactly what it was. textContent, never innerHTML: the message quotes the author's source verbatim and what renderMermaid returns is mounted through dangerouslySetInnerHTML. The seeded broken fence now carries a complete <img onerror=x> tag so the e2e proves that — short on purpose, since the parser's window is 20 characters of past input and a tag it truncated would leave no start tag for an innerHTML bug to mount. The line number is the diagram's, not the file's: the helper only ever sees rendered HTML, never the .md around it. Both surfaces, one helper: the hub viewer reads style.css, the share page reads the inline shell in shares.go and never loads the app's stylesheet. `white-space: pre` is load-bearing (the caret only lines up in a monospace, non-wrapping box) and max-height + overflow keeps a pathological message inside its own box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
go:embed needs the built output in the module. Split from the source commit because rollup re-hashes the whole mermaid chunk graph when its importer changes: 118 of these files are renames with byte-identical content, only their import filenames differ. Co-Authored-By: Claude Opus 5 (1M context) <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.
TL;DR
catch {dropped it on the floor one line from where it was needed.dangerouslySetInnerHTML./s/share page from one helper, but the CSS had to go in two places (the share shell never loads the app's stylesheet).flowchart LR E["mermaid throws:<br/>line, source, caret,<br/>expected tokens"] E -->|"before: catch {}"| D["discarded<br/>one faint line"] E -->|"after: catch (err)"| K["printed as textContent<br/>under the note"]The one thing that can't break
renderMermaidreturns a string thatFileView.tsxmounts throughdangerouslySetInnerHTML. The parse error embeds the author's source verbatim, so the insertion istextContentand the DOM serialization at the end of the helper escapes it. AninnerHTMLhere would be a stored-XSS path through any teammate's markdown.The e2e proves it, and the seeded fence had to change for the proof to mean anything: the parser's window is 20 characters of past input, so the spec's
<img src=x onerror=alert(1)>(28 chars) can never appear whole in the message — and a tag the parser truncated leaves no start tag for aninnerHTMLbug to mount, so the test would pass either way. The seed now carries<img onerror=x>, which fits, and the test asserts both halves: the literal characters are shown, and#content imgis0.Screenshots
Hub viewer (
diagram.mdin the seededwikiproject):The
/s/<token>share page for the same file:The share shell has its own inline CSS and its own dark block, so that got driven too:
What changed
frontend/src/lib/mermaid.tscatch (err); the message goes in a siblingdiv.mermaid-err-detailviatextContent, capped at 2000 charsfrontend/src/style.css.mermaid-err-detail— monospace,white-space: pre,max-height: 12em,overflow: autowebapp/shares.gowebapp/e2e_serve_test.go<img onerror=x>tagfrontend/e2e/browse.spec.tswebapp/static/white-space: preis load-bearing — the caret marker only lines up in a monospace, non-wrapping box.max-height+overflowis what keeps a pathological expected-token list inside its own box instead of pushing the document sideways; the 2000-char slice is the same cap on the DOM side.The diagnostic sits in a sibling of
.mermaid-errrather than inside it, which is what keeps the two existing exact-text assertions (browse.spec.tsfor the viewer and the share page) green with no edit.Deviations from the reviewed plan
Two, both because the plan's assertions couldn't hold against the real parser:
<img src=x onerror=alert(1)>never appears whole in the message (20-char window), and the plan'stoContainText("<img src=x onerror=alert(1)>")would have failed. Worse, asserting the truncated form would have made the test pass even with aninnerHTMLbug. Now<img onerror=x>.-.9em 0 1.3em→-.9em 0 .4em(and-8px 0 20px→-8px 0 4pxon the share page), so the detail block sits against the note instead of a paragraph gap away. The plan put a negative top margin on the detail instead; same result, one fewer negative margin.Not done, per the plan: file-relative line numbers, agent-visible errors, and any change to the chunk-blocked path.
Reviewer note on the diff size
The second commit renames 118 files under
static/assets/. Rollup re-hashes the whole mermaid chunk graph when its importer changes; the contents are byte-identical and only the embedded import filenames differ.check-dist.shdemands it, so it can't be left out — but it can be read as one commit and skipped.Acceptance
go build ./...,go vet ./...— cleango test ./...— all packages pass (internal/webapp209s)npm run e2e— 170 passed, 1 pre-existing skipnpm test(frontend unit) — 76 passfrontend/check-dist.sh—internal/webapp/static is freshwhite-space: pre, the detail box scrolling whiledocumentElementdoes not)Architecture diagrams unchanged: no types or relationships moved —
mermaid.ts's exported surface is the same, and.mermaid-err-detailis adiv, not a seam.Closes BEA-135.
Build session
(only works on the machine the build ran on)