@llui/lexical-loro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/agent-write.d.ts +123 -0
  4. package/dist/agent-write.d.ts.map +1 -0
  5. package/dist/agent-write.js +499 -0
  6. package/dist/agent-write.js.map +1 -0
  7. package/dist/binding.d.ts +122 -0
  8. package/dist/binding.d.ts.map +1 -0
  9. package/dist/binding.js +114 -0
  10. package/dist/binding.js.map +1 -0
  11. package/dist/index.d.ts +69 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +69 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/mapping.d.ts +115 -0
  16. package/dist/mapping.d.ts.map +1 -0
  17. package/dist/mapping.js +181 -0
  18. package/dist/mapping.js.map +1 -0
  19. package/dist/order.d.ts +124 -0
  20. package/dist/order.d.ts.map +1 -0
  21. package/dist/order.js +187 -0
  22. package/dist/order.js.map +1 -0
  23. package/dist/schema.d.ts +343 -0
  24. package/dist/schema.d.ts.map +1 -0
  25. package/dist/schema.js +363 -0
  26. package/dist/schema.js.map +1 -0
  27. package/dist/seed.d.ts +72 -0
  28. package/dist/seed.d.ts.map +1 -0
  29. package/dist/seed.js +72 -0
  30. package/dist/seed.js.map +1 -0
  31. package/dist/text.d.ts +167 -0
  32. package/dist/text.d.ts.map +1 -0
  33. package/dist/text.js +289 -0
  34. package/dist/text.js.map +1 -0
  35. package/dist/to-lexical.d.ts +119 -0
  36. package/dist/to-lexical.d.ts.map +1 -0
  37. package/dist/to-lexical.js +636 -0
  38. package/dist/to-lexical.js.map +1 -0
  39. package/dist/to-loro.d.ts +154 -0
  40. package/dist/to-loro.d.ts.map +1 -0
  41. package/dist/to-loro.js +718 -0
  42. package/dist/to-loro.js.map +1 -0
  43. package/dist/undo.d.ts +94 -0
  44. package/dist/undo.d.ts.map +1 -0
  45. package/dist/undo.js +200 -0
  46. package/dist/undo.js.map +1 -0
  47. package/package.json +64 -0
  48. package/src/agent-write.ts +613 -0
  49. package/src/binding.ts +185 -0
  50. package/src/index.ts +176 -0
  51. package/src/mapping.ts +206 -0
  52. package/src/order.ts +205 -0
  53. package/src/schema.ts +509 -0
  54. package/src/seed.ts +112 -0
  55. package/src/text.ts +357 -0
  56. package/src/to-lexical.ts +792 -0
  57. package/src/to-loro.ts +914 -0
  58. package/src/undo.ts +269 -0
