Skip to content

CommandPalette: the grapheme-snap ceiling is weighed against the escaped copy, so long-ish labels lose the snap #387

Description

@IgorShevchik

Environment

main @ 54b93e33, reproduced on Node 22.

Description

#371 added a grapheme-cluster snap with a length ceiling — GRAPHEME_SNAP_MAX_LENGTH = 8192 — past which the snap degrades to surrogate pairs only. The ceiling is documented, and tested, as a bound on the field text.

It is not measured against the field text. truncateHTMLFromStart builds its snapper from html, which is content: the value after escaping and after <mark> insertion. content.length is always >= value.length, and escaping inflates it without any fixed bound — & becomes five characters, " becomes six.

So the ceiling bites early, and how early depends on the content:

const FLAG = '\u{1F1FA}\u{1F1F8}'   // 🇺🇸

// value.length = 8185 — seven characters under the documented ceiling
'a'.repeat(8140) + FLAG.repeat(10) + 'match'

// value.length = 1805 — 22% of the documented ceiling
'&'.repeat(1600) + FLAG.repeat(50) + 'match'

Both produce:

"...🇸🇺🇸🇺🇸🇺🇸🇺🇸🇺🇸🇺🇸<mark>match</mark>"

The flags re-pair into 🇸🇺 with an orphaned regional indicator at the end — a different country, with nothing to signal the loss. That is exactly the defect #364 describes and #371 exists to prevent; it simply reappears above a threshold that no reader can predict from the constant.

Why the tests did not catch it

The degraded paths block does test a 13-character discrepancy — but every fixture in it uses 'a'.repeat(...) as filler. No fixture contains an escapable character, so only the fixed single-mark overhead is covered, never the unbounded part.

.sync/PORTING.md records the discrepancy as "the threshold is crossed 13 characters earlier than a reading of value.length suggests". That understates it: the real bound is O(escapable characters), not a constant.

Additional context

Not a security issue — escaping happens before this, and the output is escaped either way. The consequence is wrong visible text.

Found by an independent review pass over #371.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions