@blankdotpage/cake 0.1.68 → 0.1.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/cake/core/mapping/cursor-source-map.d.ts +11 -0
  2. package/dist/cake/core/mapping/cursor-source-map.d.ts.map +1 -1
  3. package/dist/cake/core/mapping/cursor-source-map.js +159 -21
  4. package/dist/cake/core/runtime.d.ts +4 -0
  5. package/dist/cake/core/runtime.d.ts.map +1 -1
  6. package/dist/cake/core/runtime.js +332 -215
  7. package/dist/cake/dom/render.d.ts +32 -2
  8. package/dist/cake/dom/render.d.ts.map +1 -1
  9. package/dist/cake/dom/render.js +401 -118
  10. package/dist/cake/editor/cake-editor.d.ts +8 -1
  11. package/dist/cake/editor/cake-editor.d.ts.map +1 -1
  12. package/dist/cake/editor/cake-editor.js +172 -100
  13. package/dist/cake/editor/internal/editor-text-model.d.ts +49 -0
  14. package/dist/cake/editor/internal/editor-text-model.d.ts.map +1 -0
  15. package/dist/cake/editor/internal/editor-text-model.js +284 -0
  16. package/dist/cake/editor/selection/selection-geometry-dom.d.ts +5 -1
  17. package/dist/cake/editor/selection/selection-geometry-dom.d.ts.map +1 -1
  18. package/dist/cake/editor/selection/selection-geometry-dom.js +4 -5
  19. package/dist/cake/editor/selection/selection-layout-dom.d.ts.map +1 -1
  20. package/dist/cake/editor/selection/selection-layout-dom.js +2 -5
  21. package/dist/cake/editor/selection/selection-layout.d.ts +2 -15
  22. package/dist/cake/editor/selection/selection-layout.d.ts.map +1 -1
  23. package/dist/cake/editor/selection/selection-layout.js +1 -99
  24. package/dist/cake/editor/selection/selection-navigation.d.ts +4 -0
  25. package/dist/cake/editor/selection/selection-navigation.d.ts.map +1 -1
  26. package/dist/cake/editor/selection/selection-navigation.js +1 -2
  27. package/dist/cake/extensions/link/link.d.ts.map +1 -1
  28. package/dist/cake/extensions/link/link.js +1 -7
  29. package/dist/cake/extensions/shared/structural-reparse-policy.js +2 -2
  30. package/package.json +5 -2
  31. package/dist/cake/editor/selection/visible-text.d.ts +0 -5
  32. package/dist/cake/editor/selection/visible-text.d.ts.map +0 -1
  33. package/dist/cake/editor/selection/visible-text.js +0 -66
  34. package/dist/cake/engine/cake-engine.d.ts +0 -230
  35. package/dist/cake/engine/cake-engine.d.ts.map +0 -1
  36. package/dist/cake/engine/cake-engine.js +0 -3589
  37. package/dist/cake/engine/selection/selection-geometry-dom.d.ts +0 -24
  38. package/dist/cake/engine/selection/selection-geometry-dom.d.ts.map +0 -1
  39. package/dist/cake/engine/selection/selection-geometry-dom.js +0 -302
  40. package/dist/cake/engine/selection/selection-geometry.d.ts +0 -22
  41. package/dist/cake/engine/selection/selection-geometry.d.ts.map +0 -1
  42. package/dist/cake/engine/selection/selection-geometry.js +0 -158
  43. package/dist/cake/engine/selection/selection-layout-dom.d.ts +0 -50
  44. package/dist/cake/engine/selection/selection-layout-dom.d.ts.map +0 -1
  45. package/dist/cake/engine/selection/selection-layout-dom.js +0 -781
  46. package/dist/cake/engine/selection/selection-layout.d.ts +0 -55
  47. package/dist/cake/engine/selection/selection-layout.d.ts.map +0 -1
  48. package/dist/cake/engine/selection/selection-layout.js +0 -128
  49. package/dist/cake/engine/selection/selection-navigation.d.ts +0 -22
  50. package/dist/cake/engine/selection/selection-navigation.d.ts.map +0 -1
  51. package/dist/cake/engine/selection/selection-navigation.js +0 -229
  52. package/dist/cake/engine/selection/visible-text.d.ts +0 -5
  53. package/dist/cake/engine/selection/visible-text.d.ts.map +0 -1
  54. package/dist/cake/engine/selection/visible-text.js +0 -66
  55. package/dist/cake/react/CakeEditor.d.ts +0 -58
  56. package/dist/cake/react/CakeEditor.d.ts.map +0 -1
  57. package/dist/cake/react/CakeEditor.js +0 -225
@@ -1,5 +1,6 @@
1
- import { CursorSourceBuilder, } from "./mapping/cursor-source-map";
1
+ import { CursorSourceBuilder, createCompositeCursorSourceMap, } from "./mapping/cursor-source-map";
2
2
  import { graphemeSegments } from "../shared/segmenter";
3
+ import { getEditorTextModelForDoc, } from "../editor/internal/editor-text-model";
3
4
  /** Type guard to check if a command is a structural edit */
