Skip to content

Keep non-rendered text out of comment anchors - #168

Merged
HamptonMakes merged 2 commits into
block:mainfrom
kelchou:keep-non-rendered-text-out-of-anchors
Aug 6, 2026
Merged

Keep non-rendered text out of comment anchors#168
HamptonMakes merged 2 commits into
block:mainfrom
kelchou:keep-non-rendered-text-out-of-anchors

Conversation

@kelchou

@kelchou kelchou commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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. checkSelection extracts anchor text with range.cloneContents().textContent, and textContent includes 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 against contentTarget.textContent (which also contains the CSS), and highlightAtIndexAll()'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 reading textContent, 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 on main (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:

old text model fixed text model
marks landing inside a diagram <style> 2 0
diagram stylesheets intact after highlight pass 0 of 4 4 of 4
legitimate anchors still highlighted 31 31 (same set)
anchors matching at all 32 31 (only the CSS-poisoned anchor stops matching)

Visually confirmed: the affected flowchart renders fully styled in light mode after the fix, with prose anchor highlights still working.

🤖 Generated with Claude Code

kelchou and others added 2 commits August 3, 2026 12:58
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
kelchou marked this pull request as ready for review August 6, 2026 17:44
@kelchou
kelchou requested a review from HamptonMakes as a code owner August 6, 2026 17:44
@HamptonMakes
HamptonMakes merged commit 80abf4f into block:main Aug 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants