@blankdotpage/cake 0.1.76 → 0.1.78

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 (29) hide show
  1. package/dist/cake/clipboard.d.ts.map +1 -1
  2. package/dist/cake/clipboard.js +29 -0
  3. package/dist/cake/core/runtime.d.ts +32 -1
  4. package/dist/cake/core/runtime.d.ts.map +1 -1
  5. package/dist/cake/core/runtime.js +121 -513
  6. package/dist/cake/editor/cake-editor.d.ts +6 -1
  7. package/dist/cake/editor/cake-editor.d.ts.map +1 -1
  8. package/dist/cake/editor/cake-editor.js +18 -3
  9. package/dist/cake/extensions/blockquote/blockquote.d.ts.map +1 -1
  10. package/dist/cake/extensions/blockquote/blockquote.js +9 -0
  11. package/dist/cake/extensions/bold/bold.d.ts.map +1 -1
  12. package/dist/cake/extensions/bold/bold.js +9 -14
  13. package/dist/cake/extensions/heading/heading.d.ts.map +1 -1
  14. package/dist/cake/extensions/heading/heading.js +11 -1
  15. package/dist/cake/extensions/italic/italic.d.ts +1 -7
  16. package/dist/cake/extensions/italic/italic.d.ts.map +1 -1
  17. package/dist/cake/extensions/italic/italic.js +27 -91
  18. package/dist/cake/extensions/link/link.d.ts.map +1 -1
  19. package/dist/cake/extensions/link/link.js +7 -0
  20. package/dist/cake/extensions/list/list.d.ts.map +1 -1
  21. package/dist/cake/extensions/list/list.js +52 -0
  22. package/dist/cake/extensions/strikethrough/strikethrough.d.ts.map +1 -1
  23. package/dist/cake/extensions/strikethrough/strikethrough.js +6 -0
  24. package/dist/cake/extensions/underline/underline.d.ts.map +1 -1
  25. package/dist/cake/extensions/underline/underline.js +6 -0
  26. package/dist/cake/react/index.d.ts +0 -1
  27. package/dist/cake/react/index.d.ts.map +1 -1
  28. package/dist/cake/react/index.js +1 -5
  29. package/package.json +1 -1
@@ -19,7 +19,6 @@ export function isApplyEditCommand(command) {
19
19
  command.type === "delete-forward");
20
20
  }
21
21
  const defaultSelection = { start: 0, end: 0, affinity: "forward" };
