@blankdotpage/cake 0.1.75 → 0.1.77
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/cake/clipboard.d.ts.map +1 -1
- package/dist/cake/clipboard.js +29 -0
- package/dist/cake/core/runtime.d.ts +32 -1
- package/dist/cake/core/runtime.d.ts.map +1 -1
- package/dist/cake/core/runtime.js +400 -117
- package/dist/cake/editor/cake-editor.d.ts +5 -1
- package/dist/cake/editor/cake-editor.d.ts.map +1 -1
- package/dist/cake/editor/cake-editor.js +12 -0
- package/dist/cake/extensions/blockquote/blockquote.d.ts.map +1 -1
- package/dist/cake/extensions/blockquote/blockquote.js +9 -0
- package/dist/cake/extensions/bold/bold.d.ts.map +1 -1
- package/dist/cake/extensions/bold/bold.js +14 -3
- package/dist/cake/extensions/heading/heading.d.ts.map +1 -1
- package/dist/cake/extensions/heading/heading.js +11 -1
- package/dist/cake/extensions/italic/italic.d.ts.map +1 -1
- package/dist/cake/extensions/italic/italic.js +78 -10
- package/dist/cake/extensions/link/link.d.ts.map +1 -1
- package/dist/cake/extensions/link/link.js +7 -0
- package/dist/cake/extensions/list/list.d.ts.map +1 -1
- package/dist/cake/extensions/list/list.js +52 -0
- package/dist/cake/extensions/strikethrough/strikethrough.d.ts.map +1 -1
- package/dist/cake/extensions/strikethrough/strikethrough.js +6 -0
- package/dist/cake/extensions/underline/underline.d.ts.map +1 -1
- package/dist/cake/extensions/underline/underline.js +6 -0
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ 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;
|
|
22
23
|
function removeFromArray(arr, value) {
|
|
23
24
|
const index = arr.indexOf(value);
|
|
24
25
|
if (index === -1) {
|
|
@@ -39,6 +40,8 @@ export function createRuntimeForTests(extensions) {
|
|
|
39
40
|
const structuralReparsePolicies = [];
|
|
40
41
|
const domInlineRenderers = [];
|
|
41
42
|
const domBlockRenderers = [];
|
|
43
|
+
const inlineHtmlSerializers = [];
|
|
44
|
+
const serializeSelectionLineToHtmlFns = [];
|
|
42
45
|
const editor = {
|
|
43
46
|
registerInlineWrapperAffinity: (specs) => {
|
|
44
47
|
for (const spec of specs) {
|
|
@@ -124,6 +127,14 @@ export function createRuntimeForTests(extensions) {
|
|
|
124
127
|
domBlockRenderers.push(fn);
|
|
125
128
|
return () => removeFromArray(domBlockRenderers, fn);
|
|
126
129
|
},
|
|
130
|
+
registerInlineHtmlSerializer: (fn) => {
|
|
131
|
+
inlineHtmlSerializers.push(fn);
|
|
132
|
+
return () => removeFromArray(inlineHtmlSerializers, fn);
|
|
133
|
+
},
|
|
134
|
+
registerSerializeSelectionLineToHtml: (fn) => {
|
|
135
|
+
serializeSelectionLineToHtmlFns.push(fn);
|
|
136
|
+
return () => removeFromArray(serializeSelectionLineToHtmlFns, fn);
|
|
137
|
+
},
|
|
127
138
|
registerUI: () => {
|
|
128
139
|
return () => { };
|
|
129
140
|
},
|
|
@@ -144,11 +155,13 @@ export function createRuntimeForTests(extensions) {
|
|
|
144
155
|
structuralReparsePolicies,
|
|
145
156
|
domInlineRenderers,
|
|
146
157
|
domBlockRenderers,
|
|
158
|
+
inlineHtmlSerializers,
|
|
159
|
+
serializeSelectionLineToHtmlFns,
|
|
147
160
|
});
|
|
148
161
|
return runtime;
|
|
149
162
|
}
|
|
150
163
|
export function createRuntimeFromRegistry(registry) {
|
|
151
|
-
const { toggleMarkerToSpec, inclusiveAtEndByKind, parseBlockFns, parseInlineFns, serializeBlockFns, serializeInlineFns, normalizeBlockFns, normalizeInlineFns, onEditFns, structuralReparsePolicies, domInlineRenderers, domBlockRenderers, } = registry;
|
|
164
|
+
const { toggleMarkerToSpec, inclusiveAtEndByKind, parseBlockFns, parseInlineFns, serializeBlockFns, serializeInlineFns, normalizeBlockFns, normalizeInlineFns, onEditFns, structuralReparsePolicies, domInlineRenderers, domBlockRenderers, inlineHtmlSerializers, serializeSelectionLineToHtmlFns, } = registry;
|
|
152
165
|
const isInclusiveAtEnd = (kind) => inclusiveAtEndByKind.get(kind) ?? true;
|
|
153
166
|
const removedBlockSentinel = Symbol("removed-block");
|
|
154
167
|
const removedInlineSentinel = Symbol("removed-inline");
|
|
@@ -347,13 +360,17 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
347
360
|
}
|
|
348
361
|
content.push(normalizedInline);
|
|
349
362
|
}
|
|
363
|
+
const mergedContent = mergeAdjacentInlines(content);
|
|
364
|
+
if (mergedContent !== content) {
|
|
365
|
+
changed = true;
|
|
366
|
+
}
|
|
350
367
|
if (!changed) {
|
|
351
368
|
normalizedBlockCache.set(block, next);
|
|
352
369
|
return next;
|
|
353
370
|
}
|
|
354
371
|
const normalized = {
|
|
355
372
|
...next,
|
|
356
|
-
content,
|
|
373
|
+
content: mergedContent,
|
|
357
374
|
};
|
|
358
375
|
normalizedBlockCache.set(block, normalized);
|
|
359
376
|
return normalized;
|
|
@@ -409,17 +426,52 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
409
426
|
}
|
|
410
427
|
let next = pre;
|
|
411
428
|
if (next.type === "inline-wrapper") {
|
|
429
|
+
const children = mergeAdjacentInlines(next.children
|
|
430
|
+
.map((child) => normalizeInline(child))
|
|
431
|
+
.filter((child) => child !== null));
|
|
412
432
|
next = {
|
|
413
433
|
...next,
|
|
414
|
-
children
|
|
415
|
-
.map((child) => normalizeInline(child))
|
|
416
|
-
.filter((child) => child !== null),
|
|
434
|
+
children,
|
|
417
435
|
};
|
|
418
436
|
}
|
|
419
437
|
const normalized = applyInlineNormalizers(next);
|
|
420
438
|
normalizedInlineCache.set(inline, normalized ?? removedInlineSentinel);
|
|
421
439
|
return normalized;
|
|
422
440
|
}
|
|
441
|
+
function mergeAdjacentInlines(inlines) {
|
|
442
|
+
if (inlines.length < 2) {
|
|
443
|
+
return inlines;
|
|
444
|
+
}
|
|
445
|
+
const merged = [];
|
|
446
|
+
let changed = false;
|
|
447
|
+
for (const inline of inlines) {
|
|
448
|
+
const previous = merged[merged.length - 1];
|
|
449
|
+
if (previous?.type === "text" && inline.type === "text") {
|
|
450
|
+
merged[merged.length - 1] = {
|
|
451
|
+
...previous,
|
|
452
|
+
text: previous.text + inline.text,
|
|
453
|
+
};
|
|
454
|
+
changed = true;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (previous?.type === "inline-wrapper" &&
|
|
458
|
+
inline.type === "inline-wrapper" &&
|
|
459
|
+
previous.kind === inline.kind &&
|
|
460
|
+
stableStringify(previous.data) === stableStringify(inline.data)) {
|
|
461
|
+
merged[merged.length - 1] = {
|
|
462
|
+
...previous,
|
|
463
|
+
children: mergeAdjacentInlines([
|
|
464
|
+
...previous.children,
|
|
465
|
+
...inline.children,
|
|
466
|
+
]),
|
|
467
|
+
};
|
|
468
|
+
changed = true;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
merged.push(inline);
|
|
472
|
+
}
|
|
473
|
+
return changed ? merged : inlines;
|
|
474
|
+
}
|
|
423
475
|
function createTopLevelBlockSegment(block) {
|
|
424
476
|
const serialized = serializeBlock(block);
|
|
425
477
|
return {
|
|
@@ -571,9 +623,15 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
571
623
|
? previousSegmented
|
|
572
624
|
: undefined;
|
|
573
625
|
const segmented = buildSegmentedDocState(normalized, reusablePrevious);
|
|
626
|
+
const cursorLength = segmented.map.cursorLength;
|
|
627
|
+
const clampedSelection = {
|
|
628
|
+
...selection,
|
|
629
|
+
start: Math.max(0, Math.min(cursorLength, selection.start)),
|
|
630
|
+
end: Math.max(0, Math.min(cursorLength, selection.end)),
|
|
631
|
+
};
|
|
574
632
|
return {
|
|
575
633
|
source: segmented.source,
|
|
576
|
-
selection,
|
|
634
|
+
selection: normalizeSelection(clampedSelection),
|
|
577
635
|
map: segmented.map,
|
|
578
636
|
doc: normalized,
|
|
579
637
|
runtime: runtime,
|
|
@@ -725,7 +783,8 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
725
783
|
previousState: useIncrementalSegmentedDerivation ? state : undefined,
|
|
726
784
|
});
|
|
727
785
|
const interimAffinity = structural.nextAffinity ?? "forward";
|
|
728
|
-
const
|
|
786
|
+
const interimCursor = Math.max(0, Math.min(interim.map.cursorLength, structural.nextCursor));
|
|
787
|
+
const caretSource = interim.map.cursorToSource(interimCursor, interimAffinity);
|
|
729
788
|
if (useIncrementalSegmentedDerivation) {
|
|
730
789
|
const caretCursor = interim.map.sourceToCursor(caretSource, interimAffinity);
|
|
731
790
|
return {
|
|
@@ -812,6 +871,27 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
812
871
|
replaceText.length > 0 &&
|
|
813
872
|
range.start === range.end &&
|
|
814
873
|
textModel.getGraphemeAtCursor(range.start) === "\u200B";
|
|
874
|
+
if (command.type === "insert" && shouldReplacePlaceholder) {
|
|
875
|
+
const leadingWhitespace = replaceText.match(/^\s+/)?.[0] ?? "";
|
|
876
|
+
const around = marksAroundCursor(doc, range.start);
|
|
877
|
+
if (leadingWhitespace.length > 0) {
|
|
878
|
+
if (isMarksPrefix(around.left, around.right) &&
|
|
879
|
+
around.right.length > around.left.length) {
|
|
880
|
+
const whitespaceInsert = insertTextBeforePendingPlaceholderInDoc(doc, range.start, leadingWhitespace, around.left);
|
|
881
|
+
if (whitespaceInsert) {
|
|
882
|
+
const trailingText = replaceText.slice(leadingWhitespace.length);
|
|
883
|
+
if (trailingText.length === 0) {
|
|
884
|
+
return whitespaceInsert;
|
|
885
|
+
}
|
|
886
|
+
return applyStructuralEdit({ type: "insert", text: trailingText }, whitespaceInsert.doc, {
|
|
887
|
+
start: whitespaceInsert.nextCursor,
|
|
888
|
+
end: whitespaceInsert.nextCursor,
|
|
889
|
+
affinity: whitespaceInsert.nextAffinity,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
815
895
|
const effectiveRange = shouldReplacePlaceholder
|
|
816
896
|
? { start: range.start, end: Math.min(docCursorLength, range.start + 1) }
|
|
817
897
|
: range;
|
|
@@ -1174,6 +1254,18 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
1174
1254
|
return true;
|
|
1175
1255
|
});
|
|
1176
1256
|
}
|
|
1257
|
+
function mergeMarksPreservingOrder(...groups) {
|
|
1258
|
+
const next = [];
|
|
1259
|
+
for (const group of groups) {
|
|
1260
|
+
for (const mark of group) {
|
|
1261
|
+
if (next.some((existing) => existing.key === mark.key)) {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
next.push(mark);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return next;
|
|
1268
|
+
}
|
|
1177
1269
|
function sliceRuns(runs, startCursor, endCursor) {
|
|
1178
1270
|
const [left, rest] = splitRunsAt(runs, startCursor);
|
|
1179
1271
|
const [selected, right] = splitRunsAt(rest, Math.max(0, endCursor - startCursor));
|
|
@@ -1330,27 +1422,188 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
1330
1422
|
},
|
|
1331
1423
|
};
|
|
1332
1424
|
}
|
|
1333
|
-
function
|
|
1425
|
+
function rewritePendingPlaceholderAtCursor(state, cursorOffset, marks) {
|
|
1334
1426
|
const textModel = getEditorTextModelForDoc(state.doc);
|
|
1335
1427
|
const lines = textModel.getStructuralLines();
|
|
1336
1428
|
const loc = textModel.resolveOffsetToLine(cursorOffset);
|
|
1337
1429
|
const line = lines[loc.lineIndex];
|
|
1338
1430
|
if (!line) {
|
|
1339
|
-
return
|
|
1431
|
+
return null;
|
|
1340
1432
|
}
|
|
1341
1433
|
const block = getBlockAtPath(state.doc.blocks, line.path);
|
|
1342
1434
|
if (!block || block.type !== "paragraph") {
|
|
1343
|
-
return
|
|
1435
|
+
return null;
|
|
1344
1436
|
}
|
|
1437
|
+
const placeholder = "\u200B";
|
|
1345
1438
|
const runs = paragraphToRuns(block);
|
|
1346
|
-
const { before } = sliceRuns(runs, loc.offsetInLine, loc.offsetInLine);
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1439
|
+
const { before, after } = sliceRuns(runs, loc.offsetInLine, loc.offsetInLine);
|
|
1440
|
+
const replacement = [];
|
|
1441
|
+
const firstAfter = after[0];
|
|
1442
|
+
if (firstAfter?.type === "text" && firstAfter.text.startsWith(placeholder)) {
|
|
1443
|
+
if (marks && marks.length > 0) {
|
|
1444
|
+
replacement.push({ type: "text", text: placeholder, marks });
|
|
1445
|
+
}
|
|
1446
|
+
if (firstAfter.text.length > placeholder.length) {
|
|
1447
|
+
replacement.push({
|
|
1448
|
+
...firstAfter,
|
|
1449
|
+
text: firstAfter.text.slice(placeholder.length),
|
|
1450
|
+
});
|
|
1351
1451
|
}
|
|
1352
|
-
|
|
1353
|
-
}
|
|
1452
|
+
replacement.push(...after.slice(1));
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
const lastBefore = before[before.length - 1];
|
|
1456
|
+
if (lastBefore?.type !== "text" ||
|
|
1457
|
+
!lastBefore.text.endsWith(placeholder)) {
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
const prefix = lastBefore.text.slice(0, -placeholder.length);
|
|
1461
|
+
if (prefix) {
|
|
1462
|
+
replacement.push({ ...lastBefore, text: prefix });
|
|
1463
|
+
}
|
|
1464
|
+
if (marks && marks.length > 0) {
|
|
1465
|
+
replacement.push({ type: "text", text: placeholder, marks });
|
|
1466
|
+
}
|
|
1467
|
+
replacement.push(...after);
|
|
1468
|
+
before.pop();
|
|
1469
|
+
}
|
|
1470
|
+
const mergedRuns = normalizeRuns([...before, ...replacement]);
|
|
1471
|
+
const nextBlock = {
|
|
1472
|
+
...block,
|
|
1473
|
+
content: runsToInlines(mergedRuns),
|
|
1474
|
+
};
|
|
1475
|
+
const parentPath = line.path.slice(0, -1);
|
|
1476
|
+
const indexInParent = line.path[line.path.length - 1] ?? 0;
|
|
1477
|
+
const nextDoc = {
|
|
1478
|
+
...state.doc,
|
|
1479
|
+
blocks: updateBlocksAtPath(state.doc.blocks, parentPath, (blocks) => blocks.map((child, index) => index === indexInParent ? nextBlock : child)),
|
|
1480
|
+
};
|
|
1481
|
+
const next = createStateFromDoc(nextDoc);
|
|
1482
|
+
return {
|
|
1483
|
+
...next,
|
|
1484
|
+
selection: {
|
|
1485
|
+
start: cursorOffset,
|
|
1486
|
+
end: cursorOffset,
|
|
1487
|
+
affinity: marks && marks.length > 0 ? "forward" : "backward",
|
|
1488
|
+
},
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
function updatePendingPlaceholderMarksAtCursor(state, cursorOffset, marks) {
|
|
1492
|
+
return rewritePendingPlaceholderAtCursor(state, cursorOffset, marks);
|
|
1493
|
+
}
|
|
1494
|
+
function removePendingPlaceholderAtCursor(state, cursorOffset) {
|
|
1495
|
+
return rewritePendingPlaceholderAtCursor(state, cursorOffset, null);
|
|
1496
|
+
}
|
|
1497
|
+
function getPendingPlaceholderMarksAtCursor(state, cursorOffset) {
|
|
1498
|
+
const textModel = getEditorTextModelForDoc(state.doc);
|
|
1499
|
+
const lines = textModel.getStructuralLines();
|
|
1500
|
+
const loc = textModel.resolveOffsetToLine(cursorOffset);
|
|
1501
|
+
const line = lines[loc.lineIndex];
|
|
1502
|
+
if (!line) {
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
const block = getBlockAtPath(state.doc.blocks, line.path);
|
|
1506
|
+
if (!block || block.type !== "paragraph") {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
const placeholder = "\u200B";
|
|
1510
|
+
const runs = paragraphToRuns(block);
|
|
1511
|
+
const { before, after } = sliceRuns(runs, loc.offsetInLine, loc.offsetInLine);
|
|
1512
|
+
const firstAfter = after[0];
|
|
1513
|
+
if (firstAfter?.type === "text" && firstAfter.text.startsWith(placeholder)) {
|
|
1514
|
+
return firstAfter.marks;
|
|
1515
|
+
}
|
|
1516
|
+
const lastBefore = before[before.length - 1];
|
|
1517
|
+
if (lastBefore?.type === "text" &&
|
|
1518
|
+
lastBefore.text.endsWith(placeholder)) {
|
|
1519
|
+
return lastBefore.marks;
|
|
1520
|
+
}
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
function insertTextBeforePendingPlaceholderInDoc(doc, cursorOffset, text, marks) {
|
|
1524
|
+
const textModel = getEditorTextModelForDoc(doc);
|
|
1525
|
+
const lines = textModel.getStructuralLines();
|
|
1526
|
+
const loc = textModel.resolveOffsetToLine(cursorOffset);
|
|
1527
|
+
const line = lines[loc.lineIndex];
|
|
1528
|
+
if (!line) {
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
const block = getBlockAtPath(doc.blocks, line.path);
|
|
1532
|
+
if (!block || block.type !== "paragraph") {
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
const placeholder = "\u200B";
|
|
1536
|
+
const runs = paragraphToRuns(block);
|
|
1537
|
+
const { before, after } = sliceRuns(runs, loc.offsetInLine, loc.offsetInLine);
|
|
1538
|
+
const firstAfter = after[0];
|
|
1539
|
+
if (firstAfter?.type !== "text" ||
|
|
1540
|
+
!firstAfter.text.startsWith(placeholder)) {
|
|
1541
|
+
return null;
|
|
1542
|
+
}
|
|
1543
|
+
const mergedRuns = normalizeRuns([
|
|
1544
|
+
...before,
|
|
1545
|
+
...(text.length > 0 ? [{ type: "text", text, marks }] : []),
|
|
1546
|
+
firstAfter,
|
|
1547
|
+
...after.slice(1),
|
|
1548
|
+
]);
|
|
1549
|
+
const nextBlock = {
|
|
1550
|
+
...block,
|
|
1551
|
+
content: runsToInlines(mergedRuns),
|
|
1552
|
+
};
|
|
1553
|
+
const parentPath = line.path.slice(0, -1);
|
|
1554
|
+
const indexInParent = line.path[line.path.length - 1] ?? 0;
|
|
1555
|
+
const nextDoc = {
|
|
1556
|
+
...doc,
|
|
1557
|
+
blocks: updateBlocksAtPath(doc.blocks, parentPath, (blocks) => blocks.map((child, index) => index === indexInParent ? nextBlock : child)),
|
|
1558
|
+
};
|
|
1559
|
+
return {
|
|
1560
|
+
doc: nextDoc,
|
|
1561
|
+
nextCursor: cursorOffset + Array.from(graphemeSegments(text)).length,
|
|
1562
|
+
nextAffinity: "forward",
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
function hasInlineMarkerBoundaryBefore(source, markerStart) {
|
|
1566
|
+
if (markerStart <= 0) {
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
return !WORD_CHARACTER_PATTERN.test(source[markerStart - 1] ?? "");
|
|
1570
|
+
}
|
|
1571
|
+
function pickSafeCollapsedToggleMarkerSpec(params) {
|
|
1572
|
+
const { defaultSpec, source, insertAt, affinity } = params;
|
|
1573
|
+
const candidates = Array.from(toggleMarkerToSpec.values()).filter((spec, index, all) => spec.kind === defaultSpec.kind &&
|
|
1574
|
+
all.findIndex((candidate) => candidate.kind === spec.kind &&
|
|
1575
|
+
candidate.open === spec.open &&
|
|
1576
|
+
candidate.close === spec.close) === index);
|
|
1577
|
+
if (candidates.length <= 1) {
|
|
1578
|
+
return defaultSpec;
|
|
1579
|
+
}
|
|
1580
|
+
const previousChar = source[insertAt - 1] ?? "";
|
|
1581
|
+
const nextChar = source[insertAt] ?? "";
|
|
1582
|
+
let bestSpec = defaultSpec;
|
|
1583
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1584
|
+
for (const spec of candidates) {
|
|
1585
|
+
if (spec.open === "_" &&
|
|
1586
|
+
!hasInlineMarkerBoundaryBefore(source, insertAt)) {
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
let score = 0;
|
|
1590
|
+
if (previousChar && spec.open[0] === previousChar) {
|
|
1591
|
+
score += affinity === "forward" ? 8 : 3;
|
|
1592
|
+
}
|
|
1593
|
+
if (nextChar &&
|
|
1594
|
+
spec.close[spec.close.length - 1] === nextChar) {
|
|
1595
|
+
score += affinity === "backward" ? 8 : 3;
|
|
1596
|
+
}
|
|
1597
|
+
if (spec.open === defaultSpec.open &&
|
|
1598
|
+
spec.close === defaultSpec.close) {
|
|
1599
|
+
score -= 0.5;
|
|
1600
|
+
}
|
|
1601
|
+
if (score < bestScore) {
|
|
1602
|
+
bestSpec = spec;
|
|
1603
|
+
bestScore = score;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return bestSpec;
|
|
1354
1607
|
}
|
|
1355
1608
|
function preferredAffinityAtGap(left, right, fallback) {
|
|
1356
1609
|
if (isMarksPrefix(left, right) && right.length > left.length) {
|
|
@@ -1611,18 +1864,39 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
1611
1864
|
};
|
|
1612
1865
|
if (selection.start === selection.end) {
|
|
1613
1866
|
const caret = selection.start;
|
|
1867
|
+
const pendingPlaceholderMarks = getPendingPlaceholderMarksAtCursor(state, caret);
|
|
1868
|
+
if (pendingPlaceholderMarks) {
|
|
1869
|
+
const hasMarker = pendingPlaceholderMarks.some((mark) => mark.kind === markerKind);
|
|
1870
|
+
const around = marksAroundCursor(state.doc, caret);
|
|
1871
|
+
const nextMarks = hasMarker
|
|
1872
|
+
? removeMarkByKind(pendingPlaceholderMarks, markerKind)
|
|
1873
|
+
: mergeMarksPreservingOrder(around.left, pendingPlaceholderMarks, [markerMark]);
|
|
1874
|
+
const next = nextMarks.length > 0
|
|
1875
|
+
? updatePendingPlaceholderMarksAtCursor(state, caret, nextMarks)
|
|
1876
|
+
: removePendingPlaceholderAtCursor(state, caret);
|
|
1877
|
+
if (next) {
|
|
1878
|
+
return {
|
|
1879
|
+
...next,
|
|
1880
|
+
selection: {
|
|
1881
|
+
start: caret,
|
|
1882
|
+
end: caret,
|
|
1883
|
+
affinity: "forward",
|
|
1884
|
+
},
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1614
1888
|
// When the caret is at the end boundary of an inline wrapper, toggling the
|
|
1615
1889
|
// wrapper should "exit" it (so the next character types outside). This is
|
|
1616
1890
|
// best expressed in cursor space by flipping affinity to "forward" when we
|
|
1617
1891
|
// are leaving a wrapper of the requested kind.
|
|
1618
1892
|
const around = marksAroundCursor(state.doc, caret);
|
|
1619
1893
|
if (isMarksPrefix(around.right, around.left) &&
|
|
1620
|
-
around.left.length > around.right.length
|
|
1894
|
+
around.left.length > around.right.length &&
|
|
1895
|
+
(selection.affinity ?? "forward") === "backward") {
|
|
1621
1896
|
const exiting = around.left.slice(around.right.length);
|
|
1622
1897
|
if (exiting.some((mark) => mark.kind === markerKind)) {
|
|
1623
1898
|
const remainingMarks = removeMarkByKind(around.left, markerKind);
|
|
1624
|
-
if (!marksEqual(remainingMarks, around.right)
|
|
1625
|
-
canPreserveRemainingMarksAtBoundary(state, caret, markerKind, remainingMarks)) {
|
|
1899
|
+
if (!marksEqual(remainingMarks, around.right)) {
|
|
1626
1900
|
const next = createPendingPlaceholderStateAtCursor(state, caret, remainingMarks);
|
|
1627
1901
|
if (next) {
|
|
1628
1902
|
return next;
|
|
@@ -1645,6 +1919,20 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
1645
1919
|
around.right.length > around.left.length) {
|
|
1646
1920
|
const entering = around.right.slice(around.left.length);
|
|
1647
1921
|
if (entering.some((mark) => mark.kind === markerKind)) {
|
|
1922
|
+
const remainingMarks = removeMarkByKind(around.right, markerKind);
|
|
1923
|
+
const next = remainingMarks.length > 0
|
|
1924
|
+
? updatePendingPlaceholderMarksAtCursor(state, caret, remainingMarks)
|
|
1925
|
+
: removePendingPlaceholderAtCursor(state, caret);
|
|
1926
|
+
if (next) {
|
|
1927
|
+
return {
|
|
1928
|
+
...next,
|
|
1929
|
+
selection: {
|
|
1930
|
+
start: caret,
|
|
1931
|
+
end: caret,
|
|
1932
|
+
affinity: "backward",
|
|
1933
|
+
},
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1648
1936
|
const insertAtBackward = map.cursorToSource(caret, "backward");
|
|
1649
1937
|
const insertAtForward = map.cursorToSource(caret, "forward");
|
|
1650
1938
|
const after = source.slice(insertAtBackward);
|
|
@@ -1705,6 +1993,7 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
1705
1993
|
}
|
|
1706
1994
|
if (isMarksPrefix(around.right, around.left) &&
|
|
1707
1995
|
around.left.length > around.right.length &&
|
|
1996
|
+
(selection.affinity ?? "forward") === "backward" &&
|
|
1708
1997
|
!around.left.some((mark) => mark.kind === markerKind)) {
|
|
1709
1998
|
const next = createPendingPlaceholderStateAtCursor(state, caret, [
|
|
1710
1999
|
...around.left,
|
|
@@ -1735,34 +2024,63 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
1735
2024
|
return null;
|
|
1736
2025
|
})();
|
|
1737
2026
|
// When at a boundary between cursor positions (insertAtBackward !== insertAtForward),
|
|
1738
|
-
// prefer insertAtBackward
|
|
1739
|
-
//
|
|
1740
|
-
//
|
|
2027
|
+
// only prefer insertAtBackward if the caret is intentionally anchored inside the
|
|
2028
|
+
// left formatting context. If the caret affinity is forward, the user explicitly
|
|
2029
|
+
// exited that wrapper and new markers belong on the forward side of the gap.
|
|
2030
|
+
// Still guard against inserting a longer marker into a shorter boundary run,
|
|
2031
|
+
// which would create ambiguous source (e.g., *italic*****).
|
|
1741
2032
|
const betweenLen = insertAtForward - insertAtBackward;
|
|
1742
|
-
const preferBackward = insertAtBackward !== insertAtForward &&
|
|
2033
|
+
const preferBackward = insertAtBackward !== insertAtForward &&
|
|
2034
|
+
(selection.affinity ?? "forward") === "backward" &&
|
|
2035
|
+
openLen <= betweenLen;
|
|
1743
2036
|
const insertAt = placeholderPos ?? (preferBackward ? insertAtBackward : insertAtForward);
|
|
2037
|
+
const insertMarkerSpec = placeholderPos === null
|
|
2038
|
+
? pickSafeCollapsedToggleMarkerSpec({
|
|
2039
|
+
defaultSpec: markerSpec,
|
|
2040
|
+
source,
|
|
2041
|
+
insertAt,
|
|
2042
|
+
affinity: selection.affinity ?? "forward",
|
|
2043
|
+
})
|
|
2044
|
+
: markerSpec;
|
|
2045
|
+
const insertOpenMarker = insertMarkerSpec.open;
|
|
2046
|
+
const insertCloseMarker = insertMarkerSpec.close;
|
|
2047
|
+
const insertOpenLen = insertOpenMarker.length;
|
|
2048
|
+
const baseMarks = (selection.affinity ?? "forward") === "backward"
|
|
2049
|
+
? around.left
|
|
2050
|
+
: around.right;
|
|
2051
|
+
const nextMarks = [
|
|
2052
|
+
...baseMarks.filter((mark) => mark.kind !== markerKind),
|
|
2053
|
+
markerMark,
|
|
2054
|
+
];
|
|
2055
|
+
if (placeholderPos !== null) {
|
|
2056
|
+
const next = updatePendingPlaceholderMarksAtCursor(state, caret, nextMarks);
|
|
2057
|
+
if (next) {
|
|
2058
|
+
return next;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
const docInserted = createPendingPlaceholderStateAtCursor(state, caret, nextMarks);
|
|
2062
|
+
if (docInserted) {
|
|
2063
|
+
return docInserted;
|
|
2064
|
+
}
|
|
1744
2065
|
const nextSource = placeholderPos !== null
|
|
1745
2066
|
? source.slice(0, insertAt) +
|
|
1746
|
-
|
|
2067
|
+
insertOpenMarker +
|
|
1747
2068
|
placeholder +
|
|
1748
|
-
|
|
2069
|
+
insertCloseMarker +
|
|
1749
2070
|
source.slice(insertAt + placeholder.length)
|
|
1750
2071
|
: source.slice(0, insertAt) +
|
|
1751
|
-
|
|
2072
|
+
insertOpenMarker +
|
|
1752
2073
|
placeholder +
|
|
1753
|
-
|
|
2074
|
+
insertCloseMarker +
|
|
1754
2075
|
source.slice(insertAt);
|
|
1755
2076
|
const next = createState(nextSource);
|
|
1756
|
-
const placeholderStart = insertAt +
|
|
2077
|
+
const placeholderStart = insertAt + insertOpenLen;
|
|
1757
2078
|
const startCursor = next.map.sourceToCursor(placeholderStart, "forward");
|
|
1758
|
-
return {
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
affinity: "forward",
|
|
1764
|
-
},
|
|
1765
|
-
};
|
|
2079
|
+
return createStateFromDoc(next.doc, {
|
|
2080
|
+
start: startCursor.cursorOffset,
|
|
2081
|
+
end: startCursor.cursorOffset,
|
|
2082
|
+
affinity: "forward",
|
|
2083
|
+
});
|
|
1766
2084
|
}
|
|
1767
2085
|
const cursorStart = Math.min(selection.start, selection.end);
|
|
1768
2086
|
const cursorEnd = Math.max(selection.start, selection.end);
|
|
@@ -2004,18 +2322,12 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
2004
2322
|
// Apply marks in reverse order so outer marks wrap inner marks
|
|
2005
2323
|
const sortedMarks = [...run.marks].reverse();
|
|
2006
2324
|
for (const mark of sortedMarks) {
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
else if (mark.kind === "strikethrough") {
|
|
2014
|
-
content = `<s>${content}</s>`;
|
|
2015
|
-
}
|
|
2016
|
-
else if (mark.kind === "link") {
|
|
2017
|
-
const url = mark.data?.url ?? "";
|
|
2018
|
-
content = `<a href="${escapeHtml(url)}">${content}</a>`;
|
|
2325
|
+
for (const serializeMarkToHtml of inlineHtmlSerializers) {
|
|
2326
|
+
const next = serializeMarkToHtml({ kind: mark.kind, data: mark.data }, content, { escapeHtml });
|
|
2327
|
+
if (next !== null) {
|
|
2328
|
+
content = next;
|
|
2329
|
+
break;
|
|
2330
|
+
}
|
|
2019
2331
|
}
|
|
2020
2332
|
}
|
|
2021
2333
|
html += content;
|
|
@@ -2035,22 +2347,13 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
2035
2347
|
const startLoc = textModel.resolveOffsetToLine(cursorStart);
|
|
2036
2348
|
const endLoc = textModel.resolveOffsetToLine(cursorEnd);
|
|
2037
2349
|
let html = "";
|
|
2038
|
-
let
|
|
2039
|
-
const
|
|
2040
|
-
if (
|
|
2041
|
-
html += `</${activeList.type}>`;
|
|
2042
|
-
activeList = null;
|
|
2043
|
-
}
|
|
2044
|
-
};
|
|
2045
|
-
const openList = (type, indent) => {
|
|
2046
|
-
if (activeList &&
|
|
2047
|
-
activeList.type === type &&
|
|
2048
|
-
activeList.indent === indent) {
|
|
2350
|
+
let activeGroup = null;
|
|
2351
|
+
const closeGroup = () => {
|
|
2352
|
+
if (!activeGroup) {
|
|
2049
2353
|
return;
|
|
2050
2354
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
activeList = { type, indent };
|
|
2355
|
+
html += activeGroup.close;
|
|
2356
|
+
activeGroup = null;
|
|
2054
2357
|
};
|
|
2055
2358
|
for (let lineIndex = startLoc.lineIndex; lineIndex <= endLoc.lineIndex; lineIndex += 1) {
|
|
2056
2359
|
const line = lines[lineIndex];
|
|
@@ -2067,66 +2370,46 @@ export function createRuntimeFromRegistry(registry) {
|
|
|
2067
2370
|
? endLoc.offsetInLine
|
|
2068
2371
|
: line.cursorLength;
|
|
2069
2372
|
const selectedRuns = sliceRuns(runs, startInLine, endInLine).selected;
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2373
|
+
const lineHtml = runsToHtml(normalizeRuns(selectedRuns));
|
|
2374
|
+
const lineText = runs
|
|
2375
|
+
.map((r) => (r.type === "text" ? r.text : " "))
|
|
2376
|
+
.join("");
|
|
2377
|
+
let wrapperBlock = null;
|
|
2073
2378
|
if (line.path.length > 1) {
|
|
2074
2379
|
const wrapperPath = line.path.slice(0, -1);
|
|
2075
2380
|
const wrapper = getBlockAtPath(state.doc.blocks, wrapperPath);
|
|
2076
2381
|
if (wrapper && wrapper.type === "block-wrapper") {
|
|
2077
|
-
|
|
2078
|
-
wrapperData = wrapper.data;
|
|
2382
|
+
wrapperBlock = wrapper;
|
|
2079
2383
|
}
|
|
2080
2384
|
}
|
|
2081
|
-
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
else if (wrapperKind === "numbered-list") {
|
|
2110
|
-
openList("ol", 0);
|
|
2111
|
-
html += `<li>${lineHtml}</li>`;
|
|
2112
|
-
}
|
|
2113
|
-
else if (wrapperKind === "blockquote") {
|
|
2114
|
-
closeList();
|
|
2115
|
-
html += `<blockquote>${lineHtml}</blockquote>`;
|
|
2116
|
-
}
|
|
2117
|
-
else if (listMatch) {
|
|
2118
|
-
// Plain paragraph with list markers (cake v3 list model)
|
|
2119
|
-
const isNumbered = /^\d+\.$/.test(listMatch[2]);
|
|
2120
|
-
const indent = Math.floor(listMatch[1].length / 2);
|
|
2121
|
-
openList(isNumbered ? "ol" : "ul", indent);
|
|
2122
|
-
html += `<li>${lineHtml}</li>`;
|
|
2123
|
-
}
|
|
2124
|
-
else {
|
|
2125
|
-
closeList();
|
|
2126
|
-
html += `<div>${lineHtml}</div>`;
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
closeList();
|
|
2385
|
+
let lineResult = null;
|
|
2386
|
+
for (const serializeLineToHtml of serializeSelectionLineToHtmlFns) {
|
|
2387
|
+
lineResult = serializeLineToHtml({
|
|
2388
|
+
state,
|
|
2389
|
+
line,
|
|
2390
|
+
block,
|
|
2391
|
+
wrapperBlock,
|
|
2392
|
+
lineText,
|
|
2393
|
+
startInLine,
|
|
2394
|
+
endInLine,
|
|
2395
|
+
lineCursorLength: line.cursorLength,
|
|
2396
|
+
selectedHtml: lineHtml,
|
|
2397
|
+
});
|
|
2398
|
+
if (lineResult) {
|
|
2399
|
+
break;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
const group = lineResult?.group ?? null;
|
|
2403
|
+
if (!group || !activeGroup || activeGroup.key !== group.key) {
|
|
2404
|
+
closeGroup();
|
|
2405
|
+
if (group) {
|
|
2406
|
+
html += group.open;
|
|
2407
|
+
activeGroup = group;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
html += lineResult?.html ?? `<div>${lineHtml}</div>`;
|
|
2411
|
+
}
|
|
2412
|
+
closeGroup();
|
|
2130
2413
|
if (!html) {
|
|
2131
2414
|
return "";
|
|
2132
2415
|
}
|