SIGN IN SIGN UP

fix(ui): accept whitespace-only differences in restore verification, snap excluded selection starts, and merge folder-session chrome re-stamps (#1541)

* fix(ui): accept whitespace-only differences in restore verification

A restore whose stored positions resolve onto exactly the right text was
rejected whenever the annotation spanned two block elements. `originalText`
is the browser's selection string, which carries a blank line between
blocks; the painted highlight is the wrapper marks concatenated with
nothing between them, because sibling blocks are rendered from a `.map()`
with no whitespace text node. The verification collapsed whitespace to a
single space instead of removing it, so "Alpha.\n\nBeta." never matched
"Alpha.Beta." — the correct highlight was removed, the text search could
not bridge the boundary either, and the annotation came back from a
reload, a resubmit or a markdown edit with no highlight at all.

Compare with `compactText`, the helper the same change already added for
the quote repair's identical asymmetry. Content drift — the #1509 case
the guard exists for — still differs once whitespace is gone, so the
guard keeps its teeth.

* fix(ui): snap a selection that starts inside excluded chrome

web-highlighter never enters an `.annotation-exclude` subtree: its painter
skips such an element before the "is this the start node" check, so a range
that STARTS inside one never flips its in-selection flag and paints only
the trailing text node. A drag from a GitHub alert's icon — where the
visually hidden "Tip: " lives — through the alert body therefore
highlighted the body alone, silently dropping the title, while the quote
still carried the invisible word. A title holding any inline markup
reproduced it within the title row itself, because the quote repair only
recognizes the shape where the whole non-excluded selection is one
trailing run.

Move the range's start off the excluded subtree, onto the first
annotatable text position the range covers, before anything paints or
quotes it: by node identity, never by matching text. Applied on all three
paths that reach the highlighter — the pointer-end selection (on the
capture phase, since web-highlighter's own handler reads the live
selection), `highlightRange` for pinpoint clicks and vim visual
selections, and the touch selectionchange bridge.

* fix(ui): repair excluded quotes by node position, not by text search

The quote repair removed the FIRST occurrence of each excluded element's
text anywhere in the selection string. With body prose that legitimately
reads "Tip: ", a selection running through a tip alert's title row had the
reviewer's own words removed instead of the hidden ones; the result then
failed the repair's own content check and reverted, leaving the invisible
word in the quote after all — the exact bug the repair exists to fix.

Locate the excluded runs positionally instead, from the range's own text
nodes captured before painting (painting splits and re-parents them, so
the range itself is stale by the time the highlight is created). The runs
are matched in the quote's whitespace-free coordinate space, where the
selection string and the concatenated runs agree character for character,
so the right occurrence is removed and real prose survives. The painted
highlight stays the last word on content: a shape this cannot read leaves
the quote exactly as before.

* fix(annotate): route the chrome re-stamp through the same merge

Annotation activity re-stamps the HTML chrome record so the preference
does not expire for a reviewer who is still annotating. That write went
straight to saveHtmlChromeState with the live sidebar/panel/tools values,
bypassing mergeHtmlChromeState, which the save effect uses. It was
unreachable in folder sessions while the restore was suppressed there;
once the restore was enabled, one comment in a folder session — whose file
browser forces the sidebar open — rewrote both halves, so the reviewer's
next `annotate page.html` opened with the sidebar and the annotations
drawer forced open.

Both call sites now share one saveChrome() helper, which is the only
writer of the record, so a third write cannot reintroduce the bypass.
M
Michael Ramos committed
ec7a3eb8e8229d057e056ff5e8c8e68b282ccf6a
Parent: 11cbc94
Committed by GitHub <noreply@github.com> on 9/15/2026, 2:32:13 PM