package/dist/text.d.ts ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Text: format-bitmask ↔ named-Loro-mark conversion, the run diff that replays
3
+ * Lexical's resulting state, and a cursor-biased string diff.
4
+ *
5
+ * ── Why formats are DECOMPOSED into named marks ────────────────────────────
6
+ *
7
+ * `TextNode.__format` is a bitmask. Storing it as ONE Loro value makes two peers
8
+ * concurrently toggling bold and italic on overlapping ranges a last-writer-wins
9
+ * conflict that SILENTLY DROPS one of them. Stored as independent named marks
10
+ * (`bold`, `italic`, …) the same edit converges to the union. This is measured
11
+ * in `test/expand-semantics.test.ts` §4, both the working and the broken form.
12
+ *
13
+ * The bit values are derived from Lexical's `TEXT_TYPE_TO_FORMAT` at runtime —
14
+ * never hardcoded, so a Lexical release that renumbers or adds a format is a
15
+ * compile/boot-time failure here rather than silent corruption.
16
+ *
17
+ * ── Why the outbound direction replays STATE, not keystrokes ───────────────
18
+ *
19
+ * Loro's `expand` rule cannot reproduce Lexical's boundary behaviour: no uniform
20
+ * table works, and no PER-FORMAT table can either, because the divergence set is
21
+ * identical for all 11 formats — Lexical has no per-format inclusivity, its
22
+ * caret is uniformly left-biased (`LexicalSelection.ts:3164-3204`). Three cases
23
+ * are unreachable by any table:
24
+ *
25
+ * 1. typing at index 0 of a formatted first run
26
+ * 2. a format toggled ON at a collapsed caret
27
+ * 3. a format toggled OFF at a collapsed caret
28
+ *
29
+ * So after each Lexical update we diff the RESULTING TextNode runs against what
30
+ * Loro currently holds and emit explicit `mark`/`unmark` ops for every format
31
+ * bit that differs — {@link diffRunFormats}. `expand` then governs only what
32
+ * happens to text a REMOTE peer inserts concurrently at a mark boundary.
33
+ *
34
+ * ── Index units ────────────────────────────────────────────────────────────
35
+ *
36
+ * All offsets here are UTF-16 code units: JavaScript string indices, Lexical
37
+ * offsets, and loro-crdt's JS text indices all agree (pinned in
38
+ * `test/schema.test.ts`). No conversion at this seam.
39
+ */
40
+ import type { LoroText } from 'loro-crdt';
41
+ /**
42
+ * The Lexical text formats a Loro binding represents, in bit order (see
43
+ * Lexical's `LexicalConstants.ts`). Each becomes an INDEPENDENT named mark.
44
+ */
45
+ export declare const LORO_TEXT_FORMATS: readonly ["bold", "italic", "strikethrough", "underline", "code", "subscript", "superscript", "highlight", "lowercase", "uppercase", "capitalize"];
46
+ export type LoroTextFormat = (typeof LORO_TEXT_FORMATS)[number];
47
+ /**
48
+ * Mark name → bit value, derived from Lexical rather than hardcoded.
49
+ *
50
+ * Built eagerly so a format this package names but Lexical does not define
51
+ * fails at module load — a loud boot error instead of a format that silently
52
+ * never round-trips.
53
+ */
54
+ export declare const FORMAT_BITS: Readonly<Record<LoroTextFormat, number>>;
55
+ /** Every bit this binding knows how to represent. */
56
+ export declare const KNOWN_FORMAT_MASK: number;
57
+ /** The bit value of a named format. */
58
+ export declare function formatBit(format: LoroTextFormat): number;
59
+ /** Decompose a Lexical bitmask into the named marks it sets, in bit order. */
60
+ export declare function formatsFromBitmask(bitmask: number): LoroTextFormat[];
61
+ /** Recompose named marks into a Lexical bitmask. */
62
+ export declare function bitmaskFromFormats(formats: Iterable<LoroTextFormat>): number;
63
+ /**
64
+ * Read a Loro delta's attribute bag as a Lexical bitmask.
65
+ *
66
+ * Only `true` counts as set: Loro represents an unmark as an explicit `null`
67
+ * attribute in the delta, which must read as OFF, not as "present".
68
+ */
69
+ export declare function bitmaskFromAttributes(attributes: Readonly<Record<string, unknown>> | undefined): number;
70
+ /** A maximal stretch of text sharing one Lexical format bitmask. */
71
+ export interface TextRun {
72
+ readonly text: string;
73
+ readonly format: number;
74
+ }
75
+ /** Concatenated text of a run list. */
76
+ export declare function runsText(runs: readonly TextRun[]): string;
77
+ /**
78
+ * Coalesce adjacent equal-format runs and drop empty ones, so two run lists
79
+ * describing the same content compare structurally equal.
80
+ *
81
+ * Necessary because Lexical's node boundaries are a rendering detail: `ab`+`c`
82
+ * and `abc` at the same format are the same document.
83
+ */
84
+ export declare function normalizeRuns(runs: readonly TextRun[]): TextRun[];
85
+ /** A Loro text delta item, as returned by `LoroText#toDelta`. */
86
+ export interface TextDeltaItem {
87
+ readonly insert?: unknown;
88
+ readonly attributes?: Readonly<Record<string, unknown>>;
89
+ }
90
+ /** Project a `LoroText`'s delta into normalized Lexical-shaped runs. */
91
+ export declare function runsFromDelta(delta: readonly TextDeltaItem[]): TextRun[];
92
+ /** Project a live `LoroText` into normalized Lexical-shaped runs. */
93
+ export declare function runsFromText(text: LoroText): TextRun[];
94
+ /** An explicit format operation to apply to a `LoroText`. */
95
+ export interface MarkOp {
96
+ readonly kind: 'mark' | 'unmark';
97
+ /** Inclusive UTF-16 start offset. */
98
+ readonly start: number;
99
+ /** Exclusive UTF-16 end offset. */
100
+ readonly end: number;
101
+ readonly format: LoroTextFormat;
102
+ }
103
+ /**
104
+ * Diff two run lists into the minimal explicit `mark`/`unmark` ops that turn
105
+ * `current` into `target`.
106
+ *
107
+ * `current` is what Loro holds after the text edit landed (so `expand` has
108
+ * already had its say); `target` is the runs Lexical actually produced. Both
109
+ * MUST describe the same character count — call this only after the text
110
+ * content has been reconciled.
111
+ *
112
+ * Each format is diffed INDEPENDENTLY (that is the whole point of decomposing
113
+ * the bitmask) and differing characters are coalesced into maximal ranges, so a
114
+ * whole-paragraph bolding is one op, not one per character.
115
+ */
116
+ export declare function diffRunFormats(current: readonly TextRun[], target: readonly TextRun[]): MarkOp[];
117
+ /**
118
+ * Apply {@link MarkOp}s to a `LoroText`. The caller owns the surrounding
119
+ * transaction (`doc.commit`), so a whole Lexical update lands as one Loro
120
+ * change and therefore as one remote event batch.
121
+ */
122
+ export declare function applyMarkOps(text: LoroText, ops: readonly MarkOp[]): void;
123
+ /** A single-region string edit: delete `remove` chars at `index`, insert `insert`. */
124
+ export interface TextDiff {
125
+ /** UTF-16 offset at which the change applies. */
126
+ readonly index: number;
127
+ /** Number of UTF-16 code units to delete at `index`. */
128
+ readonly remove: number;
129
+ /** Text to insert at `index` after the deletion. */
130
+ readonly insert: string;
131
+ }
132
+ /**
133
+ * Diff `a` → `b`, biased to place the change at `cursor`.
134
+ *
135
+ * A plain "common prefix / common suffix" diff is ambiguous whenever the edit
136
+ * sits next to repeated characters: typing `o` in `foo` could be described as an
137
+ * insert at index 1, 2 or 3, and the plain diff always picks the leftmost. Every
138
+ * peer then sees the character inserted at the wrong place, which drags remote
139
+ * carets and (through Loro's `expand`) can even attach the wrong formatting.
140
+ *
141
+ * Biasing the prefix scan to stop AT the cursor resolves the ambiguity in favour
142
+ * of where the user actually typed. `@lexical/yjs` uses `simpleDiffWithCursor`
143
+ * for exactly this reason; this is that algorithm (lib0's
144
+ * `simpleDiffStringWithCursor`), including its surrogate-pair rollbacks, ported
145
+ * so the package carries no lib0 dependency.
146
+ *
147
+ * Surrogate handling: the scans never stop between the halves of a surrogate
148
+ * pair, so an astral character is always inserted or deleted whole.
149
+ *
150
+ * @param cursor UTF-16 offset of the caret in `b` (the new string).
151
+ */
152
+ export declare function diffTextWithCursor(a: string, b: string, cursor: number): TextDiff;
153
+ /**
154
+ * Cursor-free variant: the change is placed as far LEFT as possible.
155
+ *
156
+ * Equivalent to lib0's `simpleDiffString`. Use it only where no caret is known
157
+ * (a programmatic document change); prefer {@link diffTextWithCursor} on any
158
+ * user-typing path, where the leftmost placement is exactly the wrong guess.
159
+ */
160
+ export declare function diffText(a: string, b: string): TextDiff;
161
+ /**
162
+ * Apply a {@link TextDiff} to a `LoroText`. A no-op diff writes nothing, so a
163
+ * Lexical update that changed only formatting produces no text ops (and so no
164
+ * spurious remote text event).
165
+ */
166
+ export declare function applyTextDiff(text: LoroText, diff: TextDiff): void;
167
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"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"}
package/dist/text.js ADDED
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Text: format-bitmask ↔ named-Loro-mark conversion, the run diff that replays
3
+ * Lexical's resulting state, and a cursor-biased string diff.
4
+ *
5
+ * ── Why formats are DECOMPOSED into named marks ────────────────────────────
6
+ *
7
+ * `TextNode.__format` is a bitmask. Storing it as ONE Loro value makes two peers
8
+ * concurrently toggling bold and italic on overlapping ranges a last-writer-wins
9
+ * conflict that SILENTLY DROPS one of them. Stored as independent named marks
10
+ * (`bold`, `italic`, …) the same edit converges to the union. This is measured
11
+ * in `test/expand-semantics.test.ts` §4, both the working and the broken form.
12
+ *
13
+ * The bit values are derived from Lexical's `TEXT_TYPE_TO_FORMAT` at runtime —
14
+ * never hardcoded, so a Lexical release that renumbers or adds a format is a
15
+ * compile/boot-time failure here rather than silent corruption.
16
+ *
17
+ * ── Why the outbound direction replays STATE, not keystrokes ───────────────
18
+ *
19
+ * Loro's `expand` rule cannot reproduce Lexical's boundary behaviour: no uniform
20
+ * table works, and no PER-FORMAT table can either, because the divergence set is
21
+ * identical for all 11 formats — Lexical has no per-format inclusivity, its
22
+ * caret is uniformly left-biased (`LexicalSelection.ts:3164-3204`). Three cases
23
+ * are unreachable by any table:
24
+ *
25
+ * 1. typing at index 0 of a formatted first run
26
+ * 2. a format toggled ON at a collapsed caret
27
+ * 3. a format toggled OFF at a collapsed caret
28
+ *
29
+ * So after each Lexical update we diff the RESULTING TextNode runs against what
30
+ * Loro currently holds and emit explicit `mark`/`unmark` ops for every format
31
+ * bit that differs — {@link diffRunFormats}. `expand` then governs only what
32
+ * happens to text a REMOTE peer inserts concurrently at a mark boundary.
33
+ *
34
+ * ── Index units ────────────────────────────────────────────────────────────
35
+ *
36
+ * All offsets here are UTF-16 code units: JavaScript string indices, Lexical
37
+ * offsets, and loro-crdt's JS text indices all agree (pinned in
38
+ * `test/schema.test.ts`). No conversion at this seam.
39
+ */
40
+ import { TEXT_TYPE_TO_FORMAT } from 'lexical';
41
+ // ---------------------------------------------------------------------------
42
+ // Format vocabulary
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * The Lexical text formats a Loro binding represents, in bit order (see
46
+ * Lexical's `LexicalConstants.ts`). Each becomes an INDEPENDENT named mark.
47
+ */
48
+ export const LORO_TEXT_FORMATS = [
49
+ 'bold',
50
+ 'italic',
51
+ 'strikethrough',
52
+ 'underline',
53
+ 'code',
54
+ 'subscript',
55
+ 'superscript',
56
+ 'highlight',
57
+ 'lowercase',
58
+ 'uppercase',
59
+ 'capitalize',
60
+ ];
61
+ /**
62
+ * Mark name → bit value, derived from Lexical rather than hardcoded.
63
+ *
64
+ * Built eagerly so a format this package names but Lexical does not define
65
+ * fails at module load — a loud boot error instead of a format that silently
66
+ * never round-trips.
67
+ */
68
+ export const FORMAT_BITS = Object.freeze(Object.fromEntries(LORO_TEXT_FORMATS.map((format) => {
69
+ const bit = TEXT_TYPE_TO_FORMAT[format];
70
+ if (bit === undefined) {
71
+ throw new Error(`lexical-loro: Lexical does not define the text format '${format}' — ` +
72
+ 'LORO_TEXT_FORMATS is out of sync with TEXT_TYPE_TO_FORMAT');
73
+ }
74
+ return [format, bit];
75
+ })));
76
+ /** Every bit this binding knows how to represent. */
77
+ export const KNOWN_FORMAT_MASK = LORO_TEXT_FORMATS.reduce((mask, format) => mask | FORMAT_BITS[format], 0);
78
+ /** The bit value of a named format. */
79
+ export function formatBit(format) {
80
+ return FORMAT_BITS[format];
81
+ }
82
+ /** Decompose a Lexical bitmask into the named marks it sets, in bit order. */
83
+ export function formatsFromBitmask(bitmask) {
84
+ return LORO_TEXT_FORMATS.filter((format) => (bitmask & FORMAT_BITS[format]) !== 0);
85
+ }
86
+ /** Recompose named marks into a Lexical bitmask. */
87
+ export function bitmaskFromFormats(formats) {
88
+ let bitmask = 0;
89
+ for (const format of formats)
90
+ bitmask |= FORMAT_BITS[format];
91
+ return bitmask;
92
+ }
93
+ /**
94
+ * Read a Loro delta's attribute bag as a Lexical bitmask.
95
+ *
96
+ * Only `true` counts as set: Loro represents an unmark as an explicit `null`
97
+ * attribute in the delta, which must read as OFF, not as "present".
98
+ */
99
+ export function bitmaskFromAttributes(attributes) {
100
+ if (attributes === undefined)
101
+ return 0;
102
+ let bitmask = 0;
103
+ for (const format of LORO_TEXT_FORMATS) {
104
+ if (attributes[format] === true)
105
+ bitmask |= FORMAT_BITS[format];
106
+ }
107
+ return bitmask;
108
+ }
109
+ /** Concatenated text of a run list. */
110
+ export function runsText(runs) {
111
+ return runs.map((run) => run.text).join('');
112
+ }
113
+ /**
114
+ * Coalesce adjacent equal-format runs and drop empty ones, so two run lists
115
+ * describing the same content compare structurally equal.
116
+ *
117
+ * Necessary because Lexical's node boundaries are a rendering detail: `ab`+`c`
118
+ * and `abc` at the same format are the same document.
119
+ */
120
+ export function normalizeRuns(runs) {
121
+ const out = [];
122
+ for (const run of runs) {
123
+ if (run.text === '')
124
+ continue;
125
+ const last = out[out.length - 1];
126
+ if (last !== undefined && last.format === run.format) {
127
+ out[out.length - 1] = { text: last.text + run.text, format: last.format };
128
+ }
129
+ else {
130
+ out.push({ text: run.text, format: run.format });
131
+ }
132
+ }
133
+ return out;
134
+ }
135
+ /** Project a `LoroText`'s delta into normalized Lexical-shaped runs. */
136
+ export function runsFromDelta(delta) {
137
+ return normalizeRuns(delta.map((item) => ({
138
+ text: String(item.insert ?? ''),
139
+ format: bitmaskFromAttributes(item.attributes),
140
+ })));
141
+ }
142
+ /** Project a live `LoroText` into normalized Lexical-shaped runs. */
143
+ export function runsFromText(text) {
144
+ return runsFromDelta(text.toDelta());
145
+ }
146
+ /**
147
+ * Expand runs into a per-character bitmask array.
148
+ *
149
+ * Per-character is what makes the run diff correct without special-casing:
150
+ * the two sides may be segmented completely differently (Lexical split a node
151
+ * where Loro did not), and comparing per character makes segmentation moot.
152
+ */
153
+ function bitmaskPerCharacter(runs) {
154
+ const out = [];
155
+ for (const run of runs) {
156
+ for (let i = 0; i < run.text.length; i++)
157
+ out.push(run.format);
158
+ }
159
+ return out;
160
+ }
161
+ /**
162
+ * Diff two run lists into the minimal explicit `mark`/`unmark` ops that turn
163
+ * `current` into `target`.
164
+ *
165
+ * `current` is what Loro holds after the text edit landed (so `expand` has
166
+ * already had its say); `target` is the runs Lexical actually produced. Both
167
+ * MUST describe the same character count — call this only after the text
168
+ * content has been reconciled.
169
+ *
170
+ * Each format is diffed INDEPENDENTLY (that is the whole point of decomposing
171
+ * the bitmask) and differing characters are coalesced into maximal ranges, so a
172
+ * whole-paragraph bolding is one op, not one per character.
173
+ */
174
+ export function diffRunFormats(current, target) {
175
+ const from = bitmaskPerCharacter(current);
176
+ const to = bitmaskPerCharacter(target);
177
+ if (from.length !== to.length) {
178
+ throw new Error(`lexical-loro: diffRunFormats requires equal-length runs — ` +
179
+ `current has ${from.length} chars, target has ${to.length}; ` +
180
+ 'reconcile the text content before diffing formats');
181
+ }
182
+ const ops = [];
183
+ for (const format of LORO_TEXT_FORMATS) {
184
+ const bit = FORMAT_BITS[format];
185
+ let start = -1;
186
+ let openKind = 'mark';
187
+ // Iterate one PAST the end so a range still open at the last character is
188
+ // closed by the same branch as any other, with no duplicated tail logic.
189
+ for (let i = 0; i <= from.length; i++) {
190
+ let kind;
191
+ if (i < from.length) {
192
+ const wanted = (to[i] & bit) !== 0;
193
+ const has = (from[i] & bit) !== 0;
194
+ if (wanted !== has)
195
+ kind = wanted ? 'mark' : 'unmark';
196
+ }
197
+ if (start !== -1 && kind !== openKind) {
198
+ ops.push({ kind: openKind, start, end: i, format });
199
+ start = -1;
200
+ }
201
+ if (kind !== undefined && start === -1) {
202
+ start = i;
203
+ openKind = kind;
204
+ }
205
+ }
206
+ }
207
+ return ops;
208
+ }
209
+ /**
210
+ * Apply {@link MarkOp}s to a `LoroText`. The caller owns the surrounding
211
+ * transaction (`doc.commit`), so a whole Lexical update lands as one Loro
212
+ * change and therefore as one remote event batch.
213
+ */
214
+ export function applyMarkOps(text, ops) {
215
+ for (const op of ops) {
216
+ if (op.kind === 'mark')
217
+ text.mark({ start: op.start, end: op.end }, op.format, true);
218
+ else
219
+ text.unmark({ start: op.start, end: op.end }, op.format);
220
+ }
221
+ }
222
+ const HIGH_SURROGATE = /[\uD800-\uDBFF]/;
223
+ const LOW_SURROGATE = /[\uDC00-\uDFFF]/;
224
+ /**
225
+ * Diff `a` → `b`, biased to place the change at `cursor`.
226
+ *
227
+ * A plain "common prefix / common suffix" diff is ambiguous whenever the edit
228
+ * sits next to repeated characters: typing `o` in `foo` could be described as an
229
+ * insert at index 1, 2 or 3, and the plain diff always picks the leftmost. Every
230
+ * peer then sees the character inserted at the wrong place, which drags remote
231
+ * carets and (through Loro's `expand`) can even attach the wrong formatting.
232
+ *
233
+ * Biasing the prefix scan to stop AT the cursor resolves the ambiguity in favour
234
+ * of where the user actually typed. `@lexical/yjs` uses `simpleDiffWithCursor`
235
+ * for exactly this reason; this is that algorithm (lib0's
236
+ * `simpleDiffStringWithCursor`), including its surrogate-pair rollbacks, ported
237
+ * so the package carries no lib0 dependency.
238
+ *
239
+ * Surrogate handling: the scans never stop between the halves of a surrogate
240
+ * pair, so an astral character is always inserted or deleted whole.
241
+ *
242
+ * @param cursor UTF-16 offset of the caret in `b` (the new string).
243
+ */
244
+ export function diffTextWithCursor(a, b, cursor) {
245
+ let left = 0;
246
+ let right = 0;
247
+ // Scan the common prefix, but stop at the cursor: the user's caret marks
248
+ // where the change really is.
249
+ while (left < a.length && left < b.length && a[left] === b[left] && left < cursor)
250
+ left++;
251
+ if (left > 0 && HIGH_SURROGATE.test(a[left - 1]))
252
+ left--;
253
+ // Scan the common suffix.
254
+ while (right + left < a.length &&
255
+ right + left < b.length &&
256
+ a[a.length - right - 1] === b[b.length - right - 1])
257
+ right++;
258
+ if (right > 0 && LOW_SURROGATE.test(a[a.length - right]))
259
+ right--;
260
+ // Resume the prefix scan past the cursor — anything still common there is not
261
+ // part of the edit, and leaving it in would make the diff larger than needed.
262
+ while (right + left < a.length && right + left < b.length && a[left] === b[left])
263
+ left++;
264
+ if (left > 0 && HIGH_SURROGATE.test(a[left - 1]))
265
+ left--;
266
+ return { index: left, remove: a.length - left - right, insert: b.slice(left, b.length - right) };
267
+ }
268
+ /**
269
+ * Cursor-free variant: the change is placed as far LEFT as possible.
270
+ *
271
+ * Equivalent to lib0's `simpleDiffString`. Use it only where no caret is known
272
+ * (a programmatic document change); prefer {@link diffTextWithCursor} on any
273
+ * user-typing path, where the leftmost placement is exactly the wrong guess.
274
+ */
275
+ export function diffText(a, b) {
276
+ return diffTextWithCursor(a, b, 0);
277
+ }
278
+ /**
279
+ * Apply a {@link TextDiff} to a `LoroText`. A no-op diff writes nothing, so a
280
+ * Lexical update that changed only formatting produces no text ops (and so no
281
+ * spurious remote text event).
282
+ */
283
+ export function applyTextDiff(text, diff) {
284
+ if (diff.remove > 0)
285
+ text.delete(diff.index, diff.remove);
286
+ if (diff.insert !== '')
287
+ text.insert(diff.index, diff.insert);
288
+ }
289
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Inbound sync: the Loro mirror → a Lexical `EditorState`.
3
+ *
4
+ * ── The one thing this must not do ─────────────────────────────────────────
5
+ *
6
+ * loro-prosemirror's inbound path replaces the whole document on every remote
7
+ * event (`sync-plugin.ts`). ProseMirror tolerates that. LEXICAL CANNOT: its
8
+ * `NodeKey` is a bare counter (`LexicalUtils.ts`: `'' + keyCounter++`), so a
9
+ * rebuild mints a fresh key for every node, which tears down all DOM, destroys
10
+ * the selection and IME composition, and — decisively for this stack — destroys
11
+ * and remounts EVERY `LLuiDecoratorNode`, because `packages/lexical/decorator.ts`
12
+ * disposes the mounted LLui sub-app on the 'destroyed' mutation.
13
+ *
14
+ * So this module is a PERSISTENT MIRROR: it resolves each remote change to an
15
+ * existing `NodeKey` through the registry in `mapping.ts` and MUTATES THAT NODE
16
+ * IN PLACE. Nodes nothing touched are never written, so their keys — and
17
+ * therefore their DOM, their mounts, and any selection inside them — survive by
18
+ * construction. That is the model `@lexical/yjs`'s `SyncEditorStates.ts` uses,
19
+ * and it is the only one that works here.
20
+ *
21
+ * ── Why it reconciles from STATE, not from the deltas ──────────────────────
22
+ *
23
+ * The events say WHERE the document changed; this module then reconciles those
24
+ * containers against Loro's CURRENT state rather than replaying each delta. Four
25
+ * reasons, all learned from the event shapes loro-crdt actually emits:
26
+ *
27
+ * 1. A batch that creates a subtree emits the parent's list insert AND a
28
+ * separate event for every descendant container. Replaying all of them
29
+ * double-applies; reconciling from state is idempotent, so the duplicate
30
+ * descriptions of one change collapse to one reconciliation.
31
+ * 2. A `LoroMovableList#move` arrives as an insert plus a delete carrying the
32
+ * SAME `ContainerID`. Reconciling by identity recognises that as a move —
33
+ * the whole point of the movable-list schema — where a delta replay would
34
+ * see a delete and rebuild the subtree.
35
+ * 3. A map deletion is reported as an `undefined` value in `MapDiff.updated`,
36
+ * which is indistinguishable from an absent key in most JS handling. Reading
37
+ * the map is unambiguous.
38
+ * 4. It is the mirror image of `to-loro.ts`, so both directions share one
39
+ * notion of "these two trees agree" (`text.ts`'s normalized runs) and cannot
40
+ * drift apart in their idea of what a difference is.
41
+ *
42
+ * The events are therefore used as a DIRTY SET, exactly as `to-loro.ts` uses
43
+ * Lexical's `dirtyElements`/`dirtyLeaves`: every element on the path from the
44
+ * root to a changed container is marked, and the walk descends only into marked
45
+ * children. A remote keystroke costs O(tree depth), not O(document).
46
+ *
47
+ * ── Selection ──────────────────────────────────────────────────────────────
48
+ *
49
+ * Untouched nodes keep their keys, so a caret outside the changed run needs no
50
+ * help at all. Only the case where a remote change lands INSIDE the exact text
51
+ * run holding the caret needs work: there the caret's offset is transformed
52
+ * through the same single-region diff used to update the run (a caret at or
53
+ * before the edit point does not move; one after it shifts by
54
+ * `insert.length - remove`; one inside a deleted span clamps to the edit point).
55
+ * That is deliberately narrow — reaching for cursors more broadly would mean
56
+ * writing selection on every remote event, which is its own source of jitter.
57
+ */
58
+ import { type LexicalEditor } from 'lexical';
59
+ import { type LoroDoc, type LoroEventBatch } from 'loro-crdt';
60
+ import { ContainerNodeMap } from './mapping.js';
61
+ import { type ElementContainer } from './schema.js';
62
+ /**
63
+ * Tags stamped on every inbound writeback.
64
+ *
65
+ * `COLLABORATION_TAG` is echo layer (b): `to-loro.ts` skips updates carrying it,
66
+ * so our own writeback cannot bounce back into the shared document.
67
+ * `SKIP_SCROLL_INTO_VIEW_TAG` stops a peer's edit from yanking the local
68
+ * viewport.
69
+ *
70
+ * `PROGRAMMATIC_TAG` is deliberately ABSENT and must stay that way — echo layer
71
+ * (c). `packages/lexical/src/foreign.ts` treats that tag as "the host pushed new
72
+ * content: cancel pending outbound work and rebase", so a remote writeback
73
+ * carrying it would silently cancel the local user's in-flight debounced
74
+ * `onChange` and the host's persistence would go dark whenever a peer types.
75
+ */
76
+ export declare const INBOUND_TAGS: readonly string[];
77
+ /** The Lexical side of the binding, plus the shared document it mirrors. */
78
+ export interface InboundTarget {
79
+ readonly doc: LoroDoc;
80
+ /** The root element container, as returned by `initDoc`. */
81
+ readonly root: ElementContainer;
82
+ /** The ContainerID ↔ NodeKey registry, owned by the binding and mutated here. */
83
+ readonly mapping: ContainerNodeMap;
84
+ readonly editor: LexicalEditor;
85
+ /**
86
+ * Commit origins whose LOCAL batches must still be applied.
87
+ *
88
+ * Echo layer (a) drops `by: 'local'` batches — they are this peer's own
89
+ * outbound writes coming back round. But not every local batch is an echo:
90
+ *
91
+ * - the CRDT-aware undo manager (`undo.ts`) produces LOCAL batches from
92
+ * `undo()`/`redo()` (`UNDO_ORIGINS`), and
93
+ * - the agent-write reconciler (`agent-write.ts`) writes the document directly
94
+ * under {@link import('./agent-write.js').AGENT_WRITE_ORIGIN},
95
+ *
96
+ * neither of which came FROM the editor, so both MUST be applied to bounce into
97
+ * it (preserving `NodeKey`s and decorator mounts). `binding.ts` passes those
98
+ * origins here. A local batch whose origin is on this list is applied; every
99
+ * other local batch is still dropped as an echo.
100
+ */
101
+ readonly localOrigins?: readonly string[];
102
+ }
103
+ /**
104
+ * Apply one Loro event batch to the editor.
105
+ *
106
+ * @returns whether anything was applied. `false` means the batch was an echo of
107
+ * our own write, or described no change the editor could see.
108
+ */
109
+ export declare function applyLoroToLexical(target: InboundTarget, batch: LoroEventBatch): boolean;
110
+ /**
111
+ * Reconcile the ENTIRE shared document into the editor, with no dirty gate.
112
+ *
113
+ * Used at boot by a peer adopting a document it has no event history for (see
114
+ * `seed.ts`), and as the fallback whenever an event's container ancestry cannot
115
+ * be resolved. Full-fidelity and identity-preserving: adopting a document the
116
+ * editor already matches writes nothing and churns no NodeKeys.
117
+ */
118
+ export declare function adoptLoroDocument(target: InboundTarget): boolean;
119
+ //# sourceMappingURL=to-lexical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-lexical.d.ts","sourceRoot":"","sources":["../src/to-lexical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAEH,OAAO,EAYL,KAAK,aAAa,EAMnB,MAAM,SAAS,CAAA;AAChB,OAAO,EAIL,KAAK,OAAO,EACZ,KAAK,cAAc,EACpB,MAAM,WAAW,CAAA;AAElB,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EASL,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAA;AAOpB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAmD,CAAA;AAE7F,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;IAClC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;IAC9B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAaxF;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAEhE"}