@haklex/rich-collab-loro 0.44.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.
@@ -0,0 +1,15 @@
1
+ import { LexicalEditor, NodeKey } from 'lexical';
2
+ import { LoroDoc, TreeID } from 'loro-crdt';
3
+ export declare const COLLAB_TAG = "collab";
4
+ export declare const TREE_NAME = "lexical";
5
+ export interface LoroBinding {
6
+ dispose: () => void;
7
+ idOf: (key: NodeKey) => TreeID | undefined;
8
+ import: (bytes: Uint8Array) => void;
9
+ keyOf: (id: TreeID) => NodeKey | undefined;
10
+ }
11
+ export interface LoroBindingOptions {
12
+ commitMessage?: string;
13
+ }
14
+ export declare function createLoroBinding(editor: LexicalEditor, doc: LoroDoc, options?: LoroBindingOptions): LoroBinding;
15
+ //# sourceMappingURL=binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,aAAa,EAElB,KAAK,OAAO,EAGb,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,KAAK,OAAO,EAA+B,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAMnF,eAAO,MAAM,UAAU,WAAW,CAAC;AACnC,eAAO,MAAM,SAAS,YAAY,CAAC;AAGnC,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IAC3C,MAAM,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACpC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,SAAS,CAAC;CAC5C;AAED,MAAM,WAAW,kBAAkB;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAmBD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,kBAAuB,GAC/B,WAAW,CA4Nb"}
@@ -0,0 +1,4 @@
1
+ export { COLLAB_TAG, createLoroBinding, type LoroBinding, type LoroBindingOptions, TREE_NAME, } from './binding';
2
+ export { $reconcileRoot, editAtVersion } from './reconcile';
3
+ export { type Frame, type FrameFocus, planFrames, streamAtVersion, type StreamStep, } from './stream';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,SAAS,GACV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,KAAK,KAAK,EACV,KAAK,UAAU,EACf,UAAU,EACV,eAAe,EACf,KAAK,UAAU,GAChB,MAAM,UAAU,CAAC"}
package/dist/index.mjs ADDED
@@ -0,0 +1,516 @@
1
+ import { $getNodeByKey, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isTextNode, $parseSerializedNode } from "lexical";
2
+ import { LoroText } from "loro-crdt";
3
+ //#region src/offset.ts
4
+ function transformOffset(previous, next, offset) {
5
+ const limit = Math.min(previous.length, next.length);
6
+ let prefix = 0;
7
+ while (prefix < limit && previous[prefix] === next[prefix]) prefix++;
8
+ let suffix = 0;
9
+ while (suffix < limit - prefix && previous[previous.length - 1 - suffix] === next[next.length - 1 - suffix]) suffix++;
10
+ if (offset <= prefix) return offset;
11
+ if (offset >= previous.length - suffix) return offset + next.length - previous.length;
12
+ return next.length - suffix;
13
+ }
14
+ //#endregion
15
+ //#region src/stable.ts
16
+ function stable(value) {
17
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
18
+ if (value && typeof value === "object") return `{${Object.keys(value).filter((key) => value[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(",")}}`;
19
+ return JSON.stringify(value);
20
+ }
21
+ //#endregion
22
+ //#region src/reconcile.ts
23
+ var DERIVED_KEYS = { listitem: ["value"] };
24
+ function withoutDerived(json) {
25
+ const { children: _children, text: _text, ...rest } = json;
26
+ for (const key of DERIVED_KEYS[json.type] ?? []) delete rest[key];
27
+ return rest;
28
+ }
29
+ function shallow(json) {
30
+ return stable(withoutDerived(json));
31
+ }
32
+ function $serialize(node) {
33
+ const json = node.exportJSON();
34
+ if ($isElementNode(node)) json.children = node.getChildren().map($serialize);
35
+ return json;
36
+ }
37
+ function lcs(a, b) {
38
+ const table = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1));
39
+ for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--) table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
40
+ const pairs = [];
41
+ let i = 0;
42
+ let j = 0;
43
+ while (i < a.length && j < b.length) if (a[i] === b[j]) pairs.push([i++, j++]);
44
+ else if (table[i + 1][j] >= table[i][j + 1]) i++;
45
+ else j++;
46
+ return pairs;
47
+ }
48
+ function $updateInPlace(node, target) {
49
+ const props = withoutDerived(target);
50
+ node.updateFromJSON(props);
51
+ return shallow(node.exportJSON()) === shallow(target);
52
+ }
53
+ function $update(node, current, target) {
54
+ if ($isTextNode(node)) {
55
+ if (shallow(current) !== shallow(target)) node.updateFromJSON({
56
+ ...target,
57
+ text: node.getTextContent()
58
+ });
59
+ if (node.getTextContent() !== target.text) node.setTextContent(target.text ?? "");
60
+ return node;
61
+ }
62
+ if ($isElementNode(node)) {
63
+ let element = node;
64
+ if (shallow(current) !== shallow(target) && !$updateInPlace(node, target)) {
65
+ const replacement = $parseSerializedNode({
66
+ ...target,
67
+ children: []
68
+ });
69
+ if (!$isElementNode(replacement)) return node;
70
+ node.replace(replacement, true);
71
+ element = replacement;
72
+ }
73
+ $reconcileChildren(element, target.children ?? []);
74
+ return element;
75
+ }
76
+ if (stable(current) === stable(target) || $updateInPlace(node, target)) return node;
77
+ const replacement = $parseSerializedNode(target);
78
+ node.replace(replacement);
79
+ return replacement;
80
+ }
81
+ function pairable(current, target) {
82
+ if (current.type !== target.type) return false;
83
+ const a = current.$?.blockId;
84
+ const b = target.$?.blockId;
85
+ return a === void 0 || b === void 0 || a === b;
86
+ }
87
+ function $reconcileChildren(parent, targets) {
88
+ const current = parent.getChildren();
89
+ const currentJson = current.map($serialize);
90
+ const pairs = lcs(currentJson.map(stable), targets.map(stable));
91
+ const result = [];
92
+ let ci = 0;
93
+ let tj = 0;
94
+ for (const [i, j] of [...pairs, [current.length, targets.length]]) {
95
+ const pending = Array.from({ length: i - ci }, (_, k) => ci + k);
96
+ for (let t = tj; t < j; t++) {
97
+ const target = targets[t];
98
+ const blockId = target.$?.blockId;
99
+ let slot = blockId === void 0 ? -1 : pending.findIndex((c) => currentJson[c].$?.blockId === blockId && pairable(currentJson[c], target));
100
+ if (slot === -1) slot = pending.findIndex((c) => pairable(currentJson[c], target));
101
+ if (slot === -1) result.push($parseSerializedNode(target));
102
+ else {
103
+ const [c] = pending.splice(slot, 1);
104
+ result.push($update(current[c], currentJson[c], target));
105
+ }
106
+ }
107
+ if (i < current.length) result.push(current[i]);
108
+ ci = i + 1;
109
+ tj = j + 1;
110
+ }
111
+ const kept = new Set(result.map((node) => node.getKey()));
112
+ for (const node of parent.getChildren()) if (!kept.has(node.getKey())) node.remove(true);
113
+ result.forEach((node, index) => {
114
+ const at = parent.getChildAtIndex(index);
115
+ if (at && at.is(node)) return;
116
+ if (index === 0) {
117
+ const first = parent.getFirstChild();
118
+ if (first) first.insertBefore(node);
119
+ else parent.append(node);
120
+ } else result[index - 1].insertAfter(node);
121
+ });
122
+ }
123
+ function $reconcileRoot(target) {
124
+ const root = $getRoot();
125
+ const targetJson = target;
126
+ if (shallow($serialize(root)) !== shallow(targetJson)) root.updateFromJSON(target);
127
+ $reconcileChildren(root, (targetJson.children ?? []).map((child) => $serialize($parseSerializedNode(child))));
128
+ }
129
+ function editAtVersion(doc, frontiers, createEditor, target, commitMessage) {
130
+ const fork = doc.forkAt(frontiers);
131
+ const editor = createEditor();
132
+ const binding = createLoroBinding(editor, fork, { commitMessage });
133
+ const from = fork.oplogVersion();
134
+ editor.update(() => $reconcileRoot(target.root), { discrete: true });
135
+ binding.dispose();
136
+ return fork.export({
137
+ from,
138
+ mode: "update"
139
+ });
140
+ }
141
+ //#endregion
142
+ //#region src/binding.ts
143
+ var COLLAB_TAG = "collab";
144
+ var TREE_NAME = "lexical";
145
+ var TEXT_KEY = "#text";
146
+ function sameValue(a, b) {
147
+ return a === b || stable(a) === stable(b);
148
+ }
149
+ function nodeProps(node) {
150
+ const { children: _children, ...props } = node.exportJSON();
151
+ if ($isTextNode(node)) delete props.text;
152
+ return props;
153
+ }
154
+ function storedProps(treeNode) {
155
+ const { [TEXT_KEY]: _text, ...props } = treeNode.data.toJSON();
156
+ return props;
157
+ }
158
+ function createLoroBinding(editor, doc, options = {}) {
159
+ const tree = doc.getTree(TREE_NAME);
160
+ tree.enableFractionalIndex(0);
161
+ const keyToId = /* @__PURE__ */ new Map();
162
+ const idToKey = /* @__PURE__ */ new Map();
163
+ const link = (key, id) => {
164
+ const previousId = keyToId.get(key);
165
+ if (previousId !== void 0) idToKey.delete(previousId);
166
+ const previousKey = idToKey.get(id);
167
+ if (previousKey !== void 0) keyToId.delete(previousKey);
168
+ keyToId.set(key, id);
169
+ idToKey.set(id, key);
170
+ };
171
+ const alive = (id) => id !== void 0 && tree.has(id) && !tree.isNodeDeleted(id);
172
+ const synced = /* @__PURE__ */ new Map();
173
+ const readBaseline = /* @__PURE__ */ new Map();
174
+ const writeNode = (node, treeNode) => {
175
+ const data = treeNode.data;
176
+ const props = nodeProps(node);
177
+ const previous = synced.get(node.getKey()) ?? {};
178
+ let wrote = false;
179
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(props)])) {
180
+ const value = props[key];
181
+ if (sameValue(previous[key], value) && (value === void 0 || data.get(key) !== void 0)) continue;
182
+ if (value === void 0) {
183
+ if (data.get(key) !== void 0) {
184
+ data.delete(key);
185
+ wrote = true;
186
+ }
187
+ } else if (!sameValue(data.get(key), value)) {
188
+ data.set(key, value);
189
+ wrote = true;
190
+ }
191
+ }
192
+ synced.set(node.getKey(), props);
193
+ if (wrote) readBaseline.set(treeNode.id, stable(storedProps(treeNode)));
194
+ if ($isTextNode(node)) {
195
+ const text = data.getOrCreateContainer(TEXT_KEY, new LoroText());
196
+ const next = node.getTextContent();
197
+ if (text.toString() !== next) text.update(next);
198
+ }
199
+ };
200
+ const writeChildren = (parent, parentId, visited) => {
201
+ parent.getChildren().forEach((child, index) => {
202
+ let id = keyToId.get(child.getKey());
203
+ if (alive(id)) {
204
+ const current = tree.getNodeByID(id);
205
+ if (current.parent()?.id !== parentId || current.index() !== index) tree.move(id, parentId, index);
206
+ } else {
207
+ id = tree.createNode(parentId, index).id;
208
+ link(child.getKey(), id);
209
+ }
210
+ visited.add(id);
211
+ writeNode(child, tree.getNodeByID(id));
212
+ if ($isElementNode(child)) writeChildren(child, id, visited);
213
+ });
214
+ };
215
+ const pushToDoc = () => {
216
+ editor.getEditorState().read(() => {
217
+ let rootId = keyToId.get("root");
218
+ if (!alive(rootId)) {
219
+ rootId = tree.roots()[0]?.id ?? tree.createNode().id;
220
+ link("root", rootId);
221
+ }
222
+ const visited = /* @__PURE__ */ new Set([rootId]);
223
+ writeNode($getRoot(), tree.getNodeByID(rootId));
224
+ writeChildren($getRoot(), rootId, visited);
225
+ for (const treeNode of tree.nodes()) if (!visited.has(treeNode.id) && alive(treeNode.id)) {
226
+ tree.delete(treeNode.id);
227
+ const key = idToKey.get(treeNode.id);
228
+ if (key !== void 0) keyToId.delete(key);
229
+ idToKey.delete(treeNode.id);
230
+ }
231
+ });
232
+ doc.commit({ message: options.commitMessage });
233
+ };
234
+ const applyText = (node, next) => {
235
+ const previous = node.getTextContent();
236
+ if (previous === next) return;
237
+ const selection = $getSelection();
238
+ const points = $isRangeSelection(selection) ? [selection.anchor, selection.focus].filter((point) => point.type === "text" && point.key === node.getKey()) : [];
239
+ const offsets = points.map((point) => transformOffset(previous, next, point.offset));
240
+ node.setTextContent(next);
241
+ points.forEach((point, index) => point.set(node.getKey(), offsets[index], "text"));
242
+ };
243
+ const readNode = (treeNode) => {
244
+ const node = readNodeUnsynced(treeNode);
245
+ synced.set(node.getKey(), nodeProps(node));
246
+ return node;
247
+ };
248
+ const readNodeUnsynced = (treeNode) => {
249
+ const props = storedProps(treeNode);
250
+ const text = $isTextLike(props) ? String(treeNode.data.get(TEXT_KEY) ?? "") : void 0;
251
+ const key = idToKey.get(treeNode.id);
252
+ const existing = key === void 0 ? null : $getNodeByKey(key);
253
+ if (existing && existing.getType() === props.type) {
254
+ if ($isTextNode(existing)) {
255
+ if (!sameValue(nodeProps(existing), props)) existing.updateFromJSON({
256
+ ...props,
257
+ text: existing.getTextContent()
258
+ });
259
+ applyText(existing, text ?? "");
260
+ return existing;
261
+ }
262
+ const storedKey = stable(props);
263
+ if (readBaseline.get(treeNode.id) === storedKey || sameValue(nodeProps(existing), props)) {
264
+ readBaseline.set(treeNode.id, storedKey);
265
+ return existing;
266
+ }
267
+ if ($updateInPlace(existing, props)) {
268
+ readBaseline.set(treeNode.id, storedKey);
269
+ return existing;
270
+ }
271
+ readBaseline.set(treeNode.id, storedKey);
272
+ const replacement = $parseSerializedNode({
273
+ ...props,
274
+ children: []
275
+ });
276
+ existing.replace(replacement, $isElementNode(existing) && $isElementNode(replacement));
277
+ link(replacement.getKey(), treeNode.id);
278
+ return replacement;
279
+ }
280
+ const created = $parseSerializedNode({
281
+ ...props,
282
+ ...text === void 0 ? {} : { text },
283
+ children: []
284
+ });
285
+ readBaseline.set(treeNode.id, stable(props));
286
+ link(created.getKey(), treeNode.id);
287
+ return created;
288
+ };
289
+ const readChildren = (parent, treeNode, visited) => {
290
+ (treeNode.children() ?? []).forEach((child, index) => {
291
+ const node = readNode(child);
292
+ visited.add(node.getKey());
293
+ const current = parent.getChildAtIndex(index);
294
+ if (!current || !current.is(node)) {
295
+ if (index === 0) {
296
+ const first = parent.getFirstChild();
297
+ if (first) first.insertBefore(node);
298
+ else parent.append(node);
299
+ } else parent.getChildAtIndex(index - 1).insertAfter(node);
300
+ }
301
+ if ($isElementNode(node)) readChildren(node, child, visited);
302
+ });
303
+ };
304
+ const $collectDescendants = (node, out) => {
305
+ for (const child of node.getChildren()) {
306
+ out.push(child);
307
+ if ($isElementNode(child)) $collectDescendants(child, out);
308
+ }
309
+ };
310
+ const pullFromDoc = () => {
311
+ editor.update(() => {
312
+ const rootTreeNode = tree.roots()[0];
313
+ if (!rootTreeNode) return;
314
+ link("root", rootTreeNode.id);
315
+ const rootProps = storedProps(rootTreeNode);
316
+ if (rootProps.type === "root" && !sameValue(nodeProps($getRoot()), rootProps)) $getRoot().updateFromJSON(rootProps);
317
+ synced.set("root", nodeProps($getRoot()));
318
+ const visited = /* @__PURE__ */ new Set(["root"]);
319
+ readChildren($getRoot(), rootTreeNode, visited);
320
+ const all = [];
321
+ $collectDescendants($getRoot(), all);
322
+ for (const node of all) if (!visited.has(node.getKey()) && node.isAttached()) node.remove();
323
+ }, {
324
+ discrete: true,
325
+ tag: COLLAB_TAG
326
+ });
327
+ };
328
+ if (tree.roots().length > 0) pullFromDoc();
329
+ else pushToDoc();
330
+ return {
331
+ dispose: editor.registerUpdateListener(({ dirtyElements, dirtyLeaves }) => {
332
+ if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;
333
+ pushToDoc();
334
+ }),
335
+ idOf: (key) => keyToId.get(key),
336
+ keyOf: (id) => idToKey.get(id),
337
+ import: (bytes) => {
338
+ doc.import(bytes);
339
+ pullFromDoc();
340
+ }
341
+ };
342
+ }
343
+ function $isTextLike(props) {
344
+ return typeof props.format === "number" && typeof props.detail === "number" && "mode" in props;
345
+ }
346
+ //#endregion
347
+ //#region src/stream.ts
348
+ var TYPING_FRAME_BUDGET = 120;
349
+ var MAX_FRAMES_PER_BLOCK = 24;
350
+ var isFlat = (block) => !block || (block.children ?? []).every((child) => child.type === "text");
351
+ var textOf = (block) => (block?.children ?? []).map((child) => child.text ?? "").join("");
352
+ function sliceRuns(runs, start, end) {
353
+ const out = [];
354
+ let offset = 0;
355
+ for (const run of runs) {
356
+ const text = run.text ?? "";
357
+ const from = Math.max(start, offset);
358
+ const to = Math.min(end, offset + text.length);
359
+ if (to > from) out.push({
360
+ ...run,
361
+ text: text.slice(from - offset, to - offset)
362
+ });
363
+ offset += text.length;
364
+ }
365
+ return out;
366
+ }
367
+ function typingSpan(item) {
368
+ const before = textOf(item.old);
369
+ const after = textOf(item.next);
370
+ const limit = Math.min(before.length, after.length);
371
+ let prefix = 0;
372
+ while (prefix < limit && before[prefix] === after[prefix]) prefix++;
373
+ let suffix = 0;
374
+ while (suffix < limit - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++;
375
+ return {
376
+ after,
377
+ prefix,
378
+ suffix,
379
+ middle: after.length - prefix - suffix
380
+ };
381
+ }
382
+ function pairGap(dels, adds) {
383
+ const pending = [...dels];
384
+ return adds.map((next) => {
385
+ const blockId = next.$?.blockId;
386
+ let slot = blockId === void 0 ? -1 : pending.findIndex((old) => old.$?.blockId === blockId);
387
+ if (slot === -1) slot = pending.findIndex((old) => pairable(old, next));
388
+ return {
389
+ next,
390
+ old: slot === -1 ? null : pending.splice(slot, 1)[0]
391
+ };
392
+ });
393
+ }
394
+ function planFrames(base, target) {
395
+ const before = base.children ?? [];
396
+ const after = target.children ?? [];
397
+ const gaps = [];
398
+ let ci = 0;
399
+ let tj = 0;
400
+ for (const [i, j] of [...lcs(before.map(stable), after.map(stable)), [before.length, after.length]]) {
401
+ gaps.push({
402
+ dels: before.slice(ci, i),
403
+ items: pairGap(before.slice(ci, i), after.slice(tj, j)),
404
+ matched: after[j] ?? null
405
+ });
406
+ ci = i + 1;
407
+ tj = j + 1;
408
+ }
409
+ const typingItems = gaps.flatMap((gap) => gap.items).filter((item) => isFlat(item.old) && isFlat(item.next) && typingSpan(item).middle > 0);
410
+ const perBlock = Math.min(MAX_FRAMES_PER_BLOCK, Math.max(1, Math.floor(TYPING_FRAME_BUDGET / Math.max(1, typingItems.length))));
411
+ const regions = gaps.map((gap) => [...gap.dels]);
412
+ const frames = [];
413
+ const render = () => ({
414
+ ...target,
415
+ children: gaps.flatMap((gap, g) => [...regions[g], ...gap.matched ? [gap.matched] : []])
416
+ });
417
+ const regionStart = (g) => gaps.slice(0, g).reduce((sum, gap, k) => sum + regions[k].length + (gap.matched ? 1 : 0), 0);
418
+ const push = (focus) => frames.push({
419
+ focus,
420
+ root: render()
421
+ });
422
+ gaps.forEach((gap, g) => {
423
+ const kept = gap.items.flatMap((item) => item.old ? [item.old] : []);
424
+ if (stable(kept) !== stable(regions[g])) {
425
+ regions[g] = kept;
426
+ push(null);
427
+ }
428
+ gap.items.forEach((item, k) => {
429
+ const region = regions[g];
430
+ const place = (block, offset, first) => {
431
+ region.splice(k, first && !item.old ? 0 : 1, block);
432
+ push({
433
+ offset,
434
+ path: [regionStart(g) + k]
435
+ });
436
+ };
437
+ const span = typingSpan(item);
438
+ if (!isFlat(item.old) || !isFlat(item.next) || span.middle === 0) {
439
+ place(item.next, item.old ? span.prefix : 0, true);
440
+ return;
441
+ }
442
+ const runs = item.next.children ?? [];
443
+ const count = Math.min(perBlock, span.middle);
444
+ const chunk = Math.ceil(span.middle / count);
445
+ let first = true;
446
+ for (let revealed = chunk; revealed < span.middle; revealed += chunk) {
447
+ const cut = span.prefix + revealed;
448
+ place({
449
+ ...item.next,
450
+ children: [...sliceRuns(runs, 0, cut), ...sliceRuns(runs, span.after.length - span.suffix, span.after.length)]
451
+ }, cut, first);
452
+ first = false;
453
+ }
454
+ place(item.next, span.prefix + span.middle, first);
455
+ });
456
+ });
457
+ return frames;
458
+ }
459
+ function $locate(focus) {
460
+ if (!focus) return null;
461
+ const block = $getRoot().getChildAtIndex(focus.path[0]);
462
+ if (!block) return null;
463
+ if (!$isElementNode(block)) return {
464
+ key: block.getKey(),
465
+ offset: 0
466
+ };
467
+ let remaining = focus.offset;
468
+ const texts = block.getChildren().filter($isTextNode);
469
+ for (const text of texts) {
470
+ const size = text.getTextContentSize();
471
+ if (remaining <= size) return {
472
+ key: text.getKey(),
473
+ offset: remaining
474
+ };
475
+ remaining -= size;
476
+ }
477
+ const last = texts.at(-1);
478
+ return last ? {
479
+ key: last.getKey(),
480
+ offset: last.getTextContentSize()
481
+ } : {
482
+ key: block.getKey(),
483
+ offset: 0
484
+ };
485
+ }
486
+ function streamAtVersion(doc, frontiers, createEditor, base, target, commitMessage) {
487
+ const fork = doc.forkAt(frontiers);
488
+ const editor = createEditor();
489
+ const binding = createLoroBinding(editor, fork, { commitMessage });
490
+ const steps = [];
491
+ let from = fork.oplogVersion();
492
+ for (const frame of planFrames(base.root, target.root)) {
493
+ editor.update(() => $reconcileRoot(frame.root), { discrete: true });
494
+ const at = editor.getEditorState().read(() => $locate(frame.focus));
495
+ const id = at ? binding.idOf(at.key) : void 0;
496
+ const update = fork.export({
497
+ from,
498
+ mode: "update"
499
+ });
500
+ from = fork.oplogVersion();
501
+ steps.push({
502
+ cursor: id && at ? {
503
+ id,
504
+ offset: at.offset
505
+ } : null,
506
+ update
507
+ });
508
+ }
509
+ binding.dispose();
510
+ return {
511
+ frontiers: fork.frontiers(),
512
+ steps
513
+ };
514
+ }
515
+ //#endregion
516
+ export { $reconcileRoot, COLLAB_TAG, TREE_NAME, createLoroBinding, editAtVersion, planFrames, streamAtVersion };
@@ -0,0 +1,2 @@
1
+ export declare function transformOffset(previous: string, next: string, offset: number): number;
2
+ //# sourceMappingURL=offset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offset.d.ts","sourceRoot":"","sources":["../src/offset.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CActF"}
@@ -0,0 +1,16 @@
1
+ import { LexicalEditor, LexicalNode, SerializedEditorState, SerializedLexicalNode } from 'lexical';
2
+ import { LoroDoc } from 'loro-crdt';
3
+ export type Json = SerializedLexicalNode & {
4
+ $?: {
5
+ blockId?: string;
6
+ };
7
+ children?: Json[];
8
+ text?: string;
9
+ };
10
+ export type Frontiers = ReturnType<LoroDoc['frontiers']>;
11
+ export declare function lcs(a: string[], b: string[]): Array<[number, number]>;
12
+ export declare function $updateInPlace(node: LexicalNode, target: Json): boolean;
13
+ export declare function pairable(current: Json, target: Json): boolean;
14
+ export declare function $reconcileRoot(target: SerializedEditorState['root']): void;
15
+ export declare function editAtVersion(doc: LoroDoc, frontiers: Frontiers, createEditor: () => LexicalEditor, target: SerializedEditorState, commitMessage?: string): Uint8Array;
16
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKzC,MAAM,MAAM,IAAI,GAAG,qBAAqB,GAAG;IACzC,CAAC,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;AAqBzD,wBAAgB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiBrE;AAID,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,GAAG,OAAO,CAIvE;AA2BD,wBAAgB,QAAQ,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,OAAO,CAK7D;AAiDD,wBAAgB,cAAc,CAAC,MAAM,EAAE,qBAAqB,CAAC,MAAM,CAAC,GAAG,IAAI,CAQ1E;AAED,wBAAgB,aAAa,CAC3B,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,MAAM,aAAa,EACjC,MAAM,EAAE,qBAAqB,EAC7B,aAAa,CAAC,EAAE,MAAM,GACrB,UAAU,CAQZ"}
@@ -0,0 +1,2 @@
1
+ export declare function stable(value: unknown): string;
2
+ //# sourceMappingURL=stable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stable.d.ts","sourceRoot":"","sources":["../src/stable.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAU7C"}
@@ -0,0 +1,24 @@
1
+ import { LexicalEditor, SerializedEditorState } from 'lexical';
2
+ import { LoroDoc, TreeID } from 'loro-crdt';
3
+ import { Frontiers, Json } from './reconcile';
4
+ export interface FrameFocus {
5
+ offset: number;
6
+ path: number[];
7
+ }
8
+ export interface Frame {
9
+ focus: FrameFocus | null;
10
+ root: Json;
11
+ }
12
+ export interface StreamStep {
13
+ cursor: {
14
+ id: TreeID;
15
+ offset: number;
16
+ } | null;
17
+ update: Uint8Array;
18
+ }
19
+ export declare function planFrames(base: Json, target: Json): Frame[];
20
+ export declare function streamAtVersion(doc: LoroDoc, frontiers: Frontiers, createEditor: () => LexicalEditor, base: SerializedEditorState, target: SerializedEditorState, commitMessage?: string): {
21
+ frontiers: Frontiers;
22
+ steps: StreamStep[];
23
+ };
24
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAElB,KAAK,qBAAqB,EAC3B,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGjD,OAAO,EAAkB,KAAK,SAAS,EAAE,KAAK,IAAI,EAAiB,MAAM,aAAa,CAAC;AAGvF,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9C,MAAM,EAAE,UAAU,CAAC;CACpB;AA6DD,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,KAAK,EAAE,CA6E5D;AAoBD,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,MAAM,aAAa,EACjC,IAAI,EAAE,qBAAqB,EAC3B,MAAM,EAAE,qBAAqB,EAC7B,aAAa,CAAC,EAAE,MAAM,GACrB;IAAE,SAAS,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAA;CAAE,CAgB/C"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@haklex/rich-collab-loro",
3
+ "version": "0.44.0",
4
+ "description": "Loro CRDT binding for Lexical — bidirectional LoroTree sync for real-time collaboration and history",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Innei/haklex.git",
8
+ "directory": "packages/rich-collab-loro"
9
+ },
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.mjs",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
18
+ "main": "./dist/index.mjs",
19
+ "files": [
20
+ "dist",
21
+ "src"
22
+ ],
23
+ "devDependencies": {
24
+ "@lexical/headless": "^0.49.0",
25
+ "lexical": "^0.49.0",
26
+ "loro-crdt": "^1.16.3",
27
+ "typescript": "^7.0.2",
28
+ "unplugin-dts": "^1.0.3",
29
+ "vite": "^8.2.2",
30
+ "@haklex/rich-headless": "0.44.0",
31
+ "@haklex/rich-litexml": "0.44.0"
32
+ },
33
+ "peerDependencies": {
34
+ "lexical": "^0.49.0",
35
+ "loro-crdt": "^1.16.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "build": "vite build",
42
+ "dev:build": "vite build --watch"
43
+ },
44
+ "types": "./dist/index.d.ts"
45
+ }