22
- const WORD_CHARACTER_PATTERN = /[\p{L}\p{N}_]/u;
23
22
  function removeFromArray(arr, value) {
24
23
  const index = arr.indexOf(value);
25
24
  if (index === -1) {
@@ -40,6 +39,8 @@ export function createRuntimeForTests(extensions) {
40
39
  const structuralReparsePolicies = [];
41
40
  const domInlineRenderers = [];
42
41
  const domBlockRenderers = [];
42
+ const inlineHtmlSerializers = [];
43
+ const serializeSelectionLineToHtmlFns = [];
43
44
  const editor = {
44
45
  registerInlineWrapperAffinity: (specs) => {
45
46
  for (const spec of specs) {
@@ -125,6 +126,14 @@ export function createRuntimeForTests(extensions) {
125
126
  domBlockRenderers.push(fn);
126
127
  return () => removeFromArray(domBlockRenderers, fn);
127
128
  },
129
+ registerInlineHtmlSerializer: (fn) => {
130
+ inlineHtmlSerializers.push(fn);
131
+ return () => removeFromArray(inlineHtmlSerializers, fn);
132
+ },
133
+ registerSerializeSelectionLineToHtml: (fn) => {
134
+ serializeSelectionLineToHtmlFns.push(fn);
135
+ return () => removeFromArray(serializeSelectionLineToHtmlFns, fn);
136
+ },
128
137
  registerUI: () => {
129
138
  return () => { };
130
139
  },
@@ -145,11 +154,13 @@ export function createRuntimeForTests(extensions) {
145
154
  structuralReparsePolicies,
146
155
  domInlineRenderers,
147
156
  domBlockRenderers,
157
+ inlineHtmlSerializers,
158
+ serializeSelectionLineToHtmlFns,
148
159
  });
149
160
  return runtime;
150
161
  }
151
162
  export function createRuntimeFromRegistry(registry) {
152
- const { toggleMarkerToSpec, inclusiveAtEndByKind, parseBlockFns, parseInlineFns, serializeBlockFns, serializeInlineFns, normalizeBlockFns, normalizeInlineFns, onEditFns, structuralReparsePolicies, domInlineRenderers, domBlockRenderers, } = registry;
163
+ const { toggleMarkerToSpec, inclusiveAtEndByKind, parseBlockFns, parseInlineFns, serializeBlockFns, serializeInlineFns, normalizeBlockFns, normalizeInlineFns, onEditFns, structuralReparsePolicies, domInlineRenderers, domBlockRenderers, inlineHtmlSerializers, serializeSelectionLineToHtmlFns, } = registry;
153
164
  const isInclusiveAtEnd = (kind) => inclusiveAtEndByKind.get(kind) ?? true;
154
165
  const removedBlockSentinel = Symbol("removed-block");
155
166
  const removedInlineSentinel = Symbol("removed-inline");
@@ -348,17 +359,13 @@ export function createRuntimeFromRegistry(registry) {
348
359
  }
349
360
  content.push(normalizedInline);
350
361
  }
351
- const mergedContent = mergeAdjacentInlines(content);
352
- if (mergedContent !== content) {
353
- changed = true;
354
- }
355
362
  if (!changed) {
356
363
  normalizedBlockCache.set(block, next);
357
364
  return next;
358
365
  }
359
366
  const normalized = {
360
367
  ...next,
361
- content: mergedContent,
368
+ content,
362
369
  };
363
370
  normalizedBlockCache.set(block, normalized);
364
371
  return normalized;
@@ -414,52 +421,17 @@ export function createRuntimeFromRegistry(registry) {
414
421
  }
415
422
  let next = pre;
416
423
  if (next.type === "inline-wrapper") {
417
- const children = mergeAdjacentInlines(next.children
418
- .map((child) => normalizeInline(child))
419
- .filter((child) => child !== null));
420
424
  next = {
421
425
  ...next,
422
- children,
426
+ children: next.children
427
+ .map((child) => normalizeInline(child))
428
+ .filter((child) => child !== null),
423
429
  };
424
430
  }
425
431
  const normalized = applyInlineNormalizers(next);
426
432
  normalizedInlineCache.set(inline, normalized ?? removedInlineSentinel);
427
433
  return normalized;
428
434
  }
429
- function mergeAdjacentInlines(inlines) {
430
- if (inlines.length < 2) {
431
- return inlines;
432
- }
433
- const merged = [];
434
- let changed = false;
435
- for (const inline of inlines) {
436
- const previous = merged[merged.length - 1];
437
- if (previous?.type === "text" && inline.type === "text") {
438
- merged[merged.length - 1] = {
439
- ...previous,
440
- text: previous.text + inline.text,
441
- };
442
- changed = true;
443
- continue;
444
- }
445
- if (previous?.type === "inline-wrapper" &&
446
- inline.type === "inline-wrapper" &&
447
- previous.kind === inline.kind &&
448
- stableStringify(previous.data) === stableStringify(inline.data)) {
449
- merged[merged.length - 1] = {
450
- ...previous,
451
- children: mergeAdjacentInlines([
452
- ...previous.children,
453
- ...inline.children,
454
- ]),
455
- };
456
- changed = true;
457
- continue;
458
- }
459
- merged.push(inline);
460
- }
461
- return changed ? merged : inlines;
462
- }
463
435
  function createTopLevelBlockSegment(block) {
464
436
  const serialized = serializeBlock(block);
465
437
  return {
@@ -611,15 +583,9 @@ export function createRuntimeFromRegistry(registry) {
611
583
  ? previousSegmented
612
584
  : undefined;
613
585
  const segmented = buildSegmentedDocState(normalized, reusablePrevious);
614
- const cursorLength = segmented.map.cursorLength;
615
- const clampedSelection = {
616
- ...selection,
617
- start: Math.max(0, Math.min(cursorLength, selection.start)),
618
- end: Math.max(0, Math.min(cursorLength, selection.end)),
619
- };
620
586
  return {
621
587
  source: segmented.source,
622
- selection: normalizeSelection(clampedSelection),
588
+ selection,
623
589
  map: segmented.map,
624
590
  doc: normalized,
625
591
  runtime: runtime,
@@ -771,8 +737,7 @@ export function createRuntimeFromRegistry(registry) {
771
737
  previousState: useIncrementalSegmentedDerivation ? state : undefined,
772
738
  });
773
739
  const interimAffinity = structural.nextAffinity ?? "forward";
774
- const interimCursor = Math.max(0, Math.min(interim.map.cursorLength, structural.nextCursor));
775
- const caretSource = interim.map.cursorToSource(interimCursor, interimAffinity);
740
+ const caretSource = interim.map.cursorToSource(structural.nextCursor, interimAffinity);
776
741
  if (useIncrementalSegmentedDerivation) {
777
742
  const caretCursor = interim.map.sourceToCursor(caretSource, interimAffinity);
778
743
  return {
@@ -859,27 +824,6 @@ export function createRuntimeFromRegistry(registry) {
859
824
  replaceText.length > 0 &&
860
825
  range.start === range.end &&
861
826
  textModel.getGraphemeAtCursor(range.start) === "\u200B";
862
- if (command.type === "insert" && shouldReplacePlaceholder) {
863
- const leadingWhitespace = replaceText.match(/^\s+/)?.[0] ?? "";
864
- const around = marksAroundCursor(doc, range.start);
865
- if (leadingWhitespace.length > 0) {
866
- if (isMarksPrefix(around.left, around.right) &&
867
- around.right.length > around.left.length) {
868
- const whitespaceInsert = insertTextBeforePendingPlaceholderInDoc(doc, range.start, leadingWhitespace, around.left);
869
- if (whitespaceInsert) {
870
- const trailingText = replaceText.slice(leadingWhitespace.length);
871
- if (trailingText.length === 0) {
872
- return whitespaceInsert;
873
- }
874
- return applyStructuralEdit({ type: "insert", text: trailingText }, whitespaceInsert.doc, {
875
- start: whitespaceInsert.nextCursor,
876
- end: whitespaceInsert.nextCursor,
877
- affinity: whitespaceInsert.nextAffinity,
878
- });
879
- }
880
- }
881
- }
882
- }
883
827
  const effectiveRange = shouldReplacePlaceholder
884
828
  ? { start: range.start, end: Math.min(docCursorLength, range.start + 1) }
885
829
  : range;
@@ -1232,28 +1176,6 @@ export function createRuntimeFromRegistry(registry) {
1232
1176
  }
1233
1177
  return true;
1234
1178
  }
1235
- function removeMarkByKind(marks, kind) {
1236
- let removed = false;
1237
- return marks.filter((mark) => {
1238
- if (!removed && mark.kind === kind) {
1239
- removed = true;
1240
- return false;
1241
- }
1242
- return true;
1243
- });
1244
- }
1245
- function mergeMarksPreservingOrder(...groups) {
1246
- const next = [];
1247
- for (const group of groups) {
1248
- for (const mark of group) {
1249
- if (next.some((existing) => existing.key === mark.key)) {
1250
- continue;
1251
- }
1252
- next.push(mark);
1253
- }
1254
- }
1255
- return next;
1256
- }
1257
1179
  function sliceRuns(runs, startCursor, endCursor) {
1258
1180
  const [left, rest] = splitRunsAt(runs, startCursor);
1259
1181
  const [selected, right] = splitRunsAt(rest, Math.max(0, endCursor - startCursor));
@@ -1362,237 +1284,6 @@ export function createRuntimeFromRegistry(registry) {
1362
1284
  const right = marksAtGraphemeIndex(runs, loc.offsetInLine);
1363
1285
  return { left: left ?? [], right: right ?? [] };
1364
1286
  }
1365
- function createPendingPlaceholderStateAtCursor(state, cursorOffset, marks) {
1366
- const textModel = getEditorTextModelForDoc(state.doc);
1367
- const lines = textModel.getStructuralLines();
1368
- const loc = textModel.resolveOffsetToLine(cursorOffset);
1369
- const line = lines[loc.lineIndex];
1370
- if (!line) {
1371
- return null;
1372
- }
1373
- const block = getBlockAtPath(state.doc.blocks, line.path);
1374
- if (!block || block.type !== "paragraph") {
1375
- return null;
1376
- }
1377
- const placeholder = "\u200B";
1378
- const runs = paragraphToRuns(block);
1379
- const { before, after } = sliceRuns(runs, loc.offsetInLine, loc.offsetInLine);
1380
- const mergedRuns = normalizeRuns([
1381
- ...before,
1382
- { type: "text", text: placeholder, marks },
1383
- ...after,
1384
- ]);
1385
- const nextBlock = {
1386
- ...block,
1387
- content: runsToInlines(mergedRuns),
1388
- };
1389
- const parentPath = line.path.slice(0, -1);
1390
- const indexInParent = line.path[line.path.length - 1] ?? 0;
1391
- const nextDoc = {
1392
- ...state.doc,
1393
- blocks: updateBlocksAtPath(state.doc.blocks, parentPath, (blocks) => blocks.map((child, index) => index === indexInParent ? nextBlock : child)),
1394
- };
1395
- const next = createStateFromDoc(nextDoc);
1396
- const sourceHint = state.map.cursorToSource(cursorOffset, "backward");
1397
- const searchStart = Math.max(0, sourceHint - 4);
1398
- const placeholderStart = next.source.indexOf(placeholder, searchStart) ?? -1;
1399
- const resolvedPlaceholderStart = placeholderStart !== -1 ? placeholderStart : next.source.indexOf(placeholder);
1400
- if (resolvedPlaceholderStart === -1) {
1401
- return null;
1402
- }
1403
- const startCursor = next.map.sourceToCursor(resolvedPlaceholderStart, "forward");
1404
- return {
1405
- ...next,
1406
- selection: {
1407
- start: startCursor.cursorOffset,
1408
- end: startCursor.cursorOffset,
1409
- affinity: "forward",
1410
- },
1411
- };
1412
- }
1413
- function rewritePendingPlaceholderAtCursor(state, cursorOffset, marks) {
1414
- const textModel = getEditorTextModelForDoc(state.doc);
1415
- const lines = textModel.getStructuralLines();
1416
- const loc = textModel.resolveOffsetToLine(cursorOffset);
1417
- const line = lines[loc.lineIndex];
1418
- if (!line) {
1419
- return null;
1420
- }
1421
- const block = getBlockAtPath(state.doc.blocks, line.path);
1422
- if (!block || block.type !== "paragraph") {
1423
- return null;
1424
- }
1425
- const placeholder = "\u200B";
1426
- const runs = paragraphToRuns(block);
1427
- const { before, after } = sliceRuns(runs, loc.offsetInLine, loc.offsetInLine);
1428
- const replacement = [];
1429
- const firstAfter = after[0];
1430
- if (firstAfter?.type === "text" && firstAfter.text.startsWith(placeholder)) {
1431
- if (marks && marks.length > 0) {
1432
- replacement.push({ type: "text", text: placeholder, marks });
1433
- }
1434
- if (firstAfter.text.length > placeholder.length) {
1435
- replacement.push({
1436
- ...firstAfter,
1437
- text: firstAfter.text.slice(placeholder.length),
1438
- });
1439
- }
1440
- replacement.push(...after.slice(1));
1441
- }
1442
- else {
1443
- const lastBefore = before[before.length - 1];
1444
- if (lastBefore?.type !== "text" ||
1445
- !lastBefore.text.endsWith(placeholder)) {
1446
- return null;
1447
- }
1448
- const prefix = lastBefore.text.slice(0, -placeholder.length);
1449
- if (prefix) {
1450
- replacement.push({ ...lastBefore, text: prefix });
1451
- }
1452
- if (marks && marks.length > 0) {
1453
- replacement.push({ type: "text", text: placeholder, marks });
1454
- }
1455
- replacement.push(...after);
1456
- before.pop();
1457
- }
1458
- const mergedRuns = normalizeRuns([...before, ...replacement]);
1459
- const nextBlock = {
1460
- ...block,
1461
- content: runsToInlines(mergedRuns),
1462
- };
1463
- const parentPath = line.path.slice(0, -1);
1464
- const indexInParent = line.path[line.path.length - 1] ?? 0;
1465
- const nextDoc = {
1466
- ...state.doc,
1467
- blocks: updateBlocksAtPath(state.doc.blocks, parentPath, (blocks) => blocks.map((child, index) => index === indexInParent ? nextBlock : child)),
1468
- };
1469
- const next = createStateFromDoc(nextDoc);
1470
- return {
1471
- ...next,
1472
- selection: {
1473
- start: cursorOffset,
1474
- end: cursorOffset,
1475
- affinity: marks && marks.length > 0 ? "forward" : "backward",
1476
- },
1477
- };
1478
- }
1479
- function updatePendingPlaceholderMarksAtCursor(state, cursorOffset, marks) {
1480
- return rewritePendingPlaceholderAtCursor(state, cursorOffset, marks);
1481
- }
1482
- function removePendingPlaceholderAtCursor(state, cursorOffset) {
1483
- return rewritePendingPlaceholderAtCursor(state, cursorOffset, null);
1484
- }
1485
- function getPendingPlaceholderMarksAtCursor(state, cursorOffset) {
1486
- const textModel = getEditorTextModelForDoc(state.doc);
1487
- const lines = textModel.getStructuralLines();
1488
- const loc = textModel.resolveOffsetToLine(cursorOffset);
1489
- const line = lines[loc.lineIndex];
1490
- if (!line) {
1491
- return null;
1492
- }
1493
- const block = getBlockAtPath(state.doc.blocks, line.path);
1494
- if (!block || block.type !== "paragraph") {
1495
- return null;
1496
- }
1497
- const placeholder = "\u200B";
1498
- const runs = paragraphToRuns(block);
1499
- const { before, after } = sliceRuns(runs, loc.offsetInLine, loc.offsetInLine);
1500
- const firstAfter = after[0];
1501
- if (firstAfter?.type === "text" && firstAfter.text.startsWith(placeholder)) {
1502
- return firstAfter.marks;
1503
- }
1504
- const lastBefore = before[before.length - 1];
1505
- if (lastBefore?.type === "text" &&
1506
- lastBefore.text.endsWith(placeholder)) {
1507
- return lastBefore.marks;
1508
- }
1509
- return null;
1510
- }
1511
- function insertTextBeforePendingPlaceholderInDoc(doc, cursorOffset, text, marks) {
1512
- const textModel = getEditorTextModelForDoc(doc);
1513
- const lines = textModel.getStructuralLines();
1514
- const loc = textModel.resolveOffsetToLine(cursorOffset);
1515
- const line = lines[loc.lineIndex];
1516
- if (!line) {
1517
- return null;
1518
- }
1519
- const block = getBlockAtPath(doc.blocks, line.path);
1520
- if (!block || block.type !== "paragraph") {
1521
- return null;
1522
- }
1523
- const placeholder = "\u200B";
1524
- const runs = paragraphToRuns(block);
1525
- const { before, after } = sliceRuns(runs, loc.offsetInLine, loc.offsetInLine);
1526
- const firstAfter = after[0];
1527
- if (firstAfter?.type !== "text" ||
1528
- !firstAfter.text.startsWith(placeholder)) {
1529
- return null;
1530
- }
1531
- const mergedRuns = normalizeRuns([
1532
- ...before,
1533
- ...(text.length > 0 ? [{ type: "text", text, marks }] : []),
1534
- firstAfter,
1535
- ...after.slice(1),
1536
- ]);
1537
- const nextBlock = {
1538
- ...block,
1539
- content: runsToInlines(mergedRuns),
1540
- };
1541
- const parentPath = line.path.slice(0, -1);
1542
- const indexInParent = line.path[line.path.length - 1] ?? 0;
1543
- const nextDoc = {
1544
- ...doc,
1545
- blocks: updateBlocksAtPath(doc.blocks, parentPath, (blocks) => blocks.map((child, index) => index === indexInParent ? nextBlock : child)),
1546
- };
1547
- return {
1548
- doc: nextDoc,
1549
- nextCursor: cursorOffset + Array.from(graphemeSegments(text)).length,
1550
- nextAffinity: "forward",
1551
- };
1552
- }
1553
- function hasInlineMarkerBoundaryBefore(source, markerStart) {
1554
- if (markerStart <= 0) {
1555
- return true;
1556
- }
1557
- return !WORD_CHARACTER_PATTERN.test(source[markerStart - 1] ?? "");
1558
- }
1559
- function pickSafeCollapsedToggleMarkerSpec(params) {
1560
- const { defaultSpec, source, insertAt, affinity } = params;
1561
- const candidates = Array.from(toggleMarkerToSpec.values()).filter((spec, index, all) => spec.kind === defaultSpec.kind &&
1562
- all.findIndex((candidate) => candidate.kind === spec.kind &&
1563
- candidate.open === spec.open &&
1564
- candidate.close === spec.close) === index);
1565
- if (candidates.length <= 1) {
1566
- return defaultSpec;
1567
- }
1568
- const previousChar = source[insertAt - 1] ?? "";
1569
- const nextChar = source[insertAt] ?? "";
1570
- let bestSpec = defaultSpec;
1571
- let bestScore = Number.POSITIVE_INFINITY;
1572
- for (const spec of candidates) {
1573
- if (spec.open === "_" &&
1574
- !hasInlineMarkerBoundaryBefore(source, insertAt)) {
1575
- continue;
1576
- }
1577
- let score = 0;
1578
- if (previousChar && spec.open[0] === previousChar) {
1579
- score += affinity === "forward" ? 8 : 3;
1580
- }
1581
- if (nextChar &&
1582
- spec.close[spec.close.length - 1] === nextChar) {
1583
- score += affinity === "backward" ? 8 : 3;
1584
- }
1585
- if (spec.open === defaultSpec.open &&
1586
- spec.close === defaultSpec.close) {
1587
- score -= 0.5;
1588
- }
1589
- if (score < bestScore) {
1590
- bestSpec = spec;
1591
- bestScore = score;
1592
- }
1593
- }
1594
- return bestSpec;
1595
- }
1596
1287
  function preferredAffinityAtGap(left, right, fallback) {
1597
1288
  if (isMarksPrefix(left, right) && right.length > left.length) {
1598
1289
  return "forward";
@@ -1845,49 +1536,51 @@ export function createRuntimeFromRegistry(registry) {
1845
1536
  const openLen = openMarker.length;
1846
1537
  const closeLen = closeMarker.length;
1847
1538
  const placeholder = "\u200B";
1848
- const markerMark = {
1849
- kind: markerKind,
1850
- data: undefined,
1851
- key: markKey(markerKind, undefined),
1852
- };
1853
1539
  if (selection.start === selection.end) {
1854
1540
  const caret = selection.start;
1855
- const pendingPlaceholderMarks = getPendingPlaceholderMarksAtCursor(state, caret);
1856
- if (pendingPlaceholderMarks) {
1857
- const hasMarker = pendingPlaceholderMarks.some((mark) => mark.kind === markerKind);
1858
- const around = marksAroundCursor(state.doc, caret);
1859
- const nextMarks = hasMarker
1860
- ? removeMarkByKind(pendingPlaceholderMarks, markerKind)
1861
- : mergeMarksPreservingOrder(around.left, pendingPlaceholderMarks, [markerMark]);
1862
- const next = nextMarks.length > 0
1863
- ? updatePendingPlaceholderMarksAtCursor(state, caret, nextMarks)
1864
- : removePendingPlaceholderAtCursor(state, caret);
1865
- if (next) {
1866
- return {
1867
- ...next,
1868
- selection: {
1869
- start: caret,
1870
- end: caret,
1871
- affinity: "forward",
1872
- },
1873
- };
1874
- }
1875
- }
1876
1541
  // When the caret is at the end boundary of an inline wrapper, toggling the
1877
1542
  // wrapper should "exit" it (so the next character types outside). This is
1878
1543
  // best expressed in cursor space by flipping affinity to "forward" when we
1879
1544
  // are leaving a wrapper of the requested kind.
1880
1545
  const around = marksAroundCursor(state.doc, caret);
1881
1546
  if (isMarksPrefix(around.right, around.left) &&
1882
- around.left.length > around.right.length &&
1883
- (selection.affinity ?? "forward") === "backward") {
1547
+ around.left.length > around.right.length) {
1884
1548
  const exiting = around.left.slice(around.right.length);
1885
1549
  if (exiting.some((mark) => mark.kind === markerKind)) {
1886
- const remainingMarks = removeMarkByKind(around.left, markerKind);
1887
- if (!marksEqual(remainingMarks, around.right)) {
1888
- const next = createPendingPlaceholderStateAtCursor(state, caret, remainingMarks);
1889
- if (next) {
1890
- return next;
1550
+ const toggledExitingIndex = exiting.findIndex((mark) => mark.kind === markerKind);
1551
+ if (exiting.length > 1 &&
1552
+ toggledExitingIndex !== -1 &&
1553
+ toggledExitingIndex < exiting.length - 1) {
1554
+ return {
1555
+ ...state,
1556
+ selection: {
1557
+ start: caret,
1558
+ end: caret,
1559
+ affinity: "forward",
1560
+ },
1561
+ };
1562
+ }
1563
+ if (exiting.length > 1) {
1564
+ const insertAtForward = map.cursorToSource(caret, "forward");
1565
+ const insertAtBackward = map.cursorToSource(caret, "backward");
1566
+ const between = source.slice(insertAtBackward, insertAtForward);
1567
+ const markerIndex = between.indexOf(closeMarker);
1568
+ if (markerIndex !== -1) {
1569
+ const insertAt = insertAtBackward + markerIndex + closeLen;
1570
+ const nextSource = source.slice(0, insertAt) +
1571
+ placeholder +
1572
+ source.slice(insertAt);
1573
+ const next = createState(nextSource);
1574
+ const placeholderStart = insertAt;
1575
+ const startCursor = next.map.sourceToCursor(placeholderStart, "forward");
1576
+ return {
1577
+ ...next,
1578
+ selection: {
1579
+ start: startCursor.cursorOffset,
1580
+ end: startCursor.cursorOffset,
1581
+ affinity: "forward",
1582
+ },
1583
+ };
1891
1584
  }
1892
1585
  }
1893
1586
  return {
@@ -1907,20 +1600,6 @@ export function createRuntimeFromRegistry(registry) {
1907
1600
  around.right.length > around.left.length) {
1908
1601
  const entering = around.right.slice(around.left.length);
1909
1602
  if (entering.some((mark) => mark.kind === markerKind)) {
1910
- const remainingMarks = removeMarkByKind(around.right, markerKind);
1911
- const next = remainingMarks.length > 0
1912
- ? updatePendingPlaceholderMarksAtCursor(state, caret, remainingMarks)
1913
- : removePendingPlaceholderAtCursor(state, caret);
1914
- if (next) {
1915
- return {
1916
- ...next,
1917
- selection: {
1918
- start: caret,
1919
- end: caret,
1920
- affinity: "backward",
1921
- },
1922
- };
1923
- }
1924
1603
  const insertAtBackward = map.cursorToSource(caret, "backward");
1925
1604
  const insertAtForward = map.cursorToSource(caret, "forward");
1926
1605
  const after = source.slice(insertAtBackward);
@@ -1979,18 +1658,6 @@ export function createRuntimeFromRegistry(registry) {
1979
1658
  }
1980
1659
  }
1981
1660
  }
1982
- if (isMarksPrefix(around.right, around.left) &&
1983
- around.left.length > around.right.length &&
1984
- (selection.affinity ?? "forward") === "backward" &&
1985
- !around.left.some((mark) => mark.kind === markerKind)) {
1986
- const next = createPendingPlaceholderStateAtCursor(state, caret, [
1987
- ...around.left,
1988
- markerMark,
1989
- ]);
1990
- if (next) {
1991
- return next;
1992
- }
1993
- }
1994
1661
  // Otherwise, insert an empty marker pair with a zero-width placeholder
1995
1662
  // selected so the next typed character replaces it.
1996
1663
  //
@@ -2012,63 +1679,34 @@ export function createRuntimeFromRegistry(registry) {
2012
1679
  return null;
2013
1680
  })();
2014
1681
  // When at a boundary between cursor positions (insertAtBackward !== insertAtForward),
2015
- // only prefer insertAtBackward if the caret is intentionally anchored inside the
2016
- // left formatting context. If the caret affinity is forward, the user explicitly
2017
- // exited that wrapper and new markers belong on the forward side of the gap.
2018
- // Still guard against inserting a longer marker into a shorter boundary run,
2019
- // which would create ambiguous source (e.g., *italic**​***).
1682
+ // prefer insertAtBackward to keep new markers inside the current formatting context.
1683
+ // However, only do this if the new marker length is <= the boundary marker length,
1684
+ // otherwise we create ambiguous marker sequences (e.g., *italic**​*** doesn't parse).
2020
1685
  const betweenLen = insertAtForward - insertAtBackward;
2021
- const preferBackward = insertAtBackward !== insertAtForward &&
2022
- (selection.affinity ?? "forward") === "backward" &&
2023
- openLen <= betweenLen;
1686
+ const preferBackward = insertAtBackward !== insertAtForward && openLen <= betweenLen;
2024
1687
  const insertAt = placeholderPos ?? (preferBackward ? insertAtBackward : insertAtForward);
2025
- const insertMarkerSpec = placeholderPos === null
2026
- ? pickSafeCollapsedToggleMarkerSpec({
2027
- defaultSpec: markerSpec,
2028
- source,
2029
- insertAt,
2030
- affinity: selection.affinity ?? "forward",
2031
- })
2032
- : markerSpec;
2033
- const insertOpenMarker = insertMarkerSpec.open;
2034
- const insertCloseMarker = insertMarkerSpec.close;
2035
- const insertOpenLen = insertOpenMarker.length;
2036
- const baseMarks = (selection.affinity ?? "forward") === "backward"
2037
- ? around.left
2038
- : around.right;
2039
- const nextMarks = [
2040
- ...baseMarks.filter((mark) => mark.kind !== markerKind),
2041
- markerMark,
2042
- ];
2043
- if (placeholderPos !== null) {
2044
- const next = updatePendingPlaceholderMarksAtCursor(state, caret, nextMarks);
2045
- if (next) {
2046
- return next;
2047
- }
2048
- }
2049
- const docInserted = createPendingPlaceholderStateAtCursor(state, caret, nextMarks);
2050
- if (docInserted) {
2051
- return docInserted;
2052
- }
2053
1688
  const nextSource = placeholderPos !== null
2054
1689
  ? source.slice(0, insertAt) +
2055
- insertOpenMarker +
1690
+ openMarker +
2056
1691
  placeholder +
2057
- insertCloseMarker +
1692
+ closeMarker +
2058
1693
  source.slice(insertAt + placeholder.length)
2059
1694
  : source.slice(0, insertAt) +
2060
- insertOpenMarker +
1695
+ openMarker +
2061
1696
  placeholder +
2062
- insertCloseMarker +
1697
+ closeMarker +
2063
1698
  source.slice(insertAt);
2064
1699
  const next = createState(nextSource);
2065
- const placeholderStart = insertAt + insertOpenLen;
1700
+ const placeholderStart = insertAt + openLen;
2066
1701
  const startCursor = next.map.sourceToCursor(placeholderStart, "forward");
2067
- return createStateFromDoc(next.doc, {
2068
- start: startCursor.cursorOffset,
2069
- end: startCursor.cursorOffset,
2070
- affinity: "forward",
2071
- });
1702
+ return {
1703
+ ...next,
1704
+ selection: {
1705
+ start: startCursor.cursorOffset,
1706
+ end: startCursor.cursorOffset,
1707
+ affinity: "forward",
1708
+ },
1709
+ };
2072
1710
  }
2073
1711
  const cursorStart = Math.min(selection.start, selection.end);
2074
1712
  const cursorEnd = Math.max(selection.start, selection.end);
@@ -2130,6 +1768,11 @@ export function createRuntimeFromRegistry(registry) {
2130
1768
  const hasTargetMark = visibleRunsForDecision.some((run) => run.marks.some((mark) => mark.kind === markerKind));
2131
1769
  const canUnwrap = hasTargetMark &&
2132
1770
  visibleRunsForDecision.every((run) => run.marks.some((mark) => mark.kind === markerKind));
1771
+ const markerMark = {
1772
+ kind: markerKind,
1773
+ data: undefined,
1774
+ key: markKey(markerKind, undefined),
1775
+ };
2133
1776
  const removeMark = (marks) => {
2134
1777
  if (!marks.some((mark) => mark.kind === markerKind)) {
2135
1778
  return marks;
@@ -2310,18 +1953,12 @@ export function createRuntimeFromRegistry(registry) {
2310
1953
  // Apply marks in reverse order so outer marks wrap inner marks
2311
1954
  const sortedMarks = [...run.marks].reverse();
2312
1955
  for (const mark of sortedMarks) {
2313
- if (mark.kind === "bold") {
2314
- content = `<strong>${content}</strong>`;
2315
- }
2316
- else if (mark.kind === "italic") {
2317
- content = `<em>${content}</em>`;
2318
- }
2319
- else if (mark.kind === "strikethrough") {
2320
- content = `<s>${content}</s>`;
2321
- }
2322
- else if (mark.kind === "link") {
2323
- const url = mark.data?.url ?? "";
2324
- content = `<a href="${escapeHtml(url)}">${content}</a>`;
1956
+ for (const serializeMarkToHtml of inlineHtmlSerializers) {
1957
+ const next = serializeMarkToHtml({ kind: mark.kind, data: mark.data }, content, { escapeHtml });
1958
+ if (next !== null) {
1959
+ content = next;
1960
+ break;
1961
+ }
2325
1962
  }
2326
1963
  }
2327
1964
  html += content;
@@ -2341,22 +1978,13 @@ export function createRuntimeFromRegistry(registry) {
2341
1978
  const startLoc = textModel.resolveOffsetToLine(cursorStart);
2342
1979
  const endLoc = textModel.resolveOffsetToLine(cursorEnd);
2343
1980
  let html = "";
2344
- let activeList = null;
2345
- const closeList = () => {
2346
- if (activeList) {
2347
- html += `</${activeList.type}>`;
2348
- activeList = null;
2349
- }
2350
- };
2351
- const openList = (type, indent) => {
2352
- if (activeList &&
2353
- activeList.type === type &&
2354
- activeList.indent === indent) {
1981
+ let activeGroup = null;
1982
+ const closeGroup = () => {
1983
+ if (!activeGroup) {
2355
1984
  return;
2356
1985
  }
2357
- closeList();
2358
- html += `<${type}>`;
2359
- activeList = { type, indent };
1986
+ html += activeGroup.close;
1987
+ activeGroup = null;
2360
1988
  };
2361
1989
  for (let lineIndex = startLoc.lineIndex; lineIndex <= endLoc.lineIndex; lineIndex += 1) {
2362
1990
  const line = lines[lineIndex];
@@ -2373,66 +2001,46 @@ export function createRuntimeFromRegistry(registry) {
2373
2001
  ? endLoc.offsetInLine
2374
2002
  : line.cursorLength;
2375
2003
  const selectedRuns = sliceRuns(runs, startInLine, endInLine).selected;
2376
- // Check if this line is inside a block-wrapper (heading or list)
2377
- let wrapperKind = null;
2378
- let wrapperData;
2004
+ const lineHtml = runsToHtml(normalizeRuns(selectedRuns));
2005
+ const lineText = runs
2006
+ .map((r) => (r.type === "text" ? r.text : " "))
2007
+ .join("");
2008
+ let wrapperBlock = null;
2379
2009
  if (line.path.length > 1) {
2380
2010
  const wrapperPath = line.path.slice(0, -1);
2381
2011
  const wrapper = getBlockAtPath(state.doc.blocks, wrapperPath);
2382
2012
  if (wrapper && wrapper.type === "block-wrapper") {
2383
- wrapperKind = wrapper.kind;
2384
- wrapperData = wrapper.data;
2013
+ wrapperBlock = wrapper;
2385
2014
  }
2386
2015
  }
2387
- // Extract plain text to check for list patterns
2388
- const plainText = runs
2389
- .map((r) => (r.type === "text" ? r.text : " "))
2390
- .join("");
2391
- const listMatch = plainText.match(/^(\s*)([-*+]|\d+\.)( )(.*)$/);
2392
- // Determine the HTML content - strip list prefix if it's a list line
2393
- let lineHtml;
2394
- if (listMatch && !wrapperKind) {
2395
- // For list lines, only include the content after the prefix
2396
- const prefixLength = Array.from(graphemeSegments(`${listMatch[1]}${listMatch[2]}${listMatch[3]}`)).length;
2397
- const contentRuns = sliceRuns(runs, prefixLength, runs.reduce((sum, r) => sum +
2398
- (r.type === "text"
2399
- ? Array.from(graphemeSegments(r.text)).length
2400
- : 1), 0)).selected;
2401
- lineHtml = runsToHtml(normalizeRuns(contentRuns));
2402
- }
2403
- else {
2404
- lineHtml = runsToHtml(normalizeRuns(selectedRuns));
2405
- }
2406
- if (wrapperKind === "heading") {
2407
- closeList();
2408
- const level = Math.min(wrapperData?.level ?? 1, 6);
2409
- html += `<h${level} style="margin:0">${lineHtml}</h${level}>`;
2410
- }
2411
- else if (wrapperKind === "bullet-list") {
2412
- openList("ul", 0);
2413
- html += `<li>${lineHtml}</li>`;
2414
- }
2415
- else if (wrapperKind === "numbered-list") {
2416
- openList("ol", 0);
2417
- html += `<li>${lineHtml}</li>`;
2418
- }
2419
- else if (wrapperKind === "blockquote") {
2420
- closeList();
2421
- html += `<blockquote>${lineHtml}</blockquote>`;
2422
- }
2423
- else if (listMatch) {
2424
- // Plain paragraph with list markers (cake v3 list model)
2425
- const isNumbered = /^\d+\.$/.test(listMatch[2]);
2426
- const indent = Math.floor(listMatch[1].length / 2);
2427
- openList(isNumbered ? "ol" : "ul", indent);
2428
- html += `<li>${lineHtml}</li>`;
2429
- }
2430
- else {
2431
- closeList();
2432
- html += `<div>${lineHtml}</div>`;
2433
- }
2434
- }
2435
- closeList();
2016
+ let lineResult = null;
2017
+ for (const serializeLineToHtml of serializeSelectionLineToHtmlFns) {
2018
+ lineResult = serializeLineToHtml({
2019
+ state,
2020
+ line,
2021
+ block,
2022
+ wrapperBlock,
2023
+ lineText,
2024
+ startInLine,
2025
+ endInLine,
2026
+ lineCursorLength: line.cursorLength,
2027
+ selectedHtml: lineHtml,
2028
+ });
2029
+ if (lineResult) {
2030
+ break;
2031
+ }
2032
+ }
2033
+ const group = lineResult?.group ?? null;
2034
+ if (!group || !activeGroup || activeGroup.key !== group.key) {
2035
+ closeGroup();
2036
+ if (group) {
2037
+ html += group.open;
2038
+ activeGroup = group;
2039
+ }
2040
+ }
2041
+ html += lineResult?.html ?? `<div>${lineHtml}</div>`;
2042
+ }
2043
+ closeGroup();
2436
2044
  if (!html) {
2437
2045
  return "";
2438
2046
  }