@codemerge/kernel 2.4.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1968 @@
1
+ //#region src/document.ts
2
+ var idCounter = 0;
3
+ function resetIdCounter(n = 0) {
4
+ idCounter = n;
5
+ }
6
+ function nextId(prefix = "n") {
7
+ idCounter += 1;
8
+ return `${prefix}${idCounter}`;
9
+ }
10
+ function createText(text, marks = []) {
11
+ return {
12
+ marks: marks.length > 0 ? marks.map((m) => ({ ...m })) : void 0,
13
+ text,
14
+ type: "text"
15
+ };
16
+ }
17
+ function createParagraph(content = [createText("")], attrs) {
18
+ return {
19
+ attrs: copyAttrs(attrs),
20
+ content: [...content],
21
+ id: nextId("p"),
22
+ type: "paragraph"
23
+ };
24
+ }
25
+ function createDoc(content) {
26
+ return {
27
+ content: content !== void 0 && content.length > 0 ? [...content] : [createParagraph()],
28
+ id: nextId("doc"),
29
+ type: "doc"
30
+ };
31
+ }
32
+ function isAttrsRecord(value) {
33
+ return value !== null && typeof value === "object" && !Array.isArray(value);
34
+ }
35
+ /** Null-prototype attrs bag (no __proto__ pollution). */
36
+ function emptyAttrsRecord() {
37
+ const raw = Object.create(null);
38
+ if (!isAttrsRecord(raw)) return {};
39
+ return raw;
40
+ }
41
+ /** Copy own enumerable attrs onto a null-prototype bag (no __proto__ pollution). */
42
+ function copyAttrs(src) {
43
+ if (!src) return;
44
+ const out = emptyAttrsRecord();
45
+ for (const key of Object.keys(src)) out[key] = src[key];
46
+ return Object.keys(out).length > 0 ? out : void 0;
47
+ }
48
+ /** Shallow clone node; children array is new, child refs shared (structural sharing). */
49
+ function cloneNode(node) {
50
+ return {
51
+ ...node,
52
+ attrs: copyAttrs(node.attrs),
53
+ content: node.content ? [...node.content] : void 0,
54
+ marks: node.marks ? node.marks.map((m) => ({
55
+ ...m,
56
+ attrs: copyAttrs(m.attrs)
57
+ })) : void 0,
58
+ text: node.text
59
+ };
60
+ }
61
+ /** Deep clone node tree (content recursively cloned). */
62
+ function deepCloneNode(node) {
63
+ const cloned = cloneNode(node);
64
+ if (cloned.content) cloned.content = cloned.content.map(deepCloneNode);
65
+ return cloned;
66
+ }
67
+ /** Assign stable ids to every non-text node missing `id`. */
68
+ function ensureNodeIds(node) {
69
+ const out = {
70
+ ...node,
71
+ content: node.content ? node.content.map(ensureNodeIds) : void 0,
72
+ marks: node.marks ? node.marks.map((m) => ({ ...m })) : void 0
73
+ };
74
+ if (out.type !== "text" && !out.id) out.id = nextId(out.type === "doc" ? "doc" : out.type === "paragraph" ? "p" : out.type === "listItem" ? "li" : "n");
75
+ return out;
76
+ }
77
+ function deepClone(value) {
78
+ return structuredClone(value);
79
+ }
80
+ /** Document JSON format version — bump when the on-disk shape changes. */
81
+ var JSON_DOC_VERSION = 1;
82
+ function docToJSON(doc) {
83
+ return {
84
+ doc: deepClone(doc),
85
+ version: 1
86
+ };
87
+ }
88
+ function isJSONDoc(value) {
89
+ if (!("version" in value) || !("doc" in value)) return false;
90
+ const doc = Reflect.get(value, "doc");
91
+ return typeof doc === "object" && doc !== null;
92
+ }
93
+ function isDocNode(value) {
94
+ return typeof Reflect.get(value, "type") === "string";
95
+ }
96
+ function docFromJSON(json) {
97
+ if (isJSONDoc(json)) {
98
+ if (json.version !== 1) throw new Error(`Unsupported JSONDoc version: ${json.version}`);
99
+ if (json.doc.type !== "doc") throw new Error("Expected root type \"doc\"");
100
+ assertTreeDepth(json.doc);
101
+ return ensureNodeIds(deepClone(json.doc));
102
+ }
103
+ if (isDocNode(json) && json.type === "doc") {
104
+ assertTreeDepth(json);
105
+ return ensureNodeIds(deepClone(json));
106
+ }
107
+ throw new Error("Expected root type \"doc\"");
108
+ }
109
+ function assertTreeDepth(node, max = 64) {
110
+ const stack = [{
111
+ n: node,
112
+ d: 0
113
+ }];
114
+ while (stack.length > 0) {
115
+ const { n, d } = stack.pop();
116
+ if (d > max) throw new Error(`Tree exceeds MAX_TREE_DEPTH (${max})`);
117
+ for (const child of n.content ?? []) stack.push({
118
+ n: child,
119
+ d: d + 1
120
+ });
121
+ }
122
+ }
123
+ function getNodeAt(doc, path) {
124
+ let cur = doc;
125
+ for (const index of path) {
126
+ if (!cur.content || index < 0 || index >= cur.content.length) throw new Error(`Invalid path: ${path.join(".")}`);
127
+ cur = cur.content[index];
128
+ }
129
+ return cur;
130
+ }
131
+ function replaceAt(doc, path, next) {
132
+ if (path.length === 0) return next;
133
+ const [head, ...rest] = path, cloned = cloneNode(doc);
134
+ if (!cloned.content) throw new Error("Cannot replace in leaf");
135
+ cloned.content[head] = replaceAt(cloned.content[head], rest, next);
136
+ return cloned;
137
+ }
138
+ function textLength(node) {
139
+ if (node.type === "text") return node.text?.length ?? 0;
140
+ if (!node.content) return 0;
141
+ return node.content.reduce((sum, child) => sum + textLength(child), 0);
142
+ }
143
+ //#endregion
144
+ //#region src/selection.ts
145
+ function createSelection(anchor, focus = anchor) {
146
+ return {
147
+ anchor: {
148
+ ...anchor,
149
+ path: [...anchor.path]
150
+ },
151
+ focus: {
152
+ ...focus,
153
+ path: [...focus.path]
154
+ }
155
+ };
156
+ }
157
+ function collapsedAt(path, offset) {
158
+ return createSelection({
159
+ offset,
160
+ path: [...path]
161
+ });
162
+ }
163
+ function isCollapsed(sel) {
164
+ return sel.anchor.offset === sel.focus.offset && sel.anchor.path.length === sel.focus.path.length && sel.anchor.path.every((v, i) => v === sel.focus.path[i]);
165
+ }
166
+ /** Resolve a point inside a paragraph path to ensure offset is within text bounds. */
167
+ function clampPoint(doc, point) {
168
+ try {
169
+ let path = [...point.path];
170
+ let node = getNodeAt(doc, path);
171
+ while (node.type === "text" && path.length > 0) {
172
+ path = path.slice(0, -1);
173
+ node = path.length === 0 ? doc : getNodeAt(doc, path);
174
+ }
175
+ if (path.length === 0 || node.type === "doc") return {
176
+ offset: 0,
177
+ path: [0]
178
+ };
179
+ const max = textLength(node);
180
+ return {
181
+ offset: Math.max(0, Math.min(point.offset, max)),
182
+ path
183
+ };
184
+ } catch {
185
+ return {
186
+ offset: 0,
187
+ path: [0]
188
+ };
189
+ }
190
+ }
191
+ function clampSelection(doc, sel) {
192
+ return {
193
+ anchor: clampPoint(doc, sel.anchor),
194
+ focus: clampPoint(doc, sel.focus)
195
+ };
196
+ }
197
+ /** Compare points: -1 if a < b, 0 equal, 1 if a > b (path then offset). */
198
+ function comparePoints(a, b) {
199
+ const len = Math.max(a.path.length, b.path.length);
200
+ for (let i = 0; i < len; i++) {
201
+ const av = a.path[i] ?? -1, bv = b.path[i] ?? -1;
202
+ if (av !== bv) return av < bv ? -1 : 1;
203
+ }
204
+ if (a.offset !== b.offset) return a.offset < b.offset ? -1 : 1;
205
+ return 0;
206
+ }
207
+ function orderedRange(sel) {
208
+ return comparePoints(sel.anchor, sel.focus) <= 0 ? {
209
+ from: sel.anchor,
210
+ to: sel.focus
211
+ } : {
212
+ from: sel.focus,
213
+ to: sel.anchor
214
+ };
215
+ }
216
+ //#endregion
217
+ //#region src/schema.ts
218
+ function createSchema() {
219
+ const schema = {
220
+ marks: /* @__PURE__ */ new Map(),
221
+ nodes: /* @__PURE__ */ new Map(),
222
+ sealed: false
223
+ };
224
+ registerNode(schema, {
225
+ group: "block",
226
+ name: "doc"
227
+ });
228
+ registerNode(schema, {
229
+ group: "block",
230
+ name: "paragraph"
231
+ });
232
+ registerNode(schema, {
233
+ group: "inline",
234
+ name: "text"
235
+ });
236
+ registerNode(schema, {
237
+ atom: true,
238
+ group: "inline",
239
+ name: "hardBreak"
240
+ });
241
+ registerMark(schema, { name: "bold" });
242
+ registerMark(schema, { name: "italic" });
243
+ registerMark(schema, { name: "underline" });
244
+ registerMark(schema, { name: "strike" });
245
+ return schema;
246
+ }
247
+ function registerNode(schema, spec) {
248
+ if (schema.sealed) throw new Error("Schema is sealed");
249
+ if (schema.nodes.has(spec.name)) throw new Error(`Node already registered: ${spec.name}`);
250
+ schema.nodes.set(spec.name, spec);
251
+ }
252
+ function registerMark(schema, spec) {
253
+ if (schema.sealed) throw new Error("Schema is sealed");
254
+ if (schema.marks.has(spec.name)) throw new Error(`Mark already registered: ${spec.name}`);
255
+ schema.marks.set(spec.name, spec);
256
+ }
257
+ function sealSchema(schema) {
258
+ schema.sealed = true;
259
+ }
260
+ function assertMark(schema, mark) {
261
+ if (!schema.marks.has(mark.type)) throw new Error(`Unknown mark: ${mark.type}`);
262
+ }
263
+ function assertNodeType(schema, type) {
264
+ if (!schema.nodes.has(type)) throw new Error(`Unknown node type: ${type}`);
265
+ }
266
+ //#endregion
267
+ //#region src/operations.ts
268
+ var BLOCK_CHILD_TYPES = /* @__PURE__ */ new Set([
269
+ "paragraph",
270
+ "heading",
271
+ "listItem",
272
+ "blockquote",
273
+ "codeBlock",
274
+ "bulletList",
275
+ "orderedList",
276
+ "table",
277
+ "tableRow",
278
+ "tableCell"
279
+ ]);
280
+ function assertIntegerPath(path) {
281
+ for (const seg of path) if (!Number.isInteger(seg)) throw new TypeError(`Path segments must be integers: ${path.join(".")}`);
282
+ }
283
+ function assertLeafTextBlock(node) {
284
+ if (node.type === "text") throw new Error("Text ops require a leaf text block");
285
+ if ((node.content ?? []).some((c) => BLOCK_CHILD_TYPES.has(c.type))) throw new Error("Text ops require a leaf text block");
286
+ }
287
+ function flattenText(paragraph) {
288
+ const runs = [];
289
+ const keeps = [];
290
+ let text = "";
291
+ for (const child of paragraph.content ?? []) if (child.type === "text") {
292
+ const start = text.length;
293
+ const chunk = child.text ?? "";
294
+ text += chunk;
295
+ runs.push({
296
+ start,
297
+ end: text.length,
298
+ node: child
299
+ });
300
+ } else keeps.push({
301
+ at: text.length,
302
+ node: deepCloneNode(child)
303
+ });
304
+ return {
305
+ text,
306
+ runs,
307
+ keeps
308
+ };
309
+ }
310
+ function rebuildParagraph(paragraph, text, markAt, keeps = []) {
311
+ const keepsSorted = [...keeps].toSorted((a, b) => a.at - b.at);
312
+ if (text.length === 0) {
313
+ if (keepsSorted.length === 0) return {
314
+ ...cloneNode(paragraph),
315
+ content: [createText("")]
316
+ };
317
+ return {
318
+ ...cloneNode(paragraph),
319
+ content: keepsSorted.map((k) => deepCloneNode(k.node))
320
+ };
321
+ }
322
+ const content = [];
323
+ let keepIdx = 0;
324
+ let i = 0;
325
+ while (keepIdx < keepsSorted.length && keepsSorted[keepIdx].at <= 0) {
326
+ content.push(deepCloneNode(keepsSorted[keepIdx].node));
327
+ keepIdx++;
328
+ }
329
+ while (i < text.length) {
330
+ const marks = markAt(i);
331
+ let j = i + 1;
332
+ const nextKeepAt = keepsSorted[keepIdx]?.at ?? text.length + 1;
333
+ while (j < text.length && j < nextKeepAt && sameMarks(markAt(j), marks)) j++;
334
+ const end = Math.min(j, nextKeepAt, text.length);
335
+ if (end > i) {
336
+ content.push(createText(text.slice(i, end), marks));
337
+ i = end;
338
+ }
339
+ while (keepIdx < keepsSorted.length && keepsSorted[keepIdx].at === i) {
340
+ content.push(deepCloneNode(keepsSorted[keepIdx].node));
341
+ keepIdx++;
342
+ }
343
+ }
344
+ while (keepIdx < keepsSorted.length && keepsSorted[keepIdx].at >= text.length) {
345
+ content.push(deepCloneNode(keepsSorted[keepIdx].node));
346
+ keepIdx++;
347
+ }
348
+ if (content.length === 0) content.push(createText(""));
349
+ return {
350
+ ...cloneNode(paragraph),
351
+ content
352
+ };
353
+ }
354
+ function shiftKeeps(keeps, offset, delta) {
355
+ return keeps.map((k) => k.at >= offset ? {
356
+ ...k,
357
+ at: k.at + delta
358
+ } : k);
359
+ }
360
+ function deleteKeepsInRange(keeps, offset, length) {
361
+ const end = offset + length;
362
+ return keeps.filter((k) => k.at <= offset || k.at >= end).map((k) => k.at >= end ? {
363
+ ...k,
364
+ at: k.at - length
365
+ } : k);
366
+ }
367
+ function captureKeepsInRange(keeps, from, to) {
368
+ return keeps.filter((k) => k.at > from && k.at < to).map((k) => ({
369
+ at: k.at - from,
370
+ node: deepCloneNode(k.node)
371
+ }));
372
+ }
373
+ function mergeSliceKeeps(base, from, sliceKeeps) {
374
+ if (sliceKeeps === void 0 || sliceKeeps.length === 0) return base;
375
+ return [...base, ...sliceKeeps.map((k) => ({
376
+ at: from + k.at,
377
+ node: deepCloneNode(k.node)
378
+ }))];
379
+ }
380
+ function assertOpSchema(schema, op) {
381
+ if (!schema) return;
382
+ if (op.type === "set_mark") assertMark(schema, op.mark);
383
+ else if (op.type === "insert_text") {
384
+ if (op.marks) for (const m of op.marks) assertMark(schema, m);
385
+ if (op.marksByOffset) for (const ms of op.marksByOffset) for (const m of ms) assertMark(schema, m);
386
+ } else if (op.type === "replace_slice") {
387
+ for (const run of op.runs) for (const m of run.marks) assertMark(schema, m);
388
+ for (const k of op.keeps ?? []) {
389
+ assertMaxTreeDepth(k.node);
390
+ assertNodeTree(schema, k.node);
391
+ }
392
+ } else if (op.type === "split_paragraph") {
393
+ if (op.rightNode) {
394
+ assertMaxTreeDepth(op.rightNode);
395
+ assertNodeTree(schema, op.rightNode);
396
+ }
397
+ } else if (op.type === "insert_node") assertNodeTree(schema, op.node);
398
+ }
399
+ function assertNodeTree(schema, node, depthChecked = false) {
400
+ if (!depthChecked) assertMaxTreeDepth(node);
401
+ assertNodeType(schema, node.type);
402
+ if (node.marks) for (const m of node.marks) assertMark(schema, m);
403
+ for (const child of node.content ?? []) assertNodeTree(schema, child, true);
404
+ }
405
+ function sameMarks(a, b) {
406
+ if (a.length !== b.length) return false;
407
+ return a.every((m, i) => m.type === b[i].type && JSON.stringify(m.attrs ?? {}) === JSON.stringify(b[i].attrs ?? {}));
408
+ }
409
+ function cloneMarks(marks) {
410
+ return marks.map((m) => ({
411
+ ...m,
412
+ attrs: m.attrs ? { ...m.attrs } : void 0
413
+ }));
414
+ }
415
+ function marksAt(runs, index) {
416
+ for (const run of runs) if (index >= run.start && index < run.end) return run.node.marks ? cloneMarks(run.node.marks) : [];
417
+ const lastRun = runs.at(-1);
418
+ if (lastRun && index === lastRun.end) return lastRun.node.marks ? cloneMarks(lastRun.node.marks) : [];
419
+ return [];
420
+ }
421
+ function captureRuns(text, runs, from, to) {
422
+ const result = [];
423
+ let i = from;
424
+ while (i < to) {
425
+ const marks = marksAt(runs, i);
426
+ let j = i + 1;
427
+ while (j < to && sameMarks(marksAt(runs, j), marks)) j++;
428
+ result.push({
429
+ text: text.slice(i, j),
430
+ marks
431
+ });
432
+ i = j;
433
+ }
434
+ return result;
435
+ }
436
+ /** True when every offset in `[from, to)` carries `markType`. Empty range → false. */
437
+ function rangeHasMark(paragraph, from, to, markType) {
438
+ const { text, runs } = flattenText(paragraph);
439
+ const start = Math.max(0, Math.min(from, text.length));
440
+ const end = Math.max(start, Math.min(to, text.length));
441
+ if (start >= end) return false;
442
+ for (let i = start; i < end; i++) if (!marksAt(runs, i).some((m) => m.type === markType)) return false;
443
+ return true;
444
+ }
445
+ /** Mark presence at a single offset (caret / left of caret). */
446
+ function offsetHasMark(paragraph, offset, markType) {
447
+ const { text, runs } = flattenText(paragraph);
448
+ if (text.length === 0) return false;
449
+ return marksAt(runs, Math.max(0, Math.min(offset, text.length - 1))).some((m) => m.type === markType);
450
+ }
451
+ /** Merge marks by type — `extra` wins on conflict. */
452
+ function mergeMarksByType(base, extra) {
453
+ const map = /* @__PURE__ */ new Map();
454
+ for (const m of base) map.set(m.type, {
455
+ ...m,
456
+ attrs: m.attrs ? { ...m.attrs } : void 0
457
+ });
458
+ for (const m of extra) map.set(m.type, {
459
+ ...m,
460
+ attrs: m.attrs ? { ...m.attrs } : void 0
461
+ });
462
+ return [...map.values()];
463
+ }
464
+ /** Hard cap on a single insert_text / replace_slice payload (paste / programmatic flood). */
465
+ var MAX_INSERT_CHARS = 1e5;
466
+ /** Cap on keep nodes injected via replace_slice (hardBreak floods). */
467
+ var MAX_SLICE_KEEPS = 1e4;
468
+ /** Max nesting depth for op/JSON trees (empty structural bombs). */
469
+ var MAX_TREE_DEPTH = 64;
470
+ /** Iterative depth check — fail closed before recursive clone/walk. */
471
+ function assertMaxTreeDepth(node, max = 64) {
472
+ const stack = [{
473
+ n: node,
474
+ d: 0
475
+ }];
476
+ while (stack.length > 0) {
477
+ const { n, d } = stack.pop();
478
+ if (d > max) throw new Error(`Tree exceeds MAX_TREE_DEPTH (${max})`);
479
+ for (const child of n.content ?? []) stack.push({
480
+ n: child,
481
+ d: d + 1
482
+ });
483
+ }
484
+ }
485
+ function applyOp(doc, op, schema) {
486
+ if ("path" in op) assertIntegerPath(op.path);
487
+ assertOpSchema(schema, op);
488
+ switch (op.type) {
489
+ case "insert_text": {
490
+ if (op.text.length > 1e5) throw new Error(`insert_text exceeds MAX_INSERT_CHARS (${MAX_INSERT_CHARS})`);
491
+ const para = getNodeAt(doc, op.path);
492
+ assertLeafTextBlock(para);
493
+ const { text, runs, keeps } = flattenText(para);
494
+ const offset = Math.max(0, Math.min(op.offset, text.length));
495
+ const nextPara = rebuildParagraph(para, text.slice(0, offset) + op.text + text.slice(offset), (i) => {
496
+ if (i >= offset && i < offset + op.text.length) {
497
+ if (op.marksByOffset) return cloneMarks(op.marksByOffset[i - offset] ?? []);
498
+ const inherited = marksAt(runs, Math.max(0, offset - 1));
499
+ return op.marks && op.marks.length > 0 ? mergeMarksByType(inherited, op.marks) : inherited;
500
+ }
501
+ const src = i < offset ? i : i - op.text.length;
502
+ return marksAt(runs, src);
503
+ }, shiftKeeps(keeps, offset, op.text.length));
504
+ return {
505
+ doc: replaceAt(doc, op.path, nextPara),
506
+ inverse: {
507
+ type: "delete_text",
508
+ path: op.path,
509
+ offset,
510
+ length: op.text.length
511
+ }
512
+ };
513
+ }
514
+ case "delete_text": {
515
+ const para = getNodeAt(doc, op.path);
516
+ assertLeafTextBlock(para);
517
+ const { text, runs, keeps } = flattenText(para);
518
+ const offset = Math.max(0, Math.min(op.offset, text.length));
519
+ const length = Math.min(op.length, text.length - offset);
520
+ const deletedKeeps = captureKeepsInRange(keeps, offset, offset + length);
521
+ const captured = captureRuns(text, runs, offset, offset + length);
522
+ const nextPara = rebuildParagraph(para, text.slice(0, offset) + text.slice(offset + length), (i) => {
523
+ const src = i < offset ? i : i + length;
524
+ return marksAt(runs, src);
525
+ }, deleteKeepsInRange(keeps, offset, length));
526
+ return {
527
+ doc: replaceAt(doc, op.path, nextPara),
528
+ inverse: {
529
+ type: "replace_slice",
530
+ path: op.path,
531
+ from: offset,
532
+ to: offset,
533
+ runs: captured,
534
+ keeps: deletedKeeps
535
+ }
536
+ };
537
+ }
538
+ case "split_paragraph": {
539
+ const parentPath = op.path.slice(0, -1);
540
+ const index = op.path.at(-1);
541
+ if (index === void 0) throw new Error("split_paragraph path is empty");
542
+ assertIntegerPath(parentPath);
543
+ const parent = parentPath.length > 0 ? getNodeAt(doc, parentPath) : doc;
544
+ const para = getNodeAt(doc, op.path);
545
+ assertLeafTextBlock(para);
546
+ const { text, runs, keeps } = flattenText(para);
547
+ const offset = Math.max(0, Math.min(op.offset, text.length));
548
+ const leftKeeps = keeps.filter((k) => k.at < offset);
549
+ const left = rebuildParagraph(para, text.slice(0, offset), (i) => marksAt(runs, i), leftKeeps);
550
+ let rightRebuilt;
551
+ if (op.rightNode) rightRebuilt = deepCloneNode(op.rightNode);
552
+ else {
553
+ const rightKeeps = keeps.filter((k) => k.at >= offset).map((k) => ({
554
+ ...k,
555
+ at: k.at - offset
556
+ }));
557
+ rightRebuilt = rebuildParagraph({
558
+ ...cloneNode(para),
559
+ content: [createText("")],
560
+ id: nextId(para.type === "listItem" ? "li" : "p")
561
+ }, text.slice(offset), (i) => marksAt(runs, offset + i), rightKeeps);
562
+ }
563
+ const nextParent = cloneNode(parent);
564
+ if (!nextParent.content) throw new Error("split parent has no content");
565
+ nextParent.content.splice(index, 1, left, rightRebuilt);
566
+ return {
567
+ doc: parentPath.length > 0 ? replaceAt(doc, parentPath, nextParent) : nextParent,
568
+ inverse: {
569
+ type: "merge_paragraph",
570
+ path: [...parentPath, index + 1]
571
+ }
572
+ };
573
+ }
574
+ case "merge_paragraph": {
575
+ const parentPath = op.path.slice(0, -1);
576
+ const index = op.path.at(-1);
577
+ if (index === void 0 || index <= 0) throw new Error("Cannot merge first paragraph");
578
+ assertIntegerPath(parentPath);
579
+ const parent = parentPath.length > 0 ? getNodeAt(doc, parentPath) : doc;
580
+ const left = getNodeAt(doc, [...parentPath, index - 1]);
581
+ const right = getNodeAt(doc, op.path);
582
+ assertLeafTextBlock(left);
583
+ assertLeafTextBlock(right);
584
+ const rightSnap = deepCloneNode(right);
585
+ const leftFlat = flattenText(left);
586
+ const rightFlat = flattenText(right);
587
+ const mergedText = leftFlat.text + rightFlat.text;
588
+ const joinAt = leftFlat.text.length;
589
+ const merged = rebuildParagraph(left, mergedText, (i) => {
590
+ if (i < joinAt) return marksAt(leftFlat.runs, i);
591
+ return marksAt(rightFlat.runs, i - joinAt);
592
+ }, [...leftFlat.keeps, ...rightFlat.keeps.map((k) => ({
593
+ ...k,
594
+ at: k.at + joinAt
595
+ }))]);
596
+ const nextParent = cloneNode(parent);
597
+ if (!nextParent.content) throw new Error("merge parent has no content");
598
+ nextParent.content.splice(index - 1, 2, merged);
599
+ return {
600
+ doc: parentPath.length > 0 ? replaceAt(doc, parentPath, nextParent) : nextParent,
601
+ inverse: {
602
+ type: "split_paragraph",
603
+ path: [...parentPath, index - 1],
604
+ offset: joinAt,
605
+ rightNode: rightSnap
606
+ }
607
+ };
608
+ }
609
+ case "set_selection": return {
610
+ doc,
611
+ inverse: op
612
+ };
613
+ case "set_attrs": {
614
+ const node = getNodeAt(doc, op.path);
615
+ const prevSnapshot = copyAttrs(node.attrs) ?? emptyAttrsRecord();
616
+ let nextAttrs;
617
+ if (op.replace) nextAttrs = copyAttrs(op.attrs) ?? emptyAttrsRecord();
618
+ else {
619
+ nextAttrs = copyAttrs(node.attrs) ?? emptyAttrsRecord();
620
+ for (const [key, value] of Object.entries(op.attrs)) if (value === null) delete nextAttrs[key];
621
+ else nextAttrs[key] = value;
622
+ }
623
+ const next = {
624
+ ...cloneNode(node),
625
+ attrs: Object.keys(nextAttrs).length > 0 ? nextAttrs : void 0
626
+ };
627
+ return {
628
+ doc: replaceAt(doc, op.path, next),
629
+ inverse: {
630
+ type: "set_attrs",
631
+ path: op.path,
632
+ attrs: prevSnapshot,
633
+ replace: true
634
+ }
635
+ };
636
+ }
637
+ case "set_mark": {
638
+ const para = getNodeAt(doc, op.path);
639
+ assertLeafTextBlock(para);
640
+ const { text, runs, keeps } = flattenText(para);
641
+ const from = Math.max(0, Math.min(op.from, text.length));
642
+ const to = Math.max(from, Math.min(op.to, text.length));
643
+ const captured = captureRuns(text, runs, from, to);
644
+ const lostKeeps = captureKeepsInRange(keeps, from, to);
645
+ const nextPara = rebuildParagraph(para, text, (i) => {
646
+ const base = marksAt(runs, i);
647
+ if (i >= from && i < to) return [...base.filter((m) => m.type !== op.mark.type), {
648
+ ...op.mark,
649
+ attrs: copyAttrs(op.mark.attrs)
650
+ }];
651
+ return base;
652
+ }, keeps);
653
+ return {
654
+ doc: replaceAt(doc, op.path, nextPara),
655
+ inverse: {
656
+ type: "replace_slice",
657
+ path: op.path,
658
+ from,
659
+ to,
660
+ runs: captured,
661
+ keeps: lostKeeps
662
+ }
663
+ };
664
+ }
665
+ case "remove_mark": {
666
+ const para = getNodeAt(doc, op.path);
667
+ assertLeafTextBlock(para);
668
+ const { text, runs, keeps } = flattenText(para);
669
+ const from = Math.max(0, Math.min(op.from, text.length));
670
+ const to = Math.max(from, Math.min(op.to, text.length));
671
+ const captured = captureRuns(text, runs, from, to);
672
+ const lostKeeps = captureKeepsInRange(keeps, from, to);
673
+ const nextPara = rebuildParagraph(para, text, (i) => {
674
+ const base = marksAt(runs, i);
675
+ if (i >= from && i < to) return base.filter((m) => m.type !== op.markType);
676
+ return base;
677
+ }, keeps);
678
+ return {
679
+ doc: replaceAt(doc, op.path, nextPara),
680
+ inverse: {
681
+ type: "replace_slice",
682
+ path: op.path,
683
+ from,
684
+ to,
685
+ runs: captured,
686
+ keeps: lostKeeps
687
+ }
688
+ };
689
+ }
690
+ case "replace_slice": {
691
+ const para = getNodeAt(doc, op.path);
692
+ assertLeafTextBlock(para);
693
+ const { text, runs, keeps } = flattenText(para);
694
+ const from = Math.max(0, Math.min(op.from, text.length));
695
+ const to = Math.max(from, Math.min(op.to, text.length));
696
+ const captured = captureRuns(text, runs, from, to);
697
+ const lostKeeps = captureKeepsInRange(keeps, from, to);
698
+ const newText = op.runs.map((r) => r.text).join("");
699
+ if (newText.length > 1e5) throw new Error(`replace_slice exceeds MAX_INSERT_CHARS (${MAX_INSERT_CHARS})`);
700
+ if ((op.keeps?.length ?? 0) > 1e4) throw new Error(`replace_slice exceeds MAX_SLICE_KEEPS (${MAX_SLICE_KEEPS})`);
701
+ const insertedMarks = [];
702
+ for (const run of op.runs) {
703
+ const marks = cloneMarks(run.marks);
704
+ for (const _ of run.text) insertedMarks.push(cloneMarks(marks));
705
+ }
706
+ const nextText = text.slice(0, from) + newText + text.slice(to);
707
+ const delta = newText.length - (to - from);
708
+ const nextPara = rebuildParagraph(para, nextText, (i) => {
709
+ if (i >= from && i < from + newText.length) return insertedMarks[i - from] ?? [];
710
+ const src = i < from ? i : i - delta;
711
+ return marksAt(runs, src);
712
+ }, mergeSliceKeeps(keeps.filter((k) => k.at <= from || k.at >= to).map((k) => k.at >= to ? {
713
+ ...k,
714
+ at: k.at + delta
715
+ } : k), from, op.keeps));
716
+ return {
717
+ doc: replaceAt(doc, op.path, nextPara),
718
+ inverse: {
719
+ type: "replace_slice",
720
+ path: op.path,
721
+ from,
722
+ to: from + newText.length,
723
+ runs: captured,
724
+ keeps: lostKeeps
725
+ }
726
+ };
727
+ }
728
+ case "insert_node": {
729
+ assertMaxTreeDepth(op.node);
730
+ if (textLength(op.node) > 1e5) throw new Error(`insert_node exceeds MAX_INSERT_CHARS (${MAX_INSERT_CHARS})`);
731
+ const nextParent = cloneNode(op.path.length > 0 ? getNodeAt(doc, op.path) : doc);
732
+ nextParent.content ?? (nextParent.content = []);
733
+ const index = Math.max(0, Math.min(op.index, nextParent.content.length));
734
+ nextParent.content.splice(index, 0, ensureNodeIds(deepCloneNode(op.node)));
735
+ return {
736
+ doc: op.path.length > 0 ? replaceAt(doc, op.path, nextParent) : nextParent,
737
+ inverse: {
738
+ type: "remove_node",
739
+ path: op.path,
740
+ index
741
+ }
742
+ };
743
+ }
744
+ case "remove_node": {
745
+ const nextParent = cloneNode(op.path.length > 0 ? getNodeAt(doc, op.path) : doc);
746
+ if (!nextParent.content || op.index < 0 || op.index >= nextParent.content.length) throw new Error("remove_node out of bounds");
747
+ const [removed] = nextParent.content.splice(op.index, 1);
748
+ return {
749
+ doc: op.path.length > 0 ? replaceAt(doc, op.path, nextParent) : nextParent,
750
+ inverse: {
751
+ type: "insert_node",
752
+ path: op.path,
753
+ index: op.index,
754
+ node: removed
755
+ }
756
+ };
757
+ }
758
+ default: throw new Error(`Unknown op: ${JSON.stringify(op)}`);
759
+ }
760
+ }
761
+ function applyOps(doc, ops, selection, schema) {
762
+ let current = doc;
763
+ let sel = selection;
764
+ const inverses = [];
765
+ for (const op of ops) {
766
+ if (op.type === "set_selection") {
767
+ inverses.push({
768
+ type: "set_selection",
769
+ selection: sel
770
+ });
771
+ sel = clampSelection(current, op.selection);
772
+ continue;
773
+ }
774
+ const result = applyOp(current, op, schema);
775
+ current = result.doc;
776
+ inverses.push(result.inverse);
777
+ if (op.type === "insert_text") sel = collapsedAt(op.path, op.offset + op.text.length);
778
+ else if (op.type === "delete_text") sel = collapsedAt(op.path, op.offset);
779
+ else if (op.type === "split_paragraph") {
780
+ const parentPath = op.path.slice(0, -1);
781
+ const index = op.path.at(-1);
782
+ if (index === void 0) throw new Error("split_paragraph path is empty");
783
+ sel = collapsedAt([...parentPath, index + 1], 0);
784
+ } else if (op.type === "merge_paragraph") {
785
+ const inv = result.inverse;
786
+ if (inv.type === "split_paragraph") sel = collapsedAt(inv.path, inv.offset);
787
+ } else if (op.type === "insert_node") {
788
+ const index = result.inverse.type === "remove_node" ? result.inverse.index : op.index;
789
+ sel = {
790
+ anchor: {
791
+ offset: sel.anchor.offset,
792
+ path: remapPathSibling(sel.anchor.path, op.path, index, 1)
793
+ },
794
+ focus: {
795
+ offset: sel.focus.offset,
796
+ path: remapPathSibling(sel.focus.path, op.path, index, 1)
797
+ }
798
+ };
799
+ sel = clampSelection(current, sel);
800
+ } else if (op.type === "remove_node") {
801
+ sel = {
802
+ anchor: {
803
+ offset: sel.anchor.offset,
804
+ path: remapPathSibling(sel.anchor.path, op.path, op.index, -1)
805
+ },
806
+ focus: {
807
+ offset: sel.focus.offset,
808
+ path: remapPathSibling(sel.focus.path, op.path, op.index, -1)
809
+ }
810
+ };
811
+ sel = clampSelection(current, sel);
812
+ } else sel = clampSelection(current, sel);
813
+ }
814
+ return {
815
+ doc: current,
816
+ selection: clampSelection(current, sel),
817
+ inverses: inverses.toReversed()
818
+ };
819
+ }
820
+ function remapPathSibling(path, parent, index, delta) {
821
+ const d = parent.length;
822
+ if (path.length <= d) return path;
823
+ for (let i = 0; i < d; i++) if (path[i] !== parent[i]) return path;
824
+ const at = path[d];
825
+ if (at === void 0) return path;
826
+ if (delta < 0 && at === index) return path;
827
+ if (delta > 0 && at >= index || delta < 0 && at > index) {
828
+ const next = [...path];
829
+ next[d] = at + delta;
830
+ return next;
831
+ }
832
+ return path;
833
+ }
834
+ function selectionTextRange(sel) {
835
+ const { from, to } = orderedRange(sel);
836
+ if (from.path.join(".") !== to.path.join(".")) return null;
837
+ return {
838
+ path: from.path,
839
+ from: from.offset,
840
+ to: to.offset
841
+ };
842
+ }
843
+ //#endregion
844
+ //#region src/transform.ts
845
+ function pathsEqual(a, b) {
846
+ if (a.length !== b.length) return false;
847
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
848
+ return true;
849
+ }
850
+ function pathPrefix(longer, shorter) {
851
+ if (longer.length < shorter.length) return false;
852
+ for (let i = 0; i < shorter.length; i++) if (longer[i] !== shorter[i]) return false;
853
+ return true;
854
+ }
855
+ /** Shift path index at `depth` by `delta` when the path is at/after `index`. */
856
+ function shiftPath(path, parentPath, index, delta) {
857
+ if (!pathPrefix(path, parentPath) || path.length <= parentPath.length) return path;
858
+ const at = parentPath.length;
859
+ const cur = path[at];
860
+ if (cur === void 0 || cur < index) return path;
861
+ const next = path.slice();
862
+ next[at] = cur + delta;
863
+ return next;
864
+ }
865
+ function mapPath(op, map) {
866
+ switch (op.type) {
867
+ case "set_selection": return op;
868
+ case "insert_text":
869
+ case "delete_text":
870
+ case "split_paragraph":
871
+ case "merge_paragraph":
872
+ case "set_mark":
873
+ case "remove_mark":
874
+ case "replace_slice":
875
+ case "set_attrs":
876
+ case "insert_node":
877
+ case "remove_node": return {
878
+ ...op,
879
+ path: map(op.path)
880
+ };
881
+ default: return op;
882
+ }
883
+ }
884
+ function mapOffset(op, path, map) {
885
+ if (!("path" in op) || !pathsEqual(op.path, path)) return op;
886
+ switch (op.type) {
887
+ case "insert_text": return {
888
+ ...op,
889
+ offset: map(op.offset)
890
+ };
891
+ case "delete_text": {
892
+ const start = map(op.offset);
893
+ const end = map(op.offset + op.length);
894
+ return {
895
+ ...op,
896
+ offset: start,
897
+ length: Math.max(0, end - start)
898
+ };
899
+ }
900
+ case "split_paragraph": return {
901
+ ...op,
902
+ offset: map(op.offset)
903
+ };
904
+ case "set_mark":
905
+ case "remove_mark":
906
+ case "replace_slice": {
907
+ const from = map(op.from);
908
+ const to = map(op.to);
909
+ return {
910
+ ...op,
911
+ from,
912
+ to: Math.max(from, to)
913
+ };
914
+ }
915
+ case "merge_paragraph":
916
+ case "insert_node":
917
+ case "remove_node":
918
+ case "set_attrs": return op;
919
+ default: return op;
920
+ }
921
+ }
922
+ /**
923
+ * Transform `op` so it can be applied after `against` was already applied
924
+ * (ProseMirror-style: rebase client op against server op).
925
+ * Returns null if `op` becomes a no-op.
926
+ */
927
+ function transformOp(op, against) {
928
+ if (op.type === "set_selection" || against.type === "set_selection") return op.type === "set_selection" ? null : op;
929
+ if (against.type === "insert_node") {
930
+ let next = mapPath(op, (p) => shiftPath(p, against.path, against.index, 1));
931
+ if ((next.type === "insert_node" || next.type === "remove_node") && pathsEqual(next.path, against.path) && next.index >= against.index) next = {
932
+ ...next,
933
+ index: next.index + 1
934
+ };
935
+ return next;
936
+ }
937
+ if (against.type === "remove_node") {
938
+ if ((op.type === "insert_node" || op.type === "remove_node") && pathsEqual(op.path, against.path)) {
939
+ if (op.index === against.index && op.type === "remove_node") return null;
940
+ if (op.index > against.index) return {
941
+ ...op,
942
+ index: op.index - 1
943
+ };
944
+ }
945
+ const removedPath = [...against.path, against.index];
946
+ if ("path" in op && pathPrefix(op.path, removedPath) && op.path.length >= removedPath.length && pathsEqual(op.path.slice(0, removedPath.length), removedPath)) return null;
947
+ let next = mapPath(op, (p) => shiftPath(p, against.path, against.index + 1, -1));
948
+ if ((next.type === "insert_node" || next.type === "remove_node") && pathsEqual(next.path, against.path) && next.index > against.index) next = {
949
+ ...next,
950
+ index: next.index - 1
951
+ };
952
+ return next;
953
+ }
954
+ if (against.type === "split_paragraph") {
955
+ const parentPath = against.path.slice(0, -1);
956
+ const index = against.path.at(-1);
957
+ if (index === void 0) return op;
958
+ if ("path" in op && pathsEqual(op.path, against.path)) {
959
+ if (op.type === "insert_text") {
960
+ if (op.offset <= against.offset) return op;
961
+ return {
962
+ ...op,
963
+ path: [...parentPath, index + 1],
964
+ offset: op.offset - against.offset
965
+ };
966
+ }
967
+ if (op.type === "delete_text") {
968
+ if (op.offset + op.length <= against.offset) return op;
969
+ if (op.offset >= against.offset) return {
970
+ ...op,
971
+ path: [...parentPath, index + 1],
972
+ offset: op.offset - against.offset
973
+ };
974
+ return {
975
+ ...op,
976
+ length: against.offset - op.offset
977
+ };
978
+ }
979
+ if (op.type === "split_paragraph") {
980
+ if (op.offset <= against.offset) return op;
981
+ return {
982
+ ...op,
983
+ path: [...parentPath, index + 1],
984
+ offset: op.offset - against.offset
985
+ };
986
+ }
987
+ }
988
+ return mapPath(op, (p) => shiftPath(p, parentPath, index + 1, 1));
989
+ }
990
+ if (against.type === "merge_paragraph") {
991
+ const parentPath = against.path.slice(0, -1);
992
+ const index = against.path.at(-1);
993
+ if (index === void 0 || index <= 0) return op;
994
+ const leftPath = [...parentPath, index - 1];
995
+ const rightPath = against.path;
996
+ if ("path" in op && pathsEqual(op.path, rightPath)) return mapPath(op, () => leftPath);
997
+ return mapPath(op, (p) => shiftPath(p, parentPath, index, -1));
998
+ }
999
+ if (against.type === "insert_text" && "path" in op && pathsEqual(op.path, against.path)) return mapOffset(op, against.path, (o) => o >= against.offset ? o + against.text.length : o);
1000
+ if (against.type === "delete_text" && "path" in op && pathsEqual(op.path, against.path)) {
1001
+ const delStart = against.offset;
1002
+ const delEnd = against.offset + against.length;
1003
+ return mapOffset(op, against.path, (o) => {
1004
+ if (o >= delEnd) return o - against.length;
1005
+ if (o >= delStart) return delStart;
1006
+ return o;
1007
+ });
1008
+ }
1009
+ if (against.type === "replace_slice" && "path" in op && pathsEqual(op.path, against.path)) {
1010
+ const removed = against.to - against.from;
1011
+ const inserted = against.runs.reduce((n, r) => n + r.text.length, 0);
1012
+ const delta = inserted - removed;
1013
+ return mapOffset(op, against.path, (o) => {
1014
+ if (o >= against.to) return o + delta;
1015
+ if (o >= against.from) return against.from + inserted;
1016
+ return o;
1017
+ });
1018
+ }
1019
+ return op;
1020
+ }
1021
+ /** Transform a sequence of ops against a single already-applied op. */
1022
+ function transformOps(ops, against) {
1023
+ const out = [];
1024
+ for (const op of ops) {
1025
+ const next = transformOp(op, against);
1026
+ if (next) out.push(next);
1027
+ }
1028
+ return out;
1029
+ }
1030
+ /**
1031
+ * Rebase client ops that were based on `baseVersion` against server ops
1032
+ * that landed since (in order). Returns ops safe to apply on the new tip.
1033
+ */
1034
+ function rebaseOps(clientOps, serverOps) {
1035
+ let pending = clientOps.filter((o) => o.type !== "set_selection");
1036
+ for (const serverOp of serverOps) {
1037
+ if (serverOp.type === "set_selection") continue;
1038
+ pending = transformOps(pending, serverOp);
1039
+ }
1040
+ return pending;
1041
+ }
1042
+ //#endregion
1043
+ //#region src/structure.ts
1044
+ var TEXT_BLOCKS = /* @__PURE__ */ new Set([
1045
+ "paragraph",
1046
+ "heading",
1047
+ "listItem",
1048
+ "blockquote",
1049
+ "codeBlock"
1050
+ ]);
1051
+ var LIST_TYPES = /* @__PURE__ */ new Set(["bulletList", "orderedList"]);
1052
+ function isTextBlock(type) {
1053
+ return TEXT_BLOCKS.has(type);
1054
+ }
1055
+ function isListType(type) {
1056
+ return LIST_TYPES.has(type);
1057
+ }
1058
+ /** Flatten text of a text-bearing node (paragraph / heading / listItem). */
1059
+ function plainText(node) {
1060
+ if (node.type === "text") return node.text ?? "";
1061
+ let out = "";
1062
+ for (const child of node.content ?? []) if (child.type === "text") out += child.text ?? "";
1063
+ else if (child.content) out += plainText(child);
1064
+ return out;
1065
+ }
1066
+ /**
1067
+ * Resolve selection path to a text-bearing leaf.
1068
+ * Lists → first/only item; listItem → nested paragraph; table → first cell paragraph.
1069
+ */
1070
+ function resolveTextPath(doc, path) {
1071
+ if (path.length === 0) return [0];
1072
+ let cur = getNodeAt(doc, path);
1073
+ const resolved = [...path];
1074
+ if (isListType(cur.type)) {
1075
+ if ((cur.content?.length ?? 0) === 0) return resolved;
1076
+ resolved.push(0);
1077
+ cur = cur.content[0];
1078
+ }
1079
+ if (cur.type === "table") {
1080
+ const row = cur.content?.[0];
1081
+ const cell = row?.content?.[0];
1082
+ if (row && cell) {
1083
+ resolved.push(0, 0);
1084
+ cur = cell;
1085
+ }
1086
+ }
1087
+ if (cur.type === "tableRow") {
1088
+ const cell = cur.content?.[0];
1089
+ if (cell) {
1090
+ resolved.push(0);
1091
+ cur = cell;
1092
+ }
1093
+ }
1094
+ if (cur.type === "tableCell") {
1095
+ const para = cur.content?.[0];
1096
+ if (para && isTextBlock(para.type)) {
1097
+ resolved.push(0);
1098
+ cur = para;
1099
+ } else if (!para) return resolved;
1100
+ }
1101
+ if (cur.type === "listItem") {
1102
+ const first = cur.content?.[0];
1103
+ if (first && first.type !== "text" && isTextBlock(first.type) && first.type !== "listItem") {
1104
+ resolved.push(0);
1105
+ cur = first;
1106
+ }
1107
+ }
1108
+ if (cur.type === "blockquote" || cur.type === "codeBlock") {
1109
+ const first = cur.content?.[0];
1110
+ if (first && isTextBlock(first.type)) {
1111
+ resolved.push(0);
1112
+ return resolveTextPath(doc, resolved);
1113
+ }
1114
+ }
1115
+ return resolved;
1116
+ }
1117
+ function createListItem(content = [createText("")]) {
1118
+ return {
1119
+ type: "listItem",
1120
+ content: [...content]
1121
+ };
1122
+ }
1123
+ function createList(listType, items) {
1124
+ return {
1125
+ type: listType,
1126
+ id: `list_${Date.now()}`,
1127
+ content: items.length > 0 ? items : [createListItem()]
1128
+ };
1129
+ }
1130
+ /** Extract a paragraph to lift out of a list item. */
1131
+ function liftedParagraphFromItem(item) {
1132
+ const content = item.content ?? [];
1133
+ const first = content[0];
1134
+ if (first !== void 0 && first.type === "paragraph") return deepCloneNode(first);
1135
+ if (first !== void 0 && isTextBlock(first.type) && first.type !== "listItem" && !isListType(first.type)) return deepCloneNode(first);
1136
+ return createParagraph(content.length > 0 ? content.map(deepCloneNode) : [createText("")]);
1137
+ }
1138
+ /**
1139
+ * Lift list item `itemIndex` out of the list at `listPath` into the list's parent.
1140
+ * Removes the list, reinserts before-list (if any), lifted paragraph, after-list (if any).
1141
+ */
1142
+ function liftIntoParent(doc, listPath, itemIndex) {
1143
+ const list = getNodeAt(doc, listPath);
1144
+ const items = list.content ?? [];
1145
+ const listIndex = listPath.at(-1);
1146
+ if (listIndex === void 0) throw new Error("liftIntoParent: empty list path");
1147
+ const parentPath = listPath.slice(0, -1);
1148
+ const item = items[itemIndex];
1149
+ if (item === void 0) throw new Error("liftIntoParent: item out of bounds");
1150
+ const before = items.slice(0, itemIndex);
1151
+ const after = items.slice(itemIndex + 1);
1152
+ const para = liftedParagraphFromItem(item);
1153
+ const ops = [];
1154
+ ops.push({
1155
+ type: "remove_node",
1156
+ path: parentPath,
1157
+ index: listIndex
1158
+ });
1159
+ let insertAt = listIndex;
1160
+ if (before.length > 0) {
1161
+ ops.push({
1162
+ type: "insert_node",
1163
+ path: parentPath,
1164
+ index: insertAt,
1165
+ node: {
1166
+ ...list,
1167
+ content: before,
1168
+ id: `list_${Date.now()}_a`
1169
+ }
1170
+ });
1171
+ insertAt += 1;
1172
+ }
1173
+ ops.push({
1174
+ type: "insert_node",
1175
+ path: parentPath,
1176
+ index: insertAt,
1177
+ node: para
1178
+ });
1179
+ const liftedIndex = insertAt;
1180
+ const selectionPath = [...parentPath, insertAt];
1181
+ insertAt += 1;
1182
+ if (after.length > 0) ops.push({
1183
+ type: "insert_node",
1184
+ path: parentPath,
1185
+ index: insertAt,
1186
+ node: {
1187
+ ...list,
1188
+ content: after,
1189
+ id: `list_${Date.now()}_b`
1190
+ }
1191
+ });
1192
+ return {
1193
+ ops,
1194
+ selectionPath,
1195
+ liftedIndex
1196
+ };
1197
+ }
1198
+ /** Exit empty list item: lift to paragraph in the list's parent (any depth). */
1199
+ function exitListItemOps(doc, listPath, itemIndex) {
1200
+ const { ops, selectionPath } = liftIntoParent(doc, listPath, itemIndex);
1201
+ return {
1202
+ ops,
1203
+ selectionPath
1204
+ };
1205
+ }
1206
+ function textBlockLength(doc, path) {
1207
+ try {
1208
+ return textLength(getNodeAt(doc, path));
1209
+ } catch {
1210
+ return 0;
1211
+ }
1212
+ }
1213
+ //#endregion
1214
+ //#region src/normalize.ts
1215
+ /** Idempotent normalize: ensure doc has content, text blocks have text leaves, coalesce adjacent same-mark texts. */
1216
+ function normalize(doc) {
1217
+ if (doc.type !== "doc") throw new Error("normalize expects doc");
1218
+ const next = cloneNode(doc);
1219
+ if (!next.content || next.content.length === 0) {
1220
+ next.content = [{
1221
+ content: [createText("")],
1222
+ id: "p_empty",
1223
+ type: "paragraph"
1224
+ }];
1225
+ return next;
1226
+ }
1227
+ next.content = next.content.map(normalizeBlock);
1228
+ if (next.content.length === 0) next.content = [{
1229
+ content: [createText("")],
1230
+ id: "p_empty",
1231
+ type: "paragraph"
1232
+ }];
1233
+ return next;
1234
+ }
1235
+ function normalizeBlock(node) {
1236
+ if (node.type === "text") return node;
1237
+ const cloned = cloneNode(node);
1238
+ if (!cloned.content || cloned.content.length === 0) {
1239
+ if (isListType(node.type)) return {
1240
+ type: "paragraph",
1241
+ id: nextId("p"),
1242
+ content: [createText("")]
1243
+ };
1244
+ if (isTextBlock(node.type)) cloned.content = [createText("")];
1245
+ return cloned;
1246
+ }
1247
+ if (isTextBlock(node.type)) {
1248
+ cloned.content = coalesceText(cloned.content.map(normalizeBlock));
1249
+ if (cloned.content.length === 0) cloned.content = [createText("")];
1250
+ } else {
1251
+ cloned.content = cloned.content.map(normalizeBlock);
1252
+ if (isListType(node.type)) {
1253
+ cloned.content = cloned.content.filter((c) => {
1254
+ if (c.type !== "listItem") return true;
1255
+ return (c.content?.length ?? 0) > 0;
1256
+ });
1257
+ if (cloned.content.length === 0) return {
1258
+ type: "paragraph",
1259
+ id: nextId("p"),
1260
+ content: [createText("")]
1261
+ };
1262
+ }
1263
+ }
1264
+ return cloned;
1265
+ }
1266
+ function coalesceText(nodes) {
1267
+ const out = [];
1268
+ for (const n of nodes) {
1269
+ if (n.type !== "text") {
1270
+ out.push(n);
1271
+ continue;
1272
+ }
1273
+ const prev = out.at(-1);
1274
+ if (prev?.type === "text" && JSON.stringify(prev.marks ?? []) === JSON.stringify(n.marks ?? [])) out[out.length - 1] = {
1275
+ ...prev,
1276
+ text: (prev.text ?? "") + (n.text ?? "")
1277
+ };
1278
+ else out.push(n);
1279
+ }
1280
+ return out;
1281
+ }
1282
+ function isNormalizeIdempotent(doc) {
1283
+ const a = normalize(doc), b = normalize(a);
1284
+ return JSON.stringify(a) === JSON.stringify(b);
1285
+ }
1286
+ //#endregion
1287
+ //#region src/transaction.ts
1288
+ function createState(doc = createDoc(), selection) {
1289
+ const normalized = normalize(doc);
1290
+ return {
1291
+ doc: normalized,
1292
+ selection: clampSelection(normalized, selection ?? collapsedAt([0], 0))
1293
+ };
1294
+ }
1295
+ function applyTransaction(state, tr, schema) {
1296
+ const { doc, selection, inverses } = applyOps(state.doc, tr.ops, state.selection, schema);
1297
+ return {
1298
+ inverses,
1299
+ state: {
1300
+ doc: normalize(doc),
1301
+ selection
1302
+ }
1303
+ };
1304
+ }
1305
+ function transaction(...ops) {
1306
+ return { ops };
1307
+ }
1308
+ //#endregion
1309
+ //#region src/history.ts
1310
+ function createHistory(options = {}) {
1311
+ const maxDepth = options.maxDepth ?? 100, mergeWindowMs = options.mergeWindowMs ?? 500, maxEntryChars = options.maxEntryChars ?? 1e4, schema = options.schema, undoStack = [], redoStack = [];
1312
+ function push(entry) {
1313
+ const last = undoStack.at(-1);
1314
+ if (last && entry.at - last.at <= mergeWindowMs && isTyping(entry.redo) && isTyping(last.redo) && last.chars + entry.chars <= maxEntryChars) {
1315
+ last.redo = [...last.redo, ...entry.redo];
1316
+ last.inverses = [...entry.inverses, ...last.inverses];
1317
+ last.selection = entry.selection;
1318
+ last.at = entry.at;
1319
+ last.chars += entry.chars;
1320
+ } else undoStack.push(entry);
1321
+ while (undoStack.length > maxDepth) undoStack.shift();
1322
+ redoStack.length = 0;
1323
+ }
1324
+ return {
1325
+ apply(state, tr, now = Date.now()) {
1326
+ const { state: next, inverses } = applyTransaction(state, tr, schema);
1327
+ if (tr.ops.some((op) => op.type !== "set_selection")) {
1328
+ const redo = tr.ops.filter((o) => o.type !== "set_selection");
1329
+ push({
1330
+ inverses,
1331
+ redo,
1332
+ selection: next.selection,
1333
+ at: now,
1334
+ chars: entryCharCount(redo)
1335
+ });
1336
+ }
1337
+ return next;
1338
+ },
1339
+ canRedo: () => redoStack.length > 0,
1340
+ canUndo: () => undoStack.length > 0,
1341
+ clear() {
1342
+ undoStack.length = 0;
1343
+ redoStack.length = 0;
1344
+ },
1345
+ depth: () => undoStack.length,
1346
+ redo(state) {
1347
+ const entry = redoStack.pop();
1348
+ if (!entry) return state;
1349
+ const { state: next, inverses } = applyTransaction(state, transaction(...entry.redo), schema);
1350
+ undoStack.push({
1351
+ inverses,
1352
+ redo: entry.redo,
1353
+ selection: entry.selection,
1354
+ at: Date.now(),
1355
+ chars: entry.chars
1356
+ });
1357
+ return {
1358
+ ...next,
1359
+ selection: entry.selection
1360
+ };
1361
+ },
1362
+ undo(state) {
1363
+ const entry = undoStack.pop();
1364
+ if (!entry) return state;
1365
+ const { state: next } = applyTransaction(state, transaction(...entry.inverses), schema);
1366
+ redoStack.push(entry);
1367
+ return next;
1368
+ }
1369
+ };
1370
+ }
1371
+ function isTyping(ops) {
1372
+ return ops.length > 0 && ops.every((o) => o.type === "insert_text" || o.type === "delete_text");
1373
+ }
1374
+ function entryCharCount(ops) {
1375
+ let n = 0;
1376
+ for (const op of ops) if (op.type === "insert_text") n += op.text.length;
1377
+ else if (op.type === "delete_text") n += op.length;
1378
+ return n;
1379
+ }
1380
+ function emptyState() {
1381
+ return createState();
1382
+ }
1383
+ //#endregion
1384
+ //#region src/commands.ts
1385
+ function textPath(state) {
1386
+ return resolveTextPath(state.doc, state.selection.anchor.path);
1387
+ }
1388
+ function samePath(a, b) {
1389
+ return a.length === b.length && a.every((v, i) => v === b[i]);
1390
+ }
1391
+ function isLeafTextBlock(node) {
1392
+ return isTextBlock(node.type) && !(node.content ?? []).some((c) => c.type !== "text" && (isTextBlock(c.type) || isListType(c.type) || c.type === "table" || c.type === "tableRow" || c.type === "tableCell"));
1393
+ }
1394
+ /**
1395
+ * Ops to delete the current selection — same-path text or cross-block (siblings / root span).
1396
+ * Keeps selectAll → deleteBackward/insertText working.
1397
+ */
1398
+ function deleteSelectionRangeOps(doc, sel) {
1399
+ const { from, to } = orderedRange(sel);
1400
+ if (samePath(from.path, to.path)) {
1401
+ if (from.offset === to.offset) return null;
1402
+ return [{
1403
+ type: "delete_text",
1404
+ path: resolveTextPath(doc, from.path),
1405
+ offset: from.offset,
1406
+ length: to.offset - from.offset
1407
+ }];
1408
+ }
1409
+ if (from.path.length === to.path.length && from.path.length > 0 && samePath(from.path.slice(0, -1), to.path.slice(0, -1))) return deleteSiblingSpan(doc, from, to);
1410
+ return deleteRootSpan(doc, from, to);
1411
+ }
1412
+ function deleteSiblingSpan(doc, from, to) {
1413
+ return deleteSpanUnderParent(doc, from, to, from.path.slice(0, -1), from.path.at(-1) ?? 0, to.path.at(-1) ?? 0);
1414
+ }
1415
+ function deleteSpanUnderParent(doc, from, to, parent, fromIdx, toIdx) {
1416
+ const fromPath = resolveTextPath(doc, from.path);
1417
+ const toPath = resolveTextPath(doc, to.path);
1418
+ const ops = [];
1419
+ const fromLen = textLength(getNodeAt(doc, fromPath));
1420
+ if (from.offset < fromLen) ops.push({
1421
+ type: "delete_text",
1422
+ path: fromPath,
1423
+ offset: from.offset,
1424
+ length: fromLen - from.offset
1425
+ });
1426
+ const fromBlock = getNodeAt(doc, [...parent, fromIdx]);
1427
+ const toBlock = getNodeAt(doc, [...parent, toIdx]);
1428
+ if (isLeafTextBlock(fromBlock) && isLeafTextBlock(toBlock) && toIdx > fromIdx) {
1429
+ if (to.offset > 0) {
1430
+ const remappedTo = [...toPath];
1431
+ if (parent.length < remappedTo.length) remappedTo[parent.length] = toIdx;
1432
+ ops.push({
1433
+ type: "delete_text",
1434
+ path: remappedTo,
1435
+ offset: 0,
1436
+ length: to.offset
1437
+ });
1438
+ }
1439
+ for (let i = toIdx - 1; i > fromIdx; i--) ops.push({
1440
+ type: "remove_node",
1441
+ path: parent,
1442
+ index: i
1443
+ });
1444
+ ops.push({
1445
+ type: "merge_paragraph",
1446
+ path: [...parent, fromIdx + 1]
1447
+ });
1448
+ } else return deleteLeafSpan(doc, from, to);
1449
+ ops.push({
1450
+ type: "set_selection",
1451
+ selection: collapsedAt(fromPath, from.offset)
1452
+ });
1453
+ return ops;
1454
+ }
1455
+ function collectTextLeaves(node, path, out) {
1456
+ if (isLeafTextBlock(node)) {
1457
+ out.push([...path]);
1458
+ return;
1459
+ }
1460
+ (node.content ?? []).forEach((child, i) => {
1461
+ collectTextLeaves(child, [...path, i], out);
1462
+ });
1463
+ }
1464
+ function removableAncestor(doc, leafPath) {
1465
+ if (leafPath.length >= 2) {
1466
+ const itemPath = leafPath.slice(0, -1);
1467
+ try {
1468
+ const item = getNodeAt(doc, itemPath);
1469
+ if (item.type === "listItem" || item.type === "tableCell") return {
1470
+ parent: itemPath.slice(0, -1),
1471
+ index: itemPath.at(-1) ?? 0
1472
+ };
1473
+ } catch {}
1474
+ }
1475
+ return {
1476
+ parent: leafPath.slice(0, -1),
1477
+ index: leafPath.at(-1) ?? 0
1478
+ };
1479
+ }
1480
+ function deleteLeafSpan(doc, from, to) {
1481
+ const fromPath = resolveTextPath(doc, from.path);
1482
+ const toPath = resolveTextPath(doc, to.path);
1483
+ let d = 0;
1484
+ while (d < fromPath.length && d < toPath.length && fromPath[d] === toPath[d]) d++;
1485
+ const rootPath = fromPath.slice(0, d);
1486
+ const rootNode = rootPath.length === 0 ? doc : getNodeAt(doc, rootPath);
1487
+ const leaves = [];
1488
+ collectTextLeaves(rootNode, rootPath, leaves);
1489
+ const i0 = leaves.findIndex((p) => samePath(p, fromPath));
1490
+ const i1 = leaves.findIndex((p) => samePath(p, toPath));
1491
+ if (i0 === -1 || i1 === -1) return [];
1492
+ const start = Math.min(i0, i1);
1493
+ const end = Math.max(i0, i1);
1494
+ const ops = [];
1495
+ const first = leaves[start];
1496
+ if (first === void 0) return [];
1497
+ const firstLen = textLength(getNodeAt(doc, first));
1498
+ const firstFrom = samePath(first, fromPath) ? from.offset : 0;
1499
+ if (firstFrom < firstLen) ops.push({
1500
+ type: "delete_text",
1501
+ path: first,
1502
+ offset: firstFrom,
1503
+ length: firstLen - firstFrom
1504
+ });
1505
+ const removals = [];
1506
+ const startFullyCovered = firstFrom === 0 && (end > start || to.offset >= firstLen);
1507
+ for (let i = end; i >= start; i--) {
1508
+ if (i === start && !startFullyCovered) continue;
1509
+ const leaf = leaves[i];
1510
+ if (leaf === void 0) continue;
1511
+ if (!(i === start ? startFullyCovered : i < end || to.offset >= textLength(getNodeAt(doc, leaf)))) {
1512
+ if (i === end && to.offset > 0) ops.push({
1513
+ type: "delete_text",
1514
+ path: leaf,
1515
+ offset: 0,
1516
+ length: to.offset
1517
+ });
1518
+ continue;
1519
+ }
1520
+ const rem = removableAncestor(doc, leaf);
1521
+ const key = `${rem.parent.join(".")}:${rem.index}`;
1522
+ if (!removals.some((r) => r.key === key)) removals.push({
1523
+ ...rem,
1524
+ key
1525
+ });
1526
+ }
1527
+ removals.sort((a, b) => {
1528
+ if (a.parent.length !== b.parent.length) return b.parent.length - a.parent.length;
1529
+ for (let i = 0; i < a.parent.length; i++) if (a.parent[i] !== b.parent[i]) return (b.parent[i] ?? 0) - (a.parent[i] ?? 0);
1530
+ return b.index - a.index;
1531
+ });
1532
+ for (const rem of removals) ops.push({
1533
+ type: "remove_node",
1534
+ path: rem.parent,
1535
+ index: rem.index
1536
+ });
1537
+ ops.push({
1538
+ type: "set_selection",
1539
+ selection: collapsedAt(fromPath, from.offset)
1540
+ });
1541
+ return ops;
1542
+ }
1543
+ function deleteRootSpan(doc, from, to) {
1544
+ const fromRoot = from.path[0] ?? 0;
1545
+ const toRoot = to.path[0] ?? 0;
1546
+ const fromPath = resolveTextPath(doc, from.path);
1547
+ const ops = [];
1548
+ if (fromRoot === toRoot) {
1549
+ let d = 0;
1550
+ while (d < from.path.length && d < to.path.length && from.path[d] === to.path[d]) d++;
1551
+ if (d < from.path.length && d < to.path.length && from.path.length === to.path.length) return deleteSpanUnderParent(doc, from, to, from.path.slice(0, d), from.path[d] ?? 0, to.path[d] ?? 0);
1552
+ return deleteLeafSpan(doc, from, to);
1553
+ }
1554
+ const fromLen = textLength(getNodeAt(doc, fromPath));
1555
+ if (from.offset < fromLen) ops.push({
1556
+ type: "delete_text",
1557
+ path: fromPath,
1558
+ offset: from.offset,
1559
+ length: fromLen - from.offset
1560
+ });
1561
+ const fromBlock = getNodeAt(doc, [fromRoot]);
1562
+ const toBlock = getNodeAt(doc, [toRoot]);
1563
+ const toPath = resolveTextPath(doc, to.path);
1564
+ if (from.path.length === 1 && to.path.length === 1 && isLeafTextBlock(fromBlock) && isLeafTextBlock(toBlock)) {
1565
+ if (to.offset > 0) ops.push({
1566
+ type: "delete_text",
1567
+ path: toPath,
1568
+ offset: 0,
1569
+ length: to.offset
1570
+ });
1571
+ for (let i = toRoot - 1; i > fromRoot; i--) ops.push({
1572
+ type: "remove_node",
1573
+ path: [],
1574
+ index: i
1575
+ });
1576
+ ops.push({
1577
+ type: "merge_paragraph",
1578
+ path: [fromRoot + 1]
1579
+ });
1580
+ } else return deleteLeafSpan(doc, from, to);
1581
+ ops.push({
1582
+ type: "set_selection",
1583
+ selection: collapsedAt(fromPath, from.offset)
1584
+ });
1585
+ return ops;
1586
+ }
1587
+ function insertText(text, marks) {
1588
+ return (state) => {
1589
+ const range = selectionTextRange(state.selection);
1590
+ if (!range) {
1591
+ const cross = deleteSelectionRangeOps(state.doc, state.selection);
1592
+ if (cross && !isCollapsedSelection(state.selection)) {
1593
+ const path = resolveTextPath(state.doc, orderedRange(state.selection).from.path);
1594
+ const offset = orderedRange(state.selection).from.offset;
1595
+ const ops = cross.filter((op) => op.type !== "set_selection");
1596
+ ops.push(marks && marks.length > 0 ? {
1597
+ type: "insert_text",
1598
+ path,
1599
+ offset,
1600
+ text,
1601
+ marks
1602
+ } : {
1603
+ type: "insert_text",
1604
+ path,
1605
+ offset,
1606
+ text
1607
+ }, {
1608
+ type: "set_selection",
1609
+ selection: collapsedAt(path, offset + text.length)
1610
+ });
1611
+ return ops;
1612
+ }
1613
+ }
1614
+ const path = resolveTextPath(state.doc, range?.path ?? state.selection.anchor.path);
1615
+ const offset = range ? range.from : state.selection.anchor.offset;
1616
+ const ops = [];
1617
+ if (range && range.from !== range.to) ops.push({
1618
+ type: "delete_text",
1619
+ path,
1620
+ offset: range.from,
1621
+ length: range.to - range.from
1622
+ });
1623
+ ops.push(marks && marks.length > 0 ? {
1624
+ type: "insert_text",
1625
+ path,
1626
+ offset,
1627
+ text,
1628
+ marks
1629
+ } : {
1630
+ type: "insert_text",
1631
+ path,
1632
+ offset,
1633
+ text
1634
+ });
1635
+ return ops;
1636
+ };
1637
+ }
1638
+ function isCollapsedSelection(sel) {
1639
+ return samePath(sel.anchor.path, sel.focus.path) && sel.anchor.offset === sel.focus.offset;
1640
+ }
1641
+ /**
1642
+ * Track-changes style backspace: mark the previous char as deleted instead of
1643
+ * removing it. Hard-deletes own `insertion` marks and already-marked deletions.
1644
+ */
1645
+ function softDeleteBackward(deletionMark) {
1646
+ return (state) => {
1647
+ if (!isCollapsedSelection(state.selection)) return deleteBackward(state);
1648
+ const path = textPath(state);
1649
+ const offset = state.selection.anchor.offset;
1650
+ if (offset <= 0) return deleteBackward(state);
1651
+ const { text, runs } = flattenTextForMarks(getNodeAt(state.doc, path));
1652
+ if (text.length === 0) return deleteBackward(state);
1653
+ const from = offset - 1;
1654
+ if (marksAtOffset(runs, from).some((m) => m.type === "insertion" || m.type === deletionMark.type)) return [{
1655
+ type: "delete_text",
1656
+ path,
1657
+ offset: from,
1658
+ length: 1
1659
+ }];
1660
+ return [{
1661
+ type: "set_mark",
1662
+ path,
1663
+ from,
1664
+ to: offset,
1665
+ mark: deletionMark
1666
+ }, {
1667
+ type: "set_selection",
1668
+ selection: collapsedAt(path, from)
1669
+ }];
1670
+ };
1671
+ }
1672
+ function flattenTextForMarks(paragraph) {
1673
+ let text = "";
1674
+ const runs = [];
1675
+ for (const child of paragraph.content ?? []) {
1676
+ if (child.type !== "text") continue;
1677
+ const start = text.length;
1678
+ const chunk = child.text ?? "";
1679
+ text += chunk;
1680
+ runs.push({
1681
+ start,
1682
+ end: text.length,
1683
+ marks: (child.marks ?? []).map((m) => ({ ...m }))
1684
+ });
1685
+ }
1686
+ return {
1687
+ text,
1688
+ runs
1689
+ };
1690
+ }
1691
+ function marksAtOffset(runs, index) {
1692
+ for (const run of runs) if (index >= run.start && index < run.end) return run.marks.map((m) => ({ ...m }));
1693
+ return [];
1694
+ }
1695
+ /** Resolve list-item path when caret is on the item or its first nested text block. */
1696
+ function listItemContext(doc, path) {
1697
+ let node;
1698
+ try {
1699
+ node = getNodeAt(doc, path);
1700
+ } catch {
1701
+ return null;
1702
+ }
1703
+ if (node.type === "listItem") {
1704
+ const itemIndex = path.at(-1) ?? 0;
1705
+ return {
1706
+ listItemPath: path,
1707
+ listPath: path.slice(0, -1),
1708
+ itemIndex
1709
+ };
1710
+ }
1711
+ if (path.length < 2) return null;
1712
+ const parentPath = path.slice(0, -1);
1713
+ try {
1714
+ if (getNodeAt(doc, parentPath).type === "listItem" && (path.at(-1) ?? 0) === 0) {
1715
+ const itemIndex = parentPath.at(-1) ?? 0;
1716
+ return {
1717
+ listItemPath: parentPath,
1718
+ listPath: parentPath.slice(0, -1),
1719
+ itemIndex
1720
+ };
1721
+ }
1722
+ } catch {}
1723
+ return null;
1724
+ }
1725
+ var deleteBackward = (state) => {
1726
+ const crossOrSame = deleteSelectionRangeOps(state.doc, state.selection);
1727
+ if (crossOrSame) {
1728
+ const range = selectionTextRange(state.selection);
1729
+ if (!range || range.from !== range.to) return crossOrSame;
1730
+ }
1731
+ const path = textPath(state);
1732
+ const offset = state.selection.anchor.offset;
1733
+ if (offset > 0) return [{
1734
+ type: "delete_text",
1735
+ path,
1736
+ offset: offset - 1,
1737
+ length: 1
1738
+ }];
1739
+ const listCtx = listItemContext(state.doc, path);
1740
+ if (listCtx) {
1741
+ const { listItemPath, listPath, itemIndex } = listCtx;
1742
+ if (itemIndex > 0) {
1743
+ const prevItemPath = [...listPath, itemIndex - 1];
1744
+ const leftItem = getNodeAt(state.doc, prevItemPath);
1745
+ const rightItem = getNodeAt(state.doc, listItemPath);
1746
+ if (isLeafTextBlock(leftItem) && isLeafTextBlock(rightItem)) return [{
1747
+ type: "merge_paragraph",
1748
+ path: listItemPath
1749
+ }];
1750
+ const ops = [];
1751
+ const leftCount = leftItem.content?.length ?? 0;
1752
+ const rightContent = rightItem.content ?? [];
1753
+ for (let i = 0; i < rightContent.length; i++) {
1754
+ const node = rightContent[i];
1755
+ if (node === void 0) continue;
1756
+ ops.push({
1757
+ type: "insert_node",
1758
+ path: prevItemPath,
1759
+ index: leftCount + i,
1760
+ node
1761
+ });
1762
+ }
1763
+ ops.push({
1764
+ type: "remove_node",
1765
+ path: listPath,
1766
+ index: itemIndex
1767
+ });
1768
+ const leftLast = leftCount > 0 ? leftItem.content?.[leftCount - 1] : void 0;
1769
+ const rightFirst = rightContent[0];
1770
+ if (leftLast !== void 0 && rightFirst !== void 0 && isLeafTextBlock(leftLast) && isLeafTextBlock(rightFirst)) {
1771
+ const joinAt = textLength(leftLast);
1772
+ ops.push({
1773
+ type: "merge_paragraph",
1774
+ path: [...prevItemPath, leftCount]
1775
+ }, {
1776
+ type: "set_selection",
1777
+ selection: collapsedAt([...prevItemPath, leftCount - 1], joinAt)
1778
+ });
1779
+ } else {
1780
+ const selPath = resolveTextPath(state.doc, prevItemPath);
1781
+ ops.push({
1782
+ type: "set_selection",
1783
+ selection: collapsedAt(selPath, textLength(getNodeAt(state.doc, selPath)))
1784
+ });
1785
+ }
1786
+ return ops;
1787
+ }
1788
+ const parentPath = listPath.slice(0, -1);
1789
+ const { ops, selectionPath, liftedIndex } = liftIntoParent(state.doc, listPath, itemIndex);
1790
+ if (liftedIndex > 0) try {
1791
+ const prev = getNodeAt(state.doc, [...parentPath, liftedIndex - 1]);
1792
+ if (isTextBlock(prev.type)) {
1793
+ const joinAt = textLength(prev);
1794
+ return [
1795
+ ...ops,
1796
+ {
1797
+ type: "merge_paragraph",
1798
+ path: [...parentPath, liftedIndex]
1799
+ },
1800
+ {
1801
+ type: "set_selection",
1802
+ selection: collapsedAt([...parentPath, liftedIndex - 1], joinAt)
1803
+ }
1804
+ ];
1805
+ }
1806
+ } catch {}
1807
+ return [...ops, {
1808
+ type: "set_selection",
1809
+ selection: collapsedAt(selectionPath, 0)
1810
+ }];
1811
+ }
1812
+ if (path.length >= 4) {
1813
+ const cellPath = path.slice(0, 3);
1814
+ try {
1815
+ if (getNodeAt(state.doc, cellPath).type === "tableCell" && (path.at(-1) ?? 0) === 0) return null;
1816
+ } catch {}
1817
+ }
1818
+ if (path.length === 1) {
1819
+ if (path[0] <= 0) return null;
1820
+ return [{
1821
+ type: "merge_paragraph",
1822
+ path
1823
+ }];
1824
+ }
1825
+ const index = path.at(-1);
1826
+ if (index === void 0 || index <= 0) return null;
1827
+ return [{
1828
+ type: "merge_paragraph",
1829
+ path
1830
+ }];
1831
+ };
1832
+ /** Delete one character forward of the caret (range → delete selection, incl. cross-block). */
1833
+ var deleteForward = (state) => {
1834
+ const crossOrSame = deleteSelectionRangeOps(state.doc, state.selection);
1835
+ if (crossOrSame) {
1836
+ const range = selectionTextRange(state.selection);
1837
+ if (!range || range.from !== range.to) return crossOrSame;
1838
+ }
1839
+ const path = textPath(state);
1840
+ const offset = state.selection.anchor.offset;
1841
+ let node;
1842
+ try {
1843
+ node = getNodeAt(state.doc, path);
1844
+ } catch {
1845
+ return null;
1846
+ }
1847
+ if (offset < textLength(node)) return [{
1848
+ type: "delete_text",
1849
+ path,
1850
+ offset,
1851
+ length: 1
1852
+ }];
1853
+ return null;
1854
+ };
1855
+ var splitBlock = (state) => {
1856
+ const path = textPath(state);
1857
+ const offset = state.selection.anchor.offset;
1858
+ let node;
1859
+ try {
1860
+ node = getNodeAt(state.doc, path);
1861
+ } catch {
1862
+ return null;
1863
+ }
1864
+ if (isListType(node.type) || node.type === "table" || node.type === "tableRow" || node.type === "tableCell") return null;
1865
+ const parentPath = path.slice(0, -1);
1866
+ let parentIsList = false;
1867
+ if (path.length >= 2) try {
1868
+ parentIsList = isListType(getNodeAt(state.doc, parentPath).type);
1869
+ } catch {
1870
+ parentIsList = false;
1871
+ }
1872
+ if (node.type === "listItem" || parentIsList && isTextBlock(node.type)) {
1873
+ if (plainText(node).length === 0) {
1874
+ const listPath = parentIsList ? parentPath : path.slice(0, -1);
1875
+ const itemIndex = path.at(-1) ?? 0;
1876
+ const { ops, selectionPath } = exitListItemOps(state.doc, listPath, itemIndex);
1877
+ return [...ops, {
1878
+ type: "set_selection",
1879
+ selection: collapsedAt(selectionPath, 0)
1880
+ }];
1881
+ }
1882
+ return [{
1883
+ type: "split_paragraph",
1884
+ path,
1885
+ offset
1886
+ }];
1887
+ }
1888
+ if (node.type === "heading" && offset >= plainText(node).length) {
1889
+ const headingParentPath = path.slice(0, -1);
1890
+ const index = (path.at(-1) ?? 0) + 1;
1891
+ return [{
1892
+ type: "insert_node",
1893
+ path: headingParentPath,
1894
+ index,
1895
+ node: createParagraph([createText("")])
1896
+ }, {
1897
+ type: "set_selection",
1898
+ selection: collapsedAt([...headingParentPath, index], 0)
1899
+ }];
1900
+ }
1901
+ if (isTextBlock(node.type)) return [{
1902
+ type: "split_paragraph",
1903
+ path,
1904
+ offset
1905
+ }];
1906
+ return null;
1907
+ };
1908
+ function toggleMark(markType) {
1909
+ return (state) => {
1910
+ const range = selectionTextRange(state.selection);
1911
+ if (!range || range.from === range.to) return null;
1912
+ if (rangeHasMark(getNodeAt(state.doc, range.path), range.from, range.to, markType)) return [{
1913
+ type: "remove_mark",
1914
+ path: range.path,
1915
+ from: range.from,
1916
+ to: range.to,
1917
+ markType
1918
+ }];
1919
+ return [{
1920
+ type: "set_mark",
1921
+ path: range.path,
1922
+ from: range.from,
1923
+ to: range.to,
1924
+ mark: { type: markType }
1925
+ }];
1926
+ };
1927
+ }
1928
+ /** Whether the current selection (or caret) carries `markType`. */
1929
+ function selectionHasMark(markType) {
1930
+ return (state) => {
1931
+ const range = selectionTextRange(state.selection);
1932
+ if (!range) return false;
1933
+ const para = getNodeAt(state.doc, range.path);
1934
+ if (range.from === range.to) return offsetHasMark(para, range.from > 0 ? range.from - 1 : range.from, markType);
1935
+ return rangeHasMark(para, range.from, range.to, markType);
1936
+ };
1937
+ }
1938
+ var selectAll = (state) => {
1939
+ const leaves = [];
1940
+ collectTextLeaves(state.doc, [], leaves);
1941
+ if (leaves.length === 0) return null;
1942
+ const first = leaves[0];
1943
+ const last = leaves.at(-1);
1944
+ if (first === void 0 || last === void 0) return null;
1945
+ const len = plainText(getNodeAt(state.doc, last)).length;
1946
+ return [{
1947
+ type: "set_selection",
1948
+ selection: {
1949
+ anchor: {
1950
+ path: first,
1951
+ offset: 0
1952
+ },
1953
+ focus: {
1954
+ path: last,
1955
+ offset: len
1956
+ }
1957
+ }
1958
+ }];
1959
+ };
1960
+ function runCommand(state, command) {
1961
+ const ops = command(state);
1962
+ if (!ops) return null;
1963
+ return transaction(...ops);
1964
+ }
1965
+ //#endregion
1966
+ export { JSON_DOC_VERSION, MAX_INSERT_CHARS, MAX_SLICE_KEEPS, MAX_TREE_DEPTH, applyOp, applyOps, applyTransaction, assertMark, assertMaxTreeDepth, assertNodeType, clampSelection, cloneNode, collapsedAt, comparePoints, copyAttrs, createDoc, createHistory, createList, createListItem, createParagraph, createSchema, createSelection, createState, createText, deepCloneNode, deleteBackward, deleteForward, deleteSelectionRangeOps, docFromJSON, docToJSON, emptyState, ensureNodeIds, exitListItemOps, getNodeAt, insertText, isCollapsed, isListType, isNormalizeIdempotent, isTextBlock, liftIntoParent, mergeMarksByType, nextId, normalize, offsetHasMark, orderedRange, plainText, rangeHasMark, rebaseOps, registerMark, registerNode, replaceAt, resetIdCounter, resolveTextPath, runCommand, sealSchema, selectAll, selectionHasMark, selectionTextRange, softDeleteBackward, splitBlock, textBlockLength, textLength, toggleMark, transaction, transformOp, transformOps };
1967
+
1968
+ //# sourceMappingURL=index.mjs.map