4
5
  export function isStructuralEdit(command) {
5
6
  return (command.type === "insert" ||
@@ -149,19 +150,33 @@ export function createRuntimeForTests(extensions) {
149
150
  export function createRuntimeFromRegistry(registry) {
150
151
  const { toggleMarkerToSpec, inclusiveAtEndByKind, parseBlockFns, parseInlineFns, serializeBlockFns, serializeInlineFns, normalizeBlockFns, normalizeInlineFns, onEditFns, structuralReparsePolicies, domInlineRenderers, domBlockRenderers, } = registry;
151
152
  const isInclusiveAtEnd = (kind) => inclusiveAtEndByKind.get(kind) ?? true;
152
- const context = {
153
+ const removedBlockSentinel = Symbol("removed-block");
154
+ const removedInlineSentinel = Symbol("removed-inline");
155
+ const normalizedDocCache = new WeakMap();
156
+ const normalizedBlockCache = new WeakMap();
157
+ const normalizedInlineCache = new WeakMap();
158
+ const serializedDocCache = new WeakMap();
159
+ const serializedBlockCache = new WeakMap();
160
+ const serializedInlineCache = new WeakMap();
161
+ const segmentedDocCache = new WeakMap();
162
+ const emptyCursorMap = new CursorSourceBuilder().build().map;
163
+ const emptySerialized = {
164
+ source: "",
165
+ map: emptyCursorMap,
166
+ };
167
+ const extensionContext = {
153
168
  parseInline: (source, start, end) => parseInlineRange(source, start, end),
154
169
  serializeInline: (inline) => serializeInline(inline),
155
170
  serializeBlock: (block) => serializeBlock(block),
156
171
  };
157
172
  function parseBlockAt(source, start) {
158
173
  for (const parseBlock of parseBlockFns) {
159
- const result = parseBlock(source, start, context);
174
+ const result = parseBlock(source, start, extensionContext);
160
175
  if (result) {
161
176
  return result;
162
177
  }
163
178
  }
164
- return parseLiteralBlock(source, start, context);
179
+ return parseLiteralBlock(source, start, extensionContext);
165
180
  }
166
181
  function parseInlineRange(source, start, end) {
167
182
  const inlines = [];
@@ -169,7 +184,7 @@ export function createRuntimeFromRegistry(registry) {
169
184
  while (pos < end) {
170
185
  let matched = false;
171
186
  for (const parseInline of parseInlineFns) {
172
- const result = parseInline(source, pos, end, context);
187
+ const result = parseInline(source, pos, end, extensionContext);
173
188
  if (result) {
174
189
  inlines.push(result.inline);
175
190
  pos = result.nextPos;
@@ -205,6 +220,10 @@ export function createRuntimeFromRegistry(registry) {
205
220
  return { type: "doc", blocks };
206
221
  }
207
222
  function serialize(doc) {
223
+ const cached = serializedDocCache.get(doc);
224
+ if (cached) {
225
+ return cached;
226
+ }
208
227
  const builder = new CursorSourceBuilder();
209
228
  const blocks = doc.blocks;
210
229
  blocks.forEach((block, index) => {
@@ -214,41 +233,67 @@ export function createRuntimeFromRegistry(registry) {
214
233
  builder.appendText("\n");
215
234
  }
216
235
  });
217
- return builder.build();
236
+ const serialized = builder.build();
237
+ serializedDocCache.set(doc, serialized);
238
+ return serialized;
218
239
  }
219
240
  function serializeBlock(block) {
241
+ const cached = serializedBlockCache.get(block);
242
+ if (cached) {
243
+ return cached;
244
+ }
220
245
  for (const serializeBlockFn of serializeBlockFns) {
221
- const result = serializeBlockFn(block, context);
246
+ const result = serializeBlockFn(block, extensionContext);
222
247
  if (result) {
248
+ serializedBlockCache.set(block, result);
223
249
  return result;
224
250
  }
225
251
  }
226
252
  if (block.type === "paragraph") {
227
- return serializeParagraph(block, serializeInline);
253
+ const result = serializeParagraph(block, (inline) => serializeInline(inline));
254
+ serializedBlockCache.set(block, result);
255
+ return result;
228
256
  }
229
257
  if (block.type === "block-wrapper") {
230
- return serializeBlockWrapper(block, serializeBlock);
258
+ const result = serializeBlockWrapper(block, (child) => serializeBlock(child));
259
+ serializedBlockCache.set(block, result);
260
+ return result;
231
261
  }
232
- return { source: "", map: new CursorSourceBuilder().build().map };
262
+ serializedBlockCache.set(block, emptySerialized);
263
+ return emptySerialized;
233
264
  }
234
265
  function serializeInline(inline) {
266
+ const cached = serializedInlineCache.get(inline);
267
+ if (cached) {
268
+ return cached;
269
+ }
235
270
  for (const serializeInlineFn of serializeInlineFns) {
236
- const result = serializeInlineFn(inline, context);
271
+ const result = serializeInlineFn(inline, extensionContext);
237
272
  if (result) {
273
+ serializedInlineCache.set(inline, result);
238
274
  return result;
239
275
  }
240
276
  }
241
277
  if (inline.type === "text") {
242
278
  const builder = new CursorSourceBuilder();
243
279
  builder.appendText(inline.text);
244
- return builder.build();
280
+ const result = builder.build();
281
+ serializedInlineCache.set(inline, result);
282
+ return result;
245
283
  }
246
284
  if (inline.type === "inline-wrapper") {
247
- return serializeInlineWrapper(inline, serializeInline);
285
+ const result = serializeInlineWrapper(inline, (child) => serializeInline(child));
286
+ serializedInlineCache.set(inline, result);
287
+ return result;
248
288
  }
249
- return { source: "", map: new CursorSourceBuilder().build().map };
289
+ serializedInlineCache.set(inline, emptySerialized);
290
+ return emptySerialized;
250
291
  }
251
292
  function normalize(doc) {
293
+ const cached = normalizedDocCache.get(doc);
294
+ if (cached) {
295
+ return cached;
296
+ }
252
297
  let changed = false;
253
298
  const blocks = [];
254
299
  for (const block of doc.blocks) {
@@ -262,19 +307,28 @@ export function createRuntimeFromRegistry(registry) {
262
307
  }
263
308
  blocks.push(normalized);
264
309
  }
265
- if (!changed) {
266
- return doc;
310
+ const normalized = changed
311
+ ? {
312
+ type: "doc",
313
+ blocks,
314
+ }
315
+ : doc;
316
+ normalizedDocCache.set(doc, normalized);
317
+ if (normalized !== doc) {
318
+ normalizedDocCache.set(normalized, normalized);
267
319
  }
268
- return {
269
- type: "doc",
270
- blocks,
271
- };
320
+ return normalized;
272
321
  }
273
322
  function normalizeBlock(block) {
323
+ const cached = normalizedBlockCache.get(block);
324
+ if (cached !== undefined) {
325
+ return cached === removedBlockSentinel ? null : cached;
326
+ }
274
327
  let next = block;
275
328
  for (const normalizeBlockFn of normalizeBlockFns) {
276
329
  const result = normalizeBlockFn(next);
277
330
  if (result === null) {
331
+ normalizedBlockCache.set(block, removedBlockSentinel);
278
332
  return null;
279
333
  }
280
334
  next = result;
@@ -283,46 +337,53 @@ export function createRuntimeFromRegistry(registry) {
283
337
  let changed = next !== block;
284
338
  const content = [];
285
339
  for (const inline of next.content) {
286
- const normalized = normalizeInline(inline);
287
- if (normalized === null) {
340
+ const normalizedInline = normalizeInline(inline);
341
+ if (normalizedInline === null) {
288
342
  changed = true;
289
343
  continue;
290
344
  }
291
- if (normalized !== inline) {
345
+ if (normalizedInline !== inline) {
292
346
  changed = true;
293
347
  }
294
- content.push(normalized);
348
+ content.push(normalizedInline);
295
349
  }
296
350
  if (!changed) {
351
+ normalizedBlockCache.set(block, next);
297
352
  return next;
298
353
  }
299
- return {
354
+ const normalized = {
300
355
  ...next,
301
356
  content,
302
357
  };
358
+ normalizedBlockCache.set(block, normalized);
359
+ return normalized;
303
360
  }
304
361
  if (next.type === "block-wrapper") {
305
362
  let changed = next !== block;
306
363
  const blocks = [];
307
364
  for (const child of next.blocks) {
308
- const normalized = normalizeBlock(child);
309
- if (normalized === null) {
365
+ const normalizedChild = normalizeBlock(child);
366
+ if (normalizedChild === null) {
310
367
  changed = true;
311
368
  continue;
312
369
  }
313
- if (normalized !== child) {
370
+ if (normalizedChild !== child) {
314
371
  changed = true;
315
372
  }
316
- blocks.push(normalized);
373
+ blocks.push(normalizedChild);
317
374
  }
318
375
  if (!changed) {
376
+ normalizedBlockCache.set(block, next);
319
377
  return next;
320
378
  }
321
- return {
379
+ const normalized = {
322
380
  ...next,
323
381
  blocks,
324
382
  };
383
+ normalizedBlockCache.set(block, normalized);
384
+ return normalized;
325
385
  }
386
+ normalizedBlockCache.set(block, next);
326
387
  return next;
327
388
  }
328
389
  function applyInlineNormalizers(inline) {
@@ -337,8 +398,13 @@ export function createRuntimeFromRegistry(registry) {
337
398
  return next;
338
399
  }
339
400
  function normalizeInline(inline) {
401
+ const cached = normalizedInlineCache.get(inline);
402
+ if (cached !== undefined) {
403
+ return cached === removedInlineSentinel ? null : cached;
404
+ }
340
405
  const pre = applyInlineNormalizers(inline);
341
406
  if (!pre) {
407
+ normalizedInlineCache.set(inline, removedInlineSentinel);
342
408
  return null;
343
409
  }
344
410
  let next = pre;
@@ -350,31 +416,188 @@ export function createRuntimeFromRegistry(registry) {
350
416
  .filter((child) => child !== null),
351
417
  };
352
418
  }
353
- return applyInlineNormalizers(next);
419
+ const normalized = applyInlineNormalizers(next);
420
+ normalizedInlineCache.set(inline, normalized ?? removedInlineSentinel);
421
+ return normalized;
354
422
  }
355
- function createState(source, selection = defaultSelection) {
356
- const doc = parse(source);
357
- const normalized = normalize(doc);
358
- const serialized = serialize(normalized);
423
+ function createTopLevelBlockSegment(block) {
424
+ const serialized = serializeBlock(block);
359
425
  return {
426
+ block,
360
427
  source: serialized.source,
361
- selection,
362
428
  map: serialized.map,
363
- doc: normalized,
364
- runtime: runtime,
429
+ sourceLength: serialized.source.length,
430
+ cursorLength: serialized.map.cursorLength,
431
+ };
432
+ }
433
+ function hasSharedTopLevelBlockIdentity(previousDoc, nextDoc) {
434
+ if (previousDoc === nextDoc) {
435
+ return true;
436
+ }
437
+ if (previousDoc.blocks.length === 0 || nextDoc.blocks.length === 0) {
438
+ return false;
439
+ }
440
+ const previousBlocks = new Set(previousDoc.blocks);
441
+ return nextDoc.blocks.some((block) => previousBlocks.has(block));
442
+ }
443
+ function buildTopLevelSegments(doc, previous) {
444
+ const previousByBlock = new Map();
445
+ if (previous) {
446
+ for (const segment of previous.segments) {
447
+ previousByBlock.set(segment.block, segment);
448
+ }
449
+ }
450
+ return doc.blocks.map((block) => {
451
+ const reused = previousByBlock.get(block);
452
+ return reused ?? createTopLevelBlockSegment(block);
453
+ });
454
+ }
455
+ function buildPrefixIndexes(segments) {
456
+ const cursorStarts = [];
457
+ const sourceStarts = [];
458
+ let cursorOffset = 0;
459
+ let sourceOffset = 0;
460
+ for (let i = 0; i < segments.length; i += 1) {
461
+ const segment = segments[i];
462
+ cursorStarts.push(cursorOffset);
463
+ sourceStarts.push(sourceOffset);
464
+ cursorOffset += segment?.cursorLength ?? 0;
465
+ sourceOffset += segment?.sourceLength ?? 0;
466
+ if (i < segments.length - 1) {
467
+ cursorOffset += 1;
468
+ sourceOffset += 1;
469
+ }
470
+ }
471
+ return {
472
+ cursorStarts,
473
+ sourceStarts,
474
+ totalCursorLength: cursorOffset,
475
+ totalSourceLength: sourceOffset,
476
+ };
477
+ }
478
+ function serializeSegmentRange(segments, startIndex, endIndex) {
479
+ if (startIndex >= endIndex) {
480
+ return "";
481
+ }
482
+ let source = "";
483
+ for (let i = startIndex; i < endIndex; i += 1) {
484
+ const segment = segments[i];
485
+ if (!segment) {
486
+ continue;
487
+ }
488
+ source += segment.source;
489
+ if (i < segments.length - 1) {
490
+ source += "\n";
491
+ }
492
+ }
493
+ return source;
494
+ }
495
+ function sourceOffsetForBlockStart(state, blockIndex) {
496
+ if (blockIndex <= 0) {
497
+ return 0;
498
+ }
499
+ if (blockIndex >= state.segments.length) {
500
+ return state.source.length;
501
+ }
502
+ return state.sourceStarts[blockIndex] ?? state.source.length;
503
+ }
504
+ function buildSegmentedSource(segments, previous) {
505
+ if (!previous) {
506
+ return serializeSegmentRange(segments, 0, segments.length);
507
+ }
508
+ const previousSegments = previous.segments;
509
+ let prefix = 0;
510
+ const maxPrefix = Math.min(previousSegments.length, segments.length);
511
+ while (prefix < maxPrefix &&
512
+ previousSegments[prefix]?.block === segments[prefix]?.block) {
513
+ prefix += 1;
514
+ }
515
+ let suffix = 0;
516
+ const maxSuffix = Math.min(previousSegments.length - prefix, segments.length - prefix);
517
+ while (suffix < maxSuffix &&
518
+ previousSegments[previousSegments.length - 1 - suffix]?.block ===
519
+ segments[segments.length - 1 - suffix]?.block) {
520
+ suffix += 1;
521
+ }
522
+ if (prefix === previousSegments.length && prefix === segments.length) {
523
+ return previous.source;
524
+ }
525
+ const oldStart = sourceOffsetForBlockStart(previous, prefix);
526
+ const oldEnd = sourceOffsetForBlockStart(previous, previousSegments.length - suffix);
527
+ const middle = serializeSegmentRange(segments, prefix, segments.length - suffix);
528
+ return (previous.source.slice(0, oldStart) +
529
+ middle +
530
+ previous.source.slice(oldEnd));
531
+ }
532
+ function buildSegmentedDocState(doc, previous) {
533
+ const cached = segmentedDocCache.get(doc);
534
+ if (cached) {
535
+ return cached;
536
+ }
537
+ const segments = buildTopLevelSegments(doc, previous);
538
+ const { cursorStarts, sourceStarts, totalCursorLength, totalSourceLength, } = buildPrefixIndexes(segments);
539
+ const source = buildSegmentedSource(segments, previous);
540
+ const map = createCompositeCursorSourceMap({
541
+ segments: segments.map((segment) => ({
542
+ map: segment.map,
543
+ cursorLength: segment.cursorLength,
544
+ sourceLength: segment.sourceLength,
545
+ })),
546
+ cursorStarts,
547
+ sourceStarts,
548
+ cursorLength: totalCursorLength,
549
+ });
550
+ const segmented = {
551
+ doc,
552
+ segments,
553
+ cursorStarts,
554
+ sourceStarts,
555
+ totalCursorLength,
556
+ totalSourceLength,
557
+ source,
558
+ map,
365
559
  };
560
+ segmentedDocCache.set(doc, segmented);
561
+ return segmented;
366
562
  }
367
- function createStateFromDoc(doc, selection = defaultSelection) {
563
+ function buildStateFromDoc(doc, selection, options) {
368
564
  const normalized = normalize(doc);
369
- const serialized = serialize(normalized);
565
+ const mode = options?.mode ?? "incremental";
566
+ const previousSegmented = mode === "incremental" && options?.previousState
567
+ ? segmentedDocCache.get(options.previousState.doc)
568
+ : undefined;
569
+ const reusablePrevious = previousSegmented &&
570
+ hasSharedTopLevelBlockIdentity(previousSegmented.doc, normalized)
571
+ ? previousSegmented
572
+ : undefined;
573
+ const segmented = buildSegmentedDocState(normalized, reusablePrevious);
370
574
  return {
371
- source: serialized.source,
575
+ source: segmented.source,
372
576
  selection,
373
- map: serialized.map,
577
+ map: segmented.map,
374
578
  doc: normalized,
375
579
  runtime: runtime,
376
580
  };
377
581
  }
582
+ function createState(source, selection = defaultSelection) {
583
+ const doc = parse(source);
584
+ return buildStateFromDoc(doc, selection, { mode: "full" });
585
+ }
586
+ function createStateFromDoc(doc, selection = defaultSelection, options) {
587
+ return buildStateFromDoc(doc, selection, options);
588
+ }
589
+ function isIncrementalDerivationCandidate(command, selection) {
590
+ if (selection.start !== selection.end) {
591
+ return false;
592
+ }
593
+ if (command.type === "insert") {
594
+ return command.text.length > 0 && !command.text.includes("\n");
595
+ }
596
+ return (command.type === "delete-backward" ||
597
+ command.type === "delete-forward" ||
598
+ command.type === "insert-line-break" ||
599
+ command.type === "insert-hard-line-break");
600
+ }
378
601
  function shouldReparseAfterStructuralEdit(command) {
379
602
  if (structuralReparsePolicies.length === 0) {
380
603
  return true;
@@ -491,14 +714,19 @@ export function createRuntimeFromRegistry(registry) {
491
714
  }
492
715
  return state;
493
716
  }
494
- // Structural edits operate directly on the current doc tree. To support
495
- // markdown-first behavior while typing, we reparse from the resulting
496
- // source and then remap the caret through source space so it stays stable
497
- // even when marker characters become source-only.
498
- const interim = createStateFromDoc(structural.doc);
717
+ // Structural edits operate on the doc tree first. Common collapsed
718
+ // typing/edit flows stay on the incremental segmented path when policy
719
+ // allows; everything else takes the explicit full fallback path through
720
+ // source + parse.
721
+ const shouldReparse = shouldReparseAfterStructuralEdit(command);
722
+ const useIncrementalSegmentedDerivation = !shouldReparse && isIncrementalDerivationCandidate(command, selection);
723
+ const interim = createStateFromDoc(structural.doc, defaultSelection, {
724
+ mode: useIncrementalSegmentedDerivation ? "incremental" : "full",
725
+ previousState: useIncrementalSegmentedDerivation ? state : undefined,
726
+ });
499
727
  const interimAffinity = structural.nextAffinity ?? "forward";
500
728
  const caretSource = interim.map.cursorToSource(structural.nextCursor, interimAffinity);
501
- if (!shouldReparseAfterStructuralEdit(command)) {
729
+ if (useIncrementalSegmentedDerivation) {
502
730
  const caretCursor = interim.map.sourceToCursor(caretSource, interimAffinity);
503
731
  return {
504
732
  ...interim,
@@ -536,11 +764,12 @@ export function createRuntimeFromRegistry(registry) {
536
764
  return state;
537
765
  }
538
766
  function applyStructuralEdit(command, doc, selection) {
539
- const lines = flattenDocToLines(doc);
767
+ const textModel = getEditorTextModelForDoc(doc);
768
+ const lines = textModel.getStructuralLines();
540
769
  if (lines.length === 0) {
541
770
  return null;
542
771
  }
543
- const docCursorLength = cursorLengthForLines(lines);
772
+ const docCursorLength = textModel.getCursorLength();
544
773
  const cursorStart = Math.max(0, Math.min(docCursorLength, Math.min(selection.start, selection.end)));
545
774
  const cursorEnd = Math.max(0, Math.min(docCursorLength, Math.max(selection.start, selection.end)));
546
775
  const affinity = selection.affinity ?? "forward";
@@ -582,12 +811,12 @@ export function createRuntimeFromRegistry(registry) {
582
811
  const shouldReplacePlaceholder = command.type === "insert" &&
583
812
  replaceText.length > 0 &&
584
813
  range.start === range.end &&
585
- graphemeAtCursor(lines, range.start) === "\u200B";
814
+ textModel.getGraphemeAtCursor(range.start) === "\u200B";
586
815
  const effectiveRange = shouldReplacePlaceholder
587
816
  ? { start: range.start, end: Math.min(docCursorLength, range.start + 1) }
588
817
  : range;
589
- const startLoc = resolveCursorToLine(lines, effectiveRange.start);
590
- const endLoc = resolveCursorToLine(lines, effectiveRange.end);
818
+ const startLoc = textModel.resolveOffsetToLine(effectiveRange.start);
819
+ const endLoc = textModel.resolveOffsetToLine(effectiveRange.end);
591
820
  const startLine = lines[startLoc.lineIndex];
592
821
  const endLine = lines[endLoc.lineIndex];
593
822
  if (!startLine || !endLine) {
@@ -615,8 +844,9 @@ export function createRuntimeFromRegistry(registry) {
615
844
  let nextBlocks = updateBlocksAtPath(doc.blocks, prevLine.parentPath, (blocks) => blocks.map((block, index) => index === prevLine.indexInParent ? nextPrevBlock : block));
616
845
  nextBlocks = updateBlocksAtPath(nextBlocks, endLine.parentPath, (blocks) => blocks.filter((_, index) => index !== endLine.indexInParent));
617
846
  const nextDoc = { ...doc, blocks: nextBlocks };
618
- const nextLines = flattenDocToLines(nextDoc);
619
- const lineStarts = getLineStartOffsets(nextLines);
847
+ const nextModel = getEditorTextModelForDoc(nextDoc);
848
+ const nextLines = nextModel.getStructuralLines();
849
+ const lineStarts = nextModel.getLineOffsets();
620
850
  const mergedLineIndex = nextLines.findIndex((line) => pathsEqual(line.path, prevLine.path));
621
851
  const mergedLine = nextLines[mergedLineIndex];
622
852
  const nextCursor = mergedLineIndex >= 0
@@ -673,8 +903,9 @@ export function createRuntimeFromRegistry(registry) {
673
903
  ...doc,
674
904
  blocks: updateBlocksAtPath(doc.blocks, parentPath, () => nextParentBlocks),
675
905
  };
676
- const nextLines = flattenDocToLines(nextDoc);
677
- const lineStarts = getLineStartOffsets(nextLines);
906
+ const nextModel = getEditorTextModelForDoc(nextDoc);
907
+ const nextLines = nextModel.getStructuralLines();
908
+ const lineStarts = nextModel.getLineOffsets();
678
909
  const nextLineIndex = Math.min(nextLines.length - 1, startLoc.lineIndex + 1);
679
910
  return {
680
911
  doc: nextDoc,
@@ -693,8 +924,9 @@ export function createRuntimeFromRegistry(registry) {
693
924
  ...doc,
694
925
  blocks: updateBlocksAtPath(doc.blocks, parentPath, () => ensured),
695
926
  };
696
- const nextLines = flattenDocToLines(nextDoc);
697
- const lineStarts = getLineStartOffsets(nextLines);
927
+ const nextModel = getEditorTextModelForDoc(nextDoc);
928
+ const nextLines = nextModel.getStructuralLines();
929
+ const lineStarts = nextModel.getLineOffsets();
698
930
  const nextLineIndex = Math.min(nextLines.length - 1, startLoc.lineIndex);
699
931
  return {
700
932
  doc: nextDoc,
@@ -724,8 +956,9 @@ export function createRuntimeFromRegistry(registry) {
724
956
  ...doc,
725
957
  blocks: updateBlocksAtPath(doc.blocks, parentPath, () => nextParentBlocks),
726
958
  };
727
- const nextLines = flattenDocToLines(nextDoc);
728
- const lineStarts = getLineStartOffsets(nextLines);
959
+ const nextModel = getEditorTextModelForDoc(nextDoc);
960
+ const nextLines = nextModel.getStructuralLines();
961
+ const lineStarts = nextModel.getLineOffsets();
729
962
  const nextLineIndex = Math.max(0, startLoc.lineIndex - 1);
730
963
  return {
731
964
  doc: nextDoc,
@@ -782,8 +1015,9 @@ export function createRuntimeFromRegistry(registry) {
782
1015
  ...doc,
783
1016
  blocks: updateBlocksAtPath(doc.blocks, wrapperParentPath, () => nextParentBlocks),
784
1017
  };
785
- const nextLines = flattenDocToLines(nextDoc);
786
- const lineStarts = getLineStartOffsets(nextLines);
1018
+ const nextModel = getEditorTextModelForDoc(nextDoc);
1019
+ const nextLines = nextModel.getStructuralLines();
1020
+ const lineStarts = nextModel.getLineOffsets();
787
1021
  const insertedPath = [...wrapperParentPath, wrapperIndexInParent + 1];
788
1022
  const insertedLineIndex = nextLines.findIndex((line) => pathsEqual(line.path, insertedPath));
789
1023
  const nextCursor = insertedLineIndex >= 0 ? (lineStarts[insertedLineIndex] ?? 0) : 0;
@@ -796,12 +1030,12 @@ export function createRuntimeFromRegistry(registry) {
796
1030
  }
797
1031
  const hasSelectedText = effectiveRange.start !== effectiveRange.end;
798
1032
  const baseMarks = hasSelectedText
799
- ? commonMarksAcrossSelection(lines, effectiveRange.start, effectiveRange.end, doc)
1033
+ ? commonMarksAcrossSelection(textModel, lines, effectiveRange.start, effectiveRange.end)
800
1034
  : marksAtCursor(startRuns, startLoc.offsetInLine, affinity);
801
1035
  const insertDoc = parse(replaceText);
802
- const insertLines = flattenDocToLines(insertDoc);
1036
+ const insertModel = getEditorTextModelForDoc(insertDoc);
803
1037
  const insertBlocks = insertDoc.blocks;
804
- const insertCursorLength = cursorLengthForLines(insertLines);
1038
+ const insertCursorLength = insertModel.getCursorLength();
805
1039
  const replacementBlocks = buildReplacementBlocks({
806
1040
  baseMarks,
807
1041
  beforeRuns,
@@ -817,9 +1051,10 @@ export function createRuntimeFromRegistry(registry) {
817
1051
  ...doc,
818
1052
  blocks: updateBlocksAtPath(doc.blocks, parentPath, () => nextParentBlocks),
819
1053
  };
820
- const nextDocCursorLength = cursorLengthForLines(flattenDocToLines(nextDoc));
1054
+ const nextModel = getEditorTextModelForDoc(nextDoc);
1055
+ const nextDocCursorLength = nextModel.getCursorLength();
821
1056
  const nextCursor = Math.max(0, Math.min(nextDocCursorLength, effectiveRange.start + insertCursorLength));
822
- const nextLines = flattenDocToLines(nextDoc);
1057
+ const nextLines = nextModel.getStructuralLines();
823
1058
  const around = marksAroundCursor(nextDoc, nextCursor);
824
1059
  const fallbackAffinity = command.type === "delete-backward"
825
1060
  ? "backward"
@@ -836,8 +1071,8 @@ export function createRuntimeFromRegistry(registry) {
836
1071
  // If the cursor is at the start of a non-first line (i.e., right after a
837
1072
  // serialized newline), keep affinity "forward" so selection anchors in the
838
1073
  // following line, not at the end of the previous one.
839
- const nextLoc = resolveCursorToLine(nextLines, nextCursor);
840
- const lineStarts = getLineStartOffsets(nextLines);
1074
+ const nextLoc = nextModel.resolveOffsetToLine(nextCursor);
1075
+ const lineStarts = nextModel.getLineOffsets();
841
1076
  if (nextLoc.lineIndex > 0 &&
842
1077
  nextLoc.offsetInLine === 0 &&
843
1078
  nextCursor === (lineStarts[nextLoc.lineIndex] ?? 0)) {
@@ -849,128 +1084,6 @@ export function createRuntimeFromRegistry(registry) {
849
1084
  nextAffinity,
850
1085
  };
851
1086
  }
852
- function cursorLengthForLines(lines) {
853
- let length = 0;
854
- for (const line of lines) {
855
- length += line.cursorLength;
856
- if (line.hasNewline) {
857
- length += 1;
858
- }
859
- }
860
- return length;
861
- }
862
- function flattenDocToLines(doc) {
863
- const entries = [];
864
- const visit = (blocks, prefix) => {
865
- blocks.forEach((block, index) => {
866
- const path = [...prefix, index];
867
- if (block.type === "block-wrapper") {
868
- visit(block.blocks, path);
869
- return;
870
- }
871
- entries.push({ path, block });
872
- });
873
- };
874
- visit(doc.blocks, []);
875
- if (entries.length === 0) {
876
- return [
877
- {
878
- path: [0],
879
- parentPath: [],
880
- indexInParent: 0,
881
- block: { type: "paragraph", content: [] },
882
- text: "",
883
- cursorLength: 0,
884
- hasNewline: false,
885
- },
886
- ];
887
- }
888
- return entries.map((entry, i) => {
889
- const parentPath = entry.path.slice(0, -1);
890
- const indexInParent = entry.path[entry.path.length - 1] ?? 0;
891
- const text = blockVisibleText(entry.block);
892
- return {
893
- path: entry.path,
894
- parentPath,
895
- indexInParent,
896
- block: entry.block,
897
- text,
898
- cursorLength: graphemeSegments(text).length,
899
- hasNewline: i < entries.length - 1,
900
- };
901
- });
902
- }
903
- function resolveCursorToLine(lines, cursorOffset) {
904
- const total = cursorLengthForLines(lines);
905
- const clamped = Math.max(0, Math.min(cursorOffset, total));
906
- let start = 0;
907
- for (let i = 0; i < lines.length; i += 1) {
908
- const line = lines[i];
909
- const end = start + line.cursorLength;
910
- if (clamped <= end || i === lines.length - 1) {
911
- return {
912
- lineIndex: i,
913
- offsetInLine: Math.max(0, Math.min(clamped - start, line.cursorLength)),
914
- };
915
- }
916
- start = end + (line.hasNewline ? 1 : 0);
917
- if (line.hasNewline && clamped === start) {
918
- return {
919
- lineIndex: Math.min(lines.length - 1, i + 1),
920
- offsetInLine: 0,
921
- };
922
- }
923
- }
924
- return {
925
- lineIndex: lines.length - 1,
926
- offsetInLine: lines[lines.length - 1]?.cursorLength ?? 0,
927
- };
928
- }
929
- function getLineStartOffsets(lines) {
930
- const offsets = [];
931
- let current = 0;
932
- for (const line of lines) {
933
- offsets.push(current);
934
- current += line.cursorLength + (line.hasNewline ? 1 : 0);
935
- }
936
- return offsets;
937
- }
938
- function graphemeAtCursor(lines, cursorOffset) {
939
- const loc = resolveCursorToLine(lines, cursorOffset);
940
- const line = lines[loc.lineIndex];
941
- if (!line) {
942
- return null;
943
- }
944
- if (loc.offsetInLine >= line.cursorLength) {
945
- return null;
946
- }
947
- const segments = Array.from(graphemeSegments(line.text));
948
- return segments[loc.offsetInLine]?.segment ?? null;
949
- }
950
- function blockVisibleText(block) {
951
- if (block.type === "paragraph") {
952
- return block.content.map(inlineVisibleText).join("");
953
- }
954
- if (block.type === "block-atom") {
955
- return "";
956
- }
957
- if (block.type === "block-wrapper") {
958
- return block.blocks.map(blockVisibleText).join("\n");
959
- }
960
- return "";
961
- }
962
- function inlineVisibleText(inline) {
963
- if (inline.type === "text") {
964
- return inline.text;
965
- }
966
- if (inline.type === "inline-wrapper") {
967
- return inline.children.map(inlineVisibleText).join("");
968
- }
969
- if (inline.type === "inline-atom") {
970
- return " ";
971
- }
972
- return "";
973
- }
974
1087
  function stableStringify(value) {
975
1088
  if (value === null || value === undefined) {
976
1089
  return "";
@@ -1141,13 +1254,14 @@ export function createRuntimeFromRegistry(registry) {
1141
1254
  return null;
1142
1255
  }
1143
1256
  function marksAroundCursor(doc, cursorOffset) {
1144
- const lines = flattenDocToLines(doc);
1145
- const loc = resolveCursorToLine(lines, cursorOffset);
1257
+ const textModel = getEditorTextModelForDoc(doc);
1258
+ const lines = textModel.getStructuralLines();
1259
+ const loc = textModel.resolveOffsetToLine(cursorOffset);
1146
1260
  const line = lines[loc.lineIndex];
1147
1261
  if (!line) {
1148
1262
  return { left: [], right: [] };
1149
1263
  }
1150
- const block = getBlockAtPath(doc.blocks, line.path);
1264
+ const block = line.block;
1151
1265
  if (!block || block.type !== "paragraph") {
1152
1266
  return { left: [], right: [] };
1153
1267
  }
@@ -1321,19 +1435,19 @@ export function createRuntimeFromRegistry(registry) {
1321
1435
  });
1322
1436
  return blocks;
1323
1437
  }
1324
- function commonMarksAcrossSelection(lines, startCursor, endCursor, doc) {
1438
+ function commonMarksAcrossSelection(textModel, lines, startCursor, endCursor) {
1325
1439
  if (startCursor === endCursor) {
1326
1440
  return [];
1327
1441
  }
1328
- const startLoc = resolveCursorToLine(lines, startCursor);
1329
- const endLoc = resolveCursorToLine(lines, endCursor);
1442
+ const startLoc = textModel.resolveOffsetToLine(startCursor);
1443
+ const endLoc = textModel.resolveOffsetToLine(endCursor);
1330
1444
  const slices = [];
1331
1445
  for (let lineIndex = startLoc.lineIndex; lineIndex <= endLoc.lineIndex; lineIndex += 1) {
1332
1446
  const line = lines[lineIndex];
1333
1447
  if (!line) {
1334
1448
  continue;
1335
1449
  }
1336
- const block = getBlockAtPath(doc.blocks, line.path);
1450
+ const block = line.block;
1337
1451
  if (!block || block.type !== "paragraph") {
1338
1452
  continue;
1339
1453
  }
@@ -1545,8 +1659,7 @@ export function createRuntimeFromRegistry(registry) {
1545
1659
  // otherwise we create ambiguous marker sequences (e.g., *italic**​*** doesn't parse).
1546
1660
  const betweenLen = insertAtForward - insertAtBackward;
1547
1661
  const preferBackward = insertAtBackward !== insertAtForward && openLen <= betweenLen;
1548
- const insertAt = placeholderPos ??
1549
- (preferBackward ? insertAtBackward : insertAtForward);
1662
+ const insertAt = placeholderPos ?? (preferBackward ? insertAtBackward : insertAtForward);
1550
1663
  const nextSource = placeholderPos !== null
1551
1664
  ? source.slice(0, insertAt) +
1552
1665
  openMarker +
@@ -1572,9 +1685,10 @@ export function createRuntimeFromRegistry(registry) {
1572
1685
  }
1573
1686
  const cursorStart = Math.min(selection.start, selection.end);
1574
1687
  const cursorEnd = Math.max(selection.start, selection.end);
1575
- const linesForSelection = flattenDocToLines(state.doc);
1576
- const startLoc = resolveCursorToLine(linesForSelection, cursorStart);
1577
- const endLoc = resolveCursorToLine(linesForSelection, cursorEnd);
1688
+ const selectionModel = getEditorTextModelForDoc(state.doc);
1689
+ const linesForSelection = selectionModel.getStructuralLines();
1690
+ const startLoc = selectionModel.resolveOffsetToLine(cursorStart);
1691
+ const endLoc = selectionModel.resolveOffsetToLine(cursorEnd);
1578
1692
  const splitRunsOnNewlines = (runs) => {
1579
1693
  const split = [];
1580
1694
  for (const run of runs) {
@@ -1612,7 +1726,7 @@ export function createRuntimeFromRegistry(registry) {
1612
1726
  if (startInLine === endInLine) {
1613
1727
  continue;
1614
1728
  }
1615
- const block = getBlockAtPath(state.doc.blocks, line.path);
1729
+ const block = line.block;
1616
1730
  if (!block || block.type !== "paragraph") {
1617
1731
  continue;
1618
1732
  }
@@ -1749,22 +1863,23 @@ export function createRuntimeFromRegistry(registry) {
1749
1863
  }
1750
1864
  function serializeSelection(state, selection) {
1751
1865
  const normalized = normalizeSelection(selection);
1752
- const lines = flattenDocToLines(state.doc);
1753
- const docCursorLength = cursorLengthForLines(lines);
1866
+ const textModel = getEditorTextModelForDoc(state.doc);
1867
+ const lines = textModel.getStructuralLines();
1868
+ const docCursorLength = textModel.getCursorLength();
1754
1869
  const cursorStart = Math.max(0, Math.min(docCursorLength, Math.min(normalized.start, normalized.end)));
1755
1870
  const cursorEnd = Math.max(0, Math.min(docCursorLength, Math.max(normalized.start, normalized.end)));
1756
1871
  if (cursorStart === cursorEnd) {
1757
1872
  return "";
1758
1873
  }
1759
- const startLoc = resolveCursorToLine(lines, cursorStart);
1760
- const endLoc = resolveCursorToLine(lines, cursorEnd);
1874
+ const startLoc = textModel.resolveOffsetToLine(cursorStart);
1875
+ const endLoc = textModel.resolveOffsetToLine(cursorEnd);
1761
1876
  const blocks = [];
1762
1877
  for (let lineIndex = startLoc.lineIndex; lineIndex <= endLoc.lineIndex; lineIndex += 1) {
1763
1878
  const line = lines[lineIndex];
1764
1879
  if (!line) {
1765
1880
  continue;
1766
1881
  }
1767
- const block = getBlockAtPath(state.doc.blocks, line.path);
1882
+ const block = line.block;
1768
1883
  if (!block || block.type !== "paragraph") {
1769
1884
  continue;
1770
1885
  }
@@ -1833,15 +1948,16 @@ export function createRuntimeFromRegistry(registry) {
1833
1948
  }
1834
1949
  function serializeSelectionToHtml(state, selection) {
1835
1950
  const normalized = normalizeSelection(selection);
1836
- const lines = flattenDocToLines(state.doc);
1837
- const docCursorLength = cursorLengthForLines(lines);
1951
+ const textModel = getEditorTextModelForDoc(state.doc);
1952
+ const lines = textModel.getStructuralLines();
1953
+ const docCursorLength = textModel.getCursorLength();
1838
1954
  const cursorStart = Math.max(0, Math.min(docCursorLength, Math.min(normalized.start, normalized.end)));
1839
1955
  const cursorEnd = Math.max(0, Math.min(docCursorLength, Math.max(normalized.start, normalized.end)));
1840
1956
  if (cursorStart === cursorEnd) {
1841
1957
  return "";
1842
1958
  }
1843
- const startLoc = resolveCursorToLine(lines, cursorStart);
1844
- const endLoc = resolveCursorToLine(lines, cursorEnd);
1959
+ const startLoc = textModel.resolveOffsetToLine(cursorStart);
1960
+ const endLoc = textModel.resolveOffsetToLine(cursorEnd);
1845
1961
  let html = "";
1846
1962
  let activeList = null;
1847
1963
  const closeList = () => {
@@ -1865,7 +1981,7 @@ export function createRuntimeFromRegistry(registry) {
1865
1981
  if (!line) {
1866
1982
  continue;
1867
1983
  }
1868
- const block = getBlockAtPath(state.doc.blocks, line.path);
1984
+ const block = line.block;
1869
1985
  if (!block || block.type !== "paragraph") {
1870
1986
  continue;
1871
1987
  }
@@ -1948,6 +2064,7 @@ export function createRuntimeFromRegistry(registry) {
1948
2064
  parse,
1949
2065
  serialize,
1950
2066
  createState,
2067
+ createStateFromDoc,
1951
2068
  updateSelection,
1952
2069
  serializeSelection,
1953
2070
  serializeSelectionToHtml,