Keep non-rendered text out of comment anchors - #168
Merged
HamptonMakes merged 2 commits intoAug 6, 2026
Conversation
A selection swept across a Mermaid diagram invisibly captures the SVG's embedded <style> sheet: textContent includes it even though it never renders. The stored anchor then re-matches that CSS on every visit, and highlightAnchors() wraps <mark>s inside the <style> element. A <style> only parses its direct child text, so the re-parented chunk drops out of the sheet and the whole diagram loses its styling — black node boxes and black-filled edge paths. The corruption is theme-dependent, which made it look like a light-mode bug: the anchor stores one theme's CSS text, so it only matches (and only corrupts) when the diagram renders in that same theme. Fix: define one rendered-text model — every text node under the content target except those inside style/script/noscript — and use it everywhere offsets are computed: anchor capture, context extraction, occurrence counting, and highlighting. Existing anchors that already carry CSS text simply stop matching, which degrades to the standard "content changed, no highlight" behavior instead of corrupting the page. Verified against a real plan with 71 comment threads: before, two CSS-carrying anchors put marks inside the diagram's <style> and broke all styling; after, every legitimate anchor still highlights, no marks land in any <style>, and all four diagrams keep their full stylesheets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two regression tests for the black-diagram bug, driving the real
pipeline (Mermaid rendered in the browser, real Stimulus controllers):
- A stored anchor carrying stylesheet text (the shape of anchors
captured before the fix) must not put a <mark> inside the SVG's
<style> — the sheet keeps zero element children and a parseable rule
list — while label and prose anchors keep highlighting.
- A selection swept across a diagram must capture anchor text without
the stylesheet: visible text only, no CSS.
Both fail against the previous controller — the capture test reproduces
the exact poisoned-anchor shape observed in production ("The diagram
#coplan-mermaid-1{font-family:...") — and pass with the fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kelchou
marked this pull request as ready for review
August 6, 2026 17:44
HamptonMakes
approved these changes
Aug 6, 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.
Example link to issue: https://coplan-square.sqprod.co/plans/019faa9b-4de6-7d61-9f4b-be329aacafa2
Problem
Mermaid diagrams on a plan can render as unstyled black shapes — big black node boxes and black-filled edge paths — while the same plan looks fine for other people.
The trigger is a comment whose anchor was captured by sweeping a selection across a diagram.
checkSelectionextracts anchor text withrange.cloneContents().textContent, andtextContentincludes the<style>sheet Mermaid embeds inside its SVG even though that text never renders. The stored anchor then carries several KB of theme CSS.On every subsequent visit,
highlightAnchors()matches that anchor againstcontentTarget.textContent(which also contains the CSS), andhighlightAtIndexAll()'s TreeWalker wraps<mark>elements around text inside the<style>element. A<style>only parses its direct child text, so the re-parented chunk silently drops out of the sheet — the whole diagram loses its stylesheet and falls back to SVG defaults: black fills everywhere.The corruption is theme-dependent, which disguises it as a light/dark bug: the anchor stores the CSS text of whichever theme was active at capture time, so it only re-matches (and only corrupts) when the diagram renders in that same theme. In the observed case the anchor held light-theme CSS — light-mode viewers got a black diagram, dark-mode viewers saw it fine.
Fix
Define one rendered-text model — every text node under the content target except those inside
style/script/noscript— and use it at every site that computes text offsets:checkSelection(anchor capture): strips non-rendered elements from the cloned fragment before readingtextContent, so new anchors can never embed CSS.extractContext,computeOccurrence,getSelectionOffset,scrollToAnchor,highlightAnchors,highlightAtIndexAll: all walk the same_renderedTextNodes()sequence, so offsets stay mutually consistent and marks can never be wrapped inside a<style>.Existing anchors that already carry CSS text simply stop matching, which degrades to the standard "content changed → no highlight" behavior instead of corrupting the page. The threads themselves remain fully usable from the sidebar.
Verification
System specs (
spec/system/mermaid_anchor_spec.rb, added in this PR): two regression tests driving the real pipeline — Mermaid rendered in the browser, real controllers. A stored anchor carrying stylesheet text must leave the SVG<style>free of marks with its sheet parseable (while label and prose anchors keep highlighting), and a selection swept across a diagram must capture visible text without CSS. Both fail against the previous controller — the capture test reproduces the exact poisoned-anchor shape observed in production (The diagram #coplan-mermaid-1{font-family:...) — and pass with the fix. Full suite run locally: the only failures are pre-existing onmain(request/service specs unrelated to this change).Browser harness against a real production plan page (71 comment threads, 4 Mermaid diagrams, 2 CSS-poisoned anchors), running Mermaid 11.16.0 with the exact controller config:
<style>Visually confirmed: the affected flowchart renders fully styled in light mode after the fix, with prose anchor highlights still working.
🤖 Generated with Claude Code