Summary
On web, the text emitted by onChangeSelection is not round-trip compatible with setLink, because the two use different serializations of the same range. Passing the event's own text back into setLink (the documented way to link the current selection) corrupts any multi-block or image-containing selection — it merges the blocks into one paragraph and drops inline formatting. Separately, setLink silently no-ops when text is empty.
Verified on 1.1.0, web.
The inconsistency
onChangeSelection serializes the selected text with nativeLeafText, which inserts a \n at block boundaries and  for inline leaves:
// src/web/EnrichedTextInput.tsx:276
const text = nativeLeafText(state.doc, from, to);
// src/web/positionMapping.ts:29
return doc.textBetween(from, to, '\n', () => '');
But setLink compares the caller's text against a different serialization — plain doc.textBetween(from, to) with no block separator and no leaf placeholder:
// src/web/formats/EnrichedLink.ts:165
const currentText = doc.textBetween(from, to);
if (text !== currentText) {
tr.replaceWith(from, to, s.schema.text(text, marksWithLink)); // :170 destructive
} else {
tr.addMark(from, to, linkMark); // :172 intended
}
For any selection spanning a block boundary (or containing an inline image), text contains \n/ while currentText does not, so text !== currentText and it takes the replaceWith branch. Result: the selected blocks collapse into a single paragraph, the \n/ become literal glyphs in the link text, and inline marks inside the range are lost.
Reproduction (public API only)
Two paragraphs First paragraph / Second paragraph. Select across the boundary, then apply the link using the text from onChangeSelection:
// sel = the {start, end, text} from the last onChangeSelection event
// -> { start: 15, end: 32, text: "\nSecond paragraph" }
ref.current.setLink(sel.start, sel.end, sel.text, "https://example.com");
Expected: the link is applied across the selection (or a safe no-op).
Actual:
Before: <p>First paragraph</p><p>Second paragraph</p>
After: <p>First paragraph<a href="https://example.com">\nSecond paragraph</a></p>
The two paragraphs are merged and the \n is rendered as a literal glyph inside the anchor.
Secondary: empty text is a silent no-op
// src/web/formats/EnrichedLink.ts:141
if (url.length === 0 || text.length === 0) {
return;
}
setLink(start, end, "", url) on a valid selection does nothing, with no return value or warning. Since "link the current selection without changing its text" is the most natural call, this is easy to hit and hard to diagnose. Confirmed on 1.1.0: setLink(6, 11, "", url) over a selected word leaves the document unchanged; setLink(6, 11, "word", url) works.
Suggested fix
Compute currentText in setLink with the same nativeLeafText serialization used by onChangeSelection (so a faithful round-trip hits the non-destructive addMark branch), and/or refuse a range whose text disagrees with the mapped content instead of silently flattening it. For the empty-text case, treating it as "apply the link mark to the existing selection" (or returning a boolean) would remove a sharp edge.
Environment
- react-native-enriched-html 1.1.0 (same code in 1.0.0)
- Platform: web
Summary
On web, the
textemitted byonChangeSelectionis not round-trip compatible withsetLink, because the two use different serializations of the same range. Passing the event's owntextback intosetLink(the documented way to link the current selection) corrupts any multi-block or image-containing selection — it merges the blocks into one paragraph and drops inline formatting. Separately,setLinksilently no-ops whentextis empty.Verified on 1.1.0, web.
The inconsistency
onChangeSelectionserializes the selected text withnativeLeafText, which inserts a\nat block boundaries andfor inline leaves:But
setLinkcompares the caller'stextagainst a different serialization — plaindoc.textBetween(from, to)with no block separator and no leaf placeholder:For any selection spanning a block boundary (or containing an inline image),
textcontains\n/whilecurrentTextdoes not, sotext !== currentTextand it takes thereplaceWithbranch. Result: the selected blocks collapse into a single paragraph, the\n/become literal glyphs in the link text, and inline marks inside the range are lost.Reproduction (public API only)
Two paragraphs
First paragraph/Second paragraph. Select across the boundary, then apply the link using thetextfromonChangeSelection:Expected: the link is applied across the selection (or a safe no-op).
Actual:
The two paragraphs are merged and the
\nis rendered as a literal glyph inside the anchor.Secondary: empty
textis a silent no-opsetLink(start, end, "", url)on a valid selection does nothing, with no return value or warning. Since "link the current selection without changing its text" is the most natural call, this is easy to hit and hard to diagnose. Confirmed on 1.1.0:setLink(6, 11, "", url)over a selected word leaves the document unchanged;setLink(6, 11, "word", url)works.Suggested fix
Compute
currentTextinsetLinkwith the samenativeLeafTextserialization used byonChangeSelection(so a faithful round-trip hits the non-destructiveaddMarkbranch), and/or refuse a range whosetextdisagrees with the mapped content instead of silently flattening it. For the empty-textcase, treating it as "apply the link mark to the existing selection" (or returning a boolean) would remove a sharp edge.Environment