@blankdotpage/cake 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -997,15 +997,36 @@ class CursorSourceBuilder {
997
997
  if (!text) {
998
998
  return;
999
999
  }
1000
- for (const segment of graphemeSegments(text)) {
1001
- this.sourceParts.push(segment.segment);
1002
- this.sourceLengthValue += segment.segment.length;
1003
- const sourceLength = this.sourceLengthValue;
1004
- this.cursorLength += 1;
1005
- this.boundaries.push({
1006
- sourceBackward: sourceLength,
1007
- sourceForward: sourceLength
1008
- });
1000
+ let isAllAscii = true;
1001
+ for (let i = 0; i < text.length; i++) {
1002
+ if (text.charCodeAt(i) >= 128) {
1003
+ isAllAscii = false;
1004
+ break;
1005
+ }
1006
+ }
1007
+ if (isAllAscii) {
1008
+ for (let i = 0; i < text.length; i++) {
1009
+ const char = text[i];
1010
+ this.sourceParts.push(char);
1011
+ this.sourceLengthValue += 1;
1012
+ const sourceLength = this.sourceLengthValue;
1013
+ this.cursorLength += 1;
1014
+ this.boundaries.push({
1015
+ sourceBackward: sourceLength,
1016
+ sourceForward: sourceLength
1017
+ });
1018
+ }
1019
+ } else {
1020
+ for (const segment of graphemeSegments(text)) {
1021
+ this.sourceParts.push(segment.segment);
1022
+ this.sourceLengthValue += segment.segment.length;
1023
+ const sourceLength = this.sourceLengthValue;
1024
+ this.cursorLength += 1;
1025
+ this.boundaries.push({
1026
+ sourceBackward: sourceLength,
1027
+ sourceForward: sourceLength
1028
+ });
1029
+ }
1009
1030
  }
1010
1031
  }
1011
1032
  appendCursorAtom(sourceText, cursorUnits = 1) {
@@ -1055,6 +1076,15 @@ class CursorSourceBuilder {
1055
1076
  };
1056
1077
  }
1057
1078
  }
1079
+ function isStructuralEdit(command) {
1080
+ return command.type === "insert" || command.type === "delete-backward" || command.type === "delete-forward" || command.type === "insert-line-break" || command.type === "exit-block-wrapper";
1081
+ }
1082
+ function isApplyEditCommand(command) {
1083
+ return command.type === "insert" || command.type === "insert-line-break" || command.type === "delete-backward" || command.type === "delete-forward";
1084
+ }
1085
+ function defineExtension(extension) {
1086
+ return extension;
1087
+ }
1058
1088
  const defaultSelection$1 = { start: 0, end: 0, affinity: "forward" };
1059
1089
  function createRuntime(extensions) {
1060
1090
  const toggleMarkerToKind = /* @__PURE__ */ new Map();
@@ -1296,7 +1326,7 @@ function createRuntime(extensions) {
1296
1326
  }
1297
1327
  }
1298
1328
  const selection = normalizeSelection$1(state.selection);
1299
- if (command.type === "insert" || command.type === "delete-backward" || command.type === "delete-forward" || command.type === "insert-line-break" || command.type === "exit-block-wrapper") {
1329
+ if (isStructuralEdit(command)) {
1300
1330
  const structural = applyStructuralEdit(command, state.doc, selection);
1301
1331
  if (!structural) {
1302
1332
  if (command.type === "delete-backward" || command.type === "delete-forward") {
@@ -1338,6 +1368,39 @@ function createRuntime(extensions) {
1338
1368
  }
1339
1369
  };
1340
1370
  }
1371
+ if (command.type === "insert" || command.type === "insert-line-break") {
1372
+ const cursorLength = state.map.cursorLength;
1373
+ const cursorStart = Math.max(
1374
+ 0,
1375
+ Math.min(cursorLength, Math.min(selection.start, selection.end))
1376
+ );
1377
+ const cursorEnd = Math.max(
1378
+ 0,
1379
+ Math.min(cursorLength, Math.max(selection.start, selection.end))
1380
+ );
1381
+ const range = { start: cursorStart, end: cursorEnd };
1382
+ const fullDocReplace = range.start === 0 && range.end === cursorLength;
1383
+ const from = fullDocReplace ? 0 : state.map.cursorToSource(range.start, "backward");
1384
+ const to = fullDocReplace ? state.source.length : state.map.cursorToSource(range.end, "forward");
1385
+ const fromClamped = Math.max(0, Math.min(from, state.source.length));
1386
+ const toClamped = Math.max(
1387
+ fromClamped,
1388
+ Math.min(to, state.source.length)
1389
+ );
1390
+ const insertText = command.type === "insert" ? command.text : "\n";
1391
+ const nextSource = state.source.slice(0, fromClamped) + insertText + state.source.slice(toClamped);
1392
+ const next2 = createState(nextSource);
1393
+ const caretSource2 = fromClamped + insertText.length;
1394
+ const caretCursor2 = next2.map.sourceToCursor(caretSource2, "forward");
1395
+ return {
1396
+ ...next2,
1397
+ selection: {
1398
+ start: caretCursor2.cursorOffset,
1399
+ end: caretCursor2.cursorOffset,
1400
+ affinity: caretCursor2.affinity
1401
+ }
1402
+ };
1403
+ }
1341
1404
  return state;
1342
1405
  }
1343
1406
  const interim = createStateFromDoc(structural.doc);
@@ -2580,7 +2643,20 @@ function parseLiteralBlock(source, start, context) {
2580
2643
  return { block: { type: "paragraph", content }, nextPos: end };
2581
2644
  }
2582
2645
  function parseLiteralInline(source, start, end) {
2583
- const segment = graphemeSegments(source.slice(start, end))[0];
2646
+ const code = source.charCodeAt(start);
2647
+ if (code < 128) {
2648
+ const text2 = source[start] ?? "";
2649
+ return { inline: { type: "text", text: text2 }, nextPos: start + 1 };
2650
+ }
2651
+ if (code >= 55296 && code <= 56319) {
2652
+ const lowCode = source.charCodeAt(start + 1);
2653
+ if (lowCode >= 56320 && lowCode <= 57343) {
2654
+ const segment2 = graphemeSegments(source.slice(start, Math.min(start + 10, end)))[0];
2655
+ const text2 = segment2 ? segment2.segment : source.slice(start, start + 2);
2656
+ return { inline: { type: "text", text: text2 }, nextPos: start + text2.length };
2657
+ }
2658
+ }
2659
+ const segment = graphemeSegments(source.slice(start, Math.min(start + 10, end)))[0];
2584
2660
  const text = segment ? segment.segment : source[start] ?? "";
2585
2661
  return { inline: { type: "text", text }, nextPos: start + text.length };
2586
2662
  }
@@ -2623,29 +2699,66 @@ function normalizeSelection$1(selection) {
2623
2699
  affinity: isRange ? "backward" : selection.affinity
2624
2700
  };
2625
2701
  }
2702
+ const graphemeCache = /* @__PURE__ */ new WeakMap();
2626
2703
  function createTextRun(node, cursorStart) {
2627
- const segments = graphemeSegments(node.data);
2704
+ const data = node.data;
2705
+ const cached = graphemeCache.get(node);
2706
+ if (cached && cached.data === data) {
2707
+ const segmentCount = Math.max(0, cached.boundaryOffsets.length - 1);
2708
+ return {
2709
+ node,
2710
+ cursorStart,
2711
+ cursorEnd: cursorStart + segmentCount,
2712
+ boundaryOffsets: cached.boundaryOffsets
2713
+ };
2714
+ }
2715
+ const segments = graphemeSegments(data);
2628
2716
  const boundaryOffsets = [0];
2629
2717
  for (const segment of segments) {
2630
2718
  boundaryOffsets.push(segment.index + segment.segment.length);
2631
2719
  }
2632
2720
  const cursorEnd = cursorStart + segments.length;
2721
+ graphemeCache.set(node, { data, boundaryOffsets });
2633
2722
  return { node, cursorStart, cursorEnd, boundaryOffsets };
2634
2723
  }
2635
2724
  function boundaryIndexForOffset(boundaryOffsets, offset) {
2636
2725
  if (offset <= 0) {
2637
2726
  return 0;
2638
2727
  }
2639
- for (let i = 1; i < boundaryOffsets.length; i += 1) {
2640
- const boundary = boundaryOffsets[i];
2641
- if (offset < boundary) {
2642
- return i - 1;
2728
+ const lastIndex = boundaryOffsets.length - 1;
2729
+ if (lastIndex <= 0) {
2730
+ return 0;
2731
+ }
2732
+ const lastBoundary = boundaryOffsets[lastIndex];
2733
+ if (offset >= lastBoundary) {
2734
+ return lastIndex;
2735
+ }
2736
+ let low = 1;
2737
+ let high = lastIndex;
2738
+ while (low < high) {
2739
+ const mid = low + high >>> 1;
2740
+ const boundary2 = boundaryOffsets[mid];
2741
+ if (boundary2 < offset) {
2742
+ low = mid + 1;
2743
+ } else {
2744
+ high = mid;
2643
2745
  }
2644
- if (offset === boundary) {
2645
- return i;
2746
+ }
2747
+ const boundary = boundaryOffsets[low];
2748
+ return boundary === offset ? low : low - 1;
2749
+ }
2750
+ function firstRunStartingAfterCursor(runs, cursorOffset) {
2751
+ let low = 0;
2752
+ let high = runs.length;
2753
+ while (low < high) {
2754
+ const mid = low + high >>> 1;
2755
+ if (runs[mid].cursorStart <= cursorOffset) {
2756
+ low = mid + 1;
2757
+ } else {
2758
+ high = mid;
2646
2759
  }
2647
2760
  }
2648
- return boundaryOffsets.length - 1;
2761
+ return low;
2649
2762
  }
2650
2763
  function createDomMap(runs) {
2651
2764
  const runForNode = /* @__PURE__ */ new Map();
@@ -2660,33 +2773,34 @@ function createDomMap(runs) {
2660
2773
  if (runs.length === 0) {
2661
2774
  return null;
2662
2775
  }
2663
- for (let i = 0; i < runs.length; i += 1) {
2664
- const run = runs[i];
2665
- const previous = i > 0 ? runs[i - 1] : null;
2666
- const next = i + 1 < runs.length ? runs[i + 1] : null;
2667
- if (cursorOffset < run.cursorStart) {
2668
- if (!previous) {
2669
- return { node: run.node, offset: run.boundaryOffsets[0] };
2670
- }
2671
- const previousOffset = previous.boundaryOffsets[previous.boundaryOffsets.length - 1];
2672
- return affinity === "backward" ? { node: previous.node, offset: previousOffset } : { node: run.node, offset: run.boundaryOffsets[0] };
2673
- }
2674
- if (cursorOffset === run.cursorStart && previous && affinity === "backward") {
2675
- return {
2676
- node: previous.node,
2677
- offset: previous.boundaryOffsets[previous.boundaryOffsets.length - 1]
2678
- };
2679
- }
2680
- if (cursorOffset <= run.cursorEnd) {
2681
- if (cursorOffset === run.cursorEnd && affinity === "forward" && next && next.cursorStart === cursorOffset) {
2682
- return { node: next.node, offset: next.boundaryOffsets[0] };
2683
- }
2684
- const index = Math.max(0, cursorOffset - run.cursorStart);
2685
- const boundedIndex = Math.min(index, run.boundaryOffsets.length - 1);
2686
- return { node: run.node, offset: run.boundaryOffsets[boundedIndex] };
2776
+ const nextIndex = firstRunStartingAfterCursor(runs, cursorOffset);
2777
+ const runIndex = nextIndex - 1;
2778
+ if (runIndex < 0) {
2779
+ const first = runs[0];
2780
+ return { node: first.node, offset: first.boundaryOffsets[0] };
2781
+ }
2782
+ const run = runs[runIndex];
2783
+ const previous = runIndex > 0 ? runs[runIndex - 1] : null;
2784
+ const next = nextIndex < runs.length ? runs[nextIndex] : null;
2785
+ if (cursorOffset === run.cursorStart && previous && affinity === "backward") {
2786
+ return {
2787
+ node: previous.node,
2788
+ offset: previous.boundaryOffsets[previous.boundaryOffsets.length - 1]
2789
+ };
2790
+ }
2791
+ if (cursorOffset <= run.cursorEnd) {
2792
+ if (cursorOffset === run.cursorEnd && affinity === "forward" && next && next.cursorStart === cursorOffset) {
2793
+ return { node: next.node, offset: next.boundaryOffsets[0] };
2687
2794
  }
2795
+ const index = Math.max(0, cursorOffset - run.cursorStart);
2796
+ const boundedIndex = Math.min(index, run.boundaryOffsets.length - 1);
2797
+ return { node: run.node, offset: run.boundaryOffsets[boundedIndex] };
2798
+ }
2799
+ if (next) {
2800
+ const runEndOffset = run.boundaryOffsets[run.boundaryOffsets.length - 1];
2801
+ return affinity === "backward" ? { node: run.node, offset: runEndOffset } : { node: next.node, offset: next.boundaryOffsets[0] };
2688
2802
  }
2689
- const last = runs[runs.length - 1];
2803
+ const last = run;
2690
2804
  return {
2691
2805
  node: last.node,
2692
2806
  offset: last.boundaryOffsets[last.boundaryOffsets.length - 1]
@@ -3074,7 +3188,9 @@ function applyDomSelection(selection, map) {
3074
3188
  if (!anchorPoint || !focusPoint) {
3075
3189
  return;
3076
3190
  }
3077
- domSelection.removeAllRanges();
3191
+ if (domSelection.rangeCount > 0 && domSelection.anchorNode === anchorPoint.node && domSelection.anchorOffset === anchorPoint.offset && domSelection.focusNode === focusPoint.node && domSelection.focusOffset === focusPoint.offset) {
3192
+ return;
3193
+ }
3078
3194
  if (isCollapsed) {
3079
3195
  domSelection.collapse(anchorPoint.node, anchorPoint.offset);
3080
3196
  return;
@@ -3094,6 +3210,7 @@ function applyDomSelection(selection, map) {
3094
3210
  domSelection.extend(focusPoint.node, focusPoint.offset);
3095
3211
  return;
3096
3212
  }
3213
+ domSelection.removeAllRanges();
3097
3214
  const range = document.createRange();
3098
3215
  const rangeStart = isForward ? anchorPoint : focusPoint;
3099
3216
  const rangeEnd = isForward ? focusPoint : anchorPoint;
@@ -3237,7 +3354,7 @@ function findTextNodeAtOrBefore$1(nodes, start) {
3237
3354
  return null;
3238
3355
  }
3239
3356
  const BOLD_KIND$1 = "bold";
3240
- const boldExtension = {
3357
+ const boldExtension = defineExtension({
3241
3358
  name: "bold",
3242
3359
  toggleInline: { kind: BOLD_KIND$1, markers: ["**"] },
3243
3360
  keybindings: [
@@ -3320,7 +3437,7 @@ const boldExtension = {
3320
3437
  }
3321
3438
  return element;
3322
3439
  }
3323
- };
3440
+ });
3324
3441
  function countSingleAsterisks(source, start, end) {
3325
3442
  let count = 0;
3326
3443
  for (let i = start; i < end; i += 1) {
@@ -3339,7 +3456,7 @@ function countSingleAsterisks(source, start, end) {
3339
3456
  const BOLD_KIND = "bold";
3340
3457
  const ITALIC_KIND$1 = "italic";
3341
3458
  const MARKERS = ["***", "___"];
3342
- const combinedEmphasisExtension = {
3459
+ const combinedEmphasisExtension = defineExtension({
3343
3460
  name: "combined-emphasis",
3344
3461
  parseInline(source, start, end, context) {
3345
3462
  const marker = MARKERS.find((m) => source.slice(start, start + 3) === m);
@@ -3373,7 +3490,7 @@ const combinedEmphasisExtension = {
3373
3490
  nextPos: close + 3
3374
3491
  };
3375
3492
  }
3376
- };
3493
+ });
3377
3494
  function ensureHttpsProtocol(url) {
3378
3495
  const trimmedUrl = url.trim();
3379
3496
  if (!trimmedUrl) {
@@ -3421,7 +3538,7 @@ function getPopoverPosition(params) {
3421
3538
  };
3422
3539
  }
3423
3540
  function CakeLinkPopover(params) {
3424
- const { container, contentRoot, toOverlayRect } = params;
3541
+ const { container, contentRoot, toOverlayRect, getSelection, executeCommand } = params;
3425
3542
  const anchorRef = useRef(null);
3426
3543
  const popoverRef = useRef(null);
3427
3544
  const inputRef = useRef(null);
@@ -3585,6 +3702,12 @@ function CakeLinkPopover(params) {
3585
3702
  window.open(displayUrl, "_blank", "noopener,noreferrer");
3586
3703
  };
3587
3704
  const handleUnlink = () => {
3705
+ const selection = getSelection();
3706
+ if (!selection) {
3707
+ close();
3708
+ return;
3709
+ }
3710
+ executeCommand({ type: "unlink", start: selection.start, end: selection.end });
3588
3711
  close();
3589
3712
  };
3590
3713
  const handleInputKeyDown = (event) => {
@@ -3888,7 +4011,7 @@ function cursorOffsetToVisibleOffset(lines, cursorOffset) {
3888
4011
  return codeUnitIndex + (lastLine.cursorToCodeUnit[lastOffset] ?? lastLine.text.length);
3889
4012
  }
3890
4013
  const LINK_KIND = "link";
3891
- const linkExtension = {
4014
+ const linkExtension = defineExtension({
3892
4015
  name: "link",
3893
4016
  inlineWrapperAffinity: [{ kind: LINK_KIND, inclusive: false }],
3894
4017
  keybindings: [
@@ -3916,6 +4039,39 @@ const linkExtension = {
3916
4039
  }
3917
4040
  ],
3918
4041
  onEdit(command, state) {
4042
+ if (command.type === "unlink") {
4043
+ const cursorPos = command.start;
4044
+ const sourcePos = state.map.cursorToSource(cursorPos, "forward");
4045
+ const source = state.source;
4046
+ let linkStart = sourcePos;
4047
+ while (linkStart > 0 && source[linkStart] !== "[") {
4048
+ linkStart--;
4049
+ }
4050
+ if (source[linkStart] !== "[") {
4051
+ return null;
4052
+ }
4053
+ const labelClose = source.indexOf("](", linkStart + 1);
4054
+ if (labelClose === -1) {
4055
+ return null;
4056
+ }
4057
+ const urlClose = source.indexOf(")", labelClose + 2);
4058
+ if (urlClose === -1) {
4059
+ return null;
4060
+ }
4061
+ const label2 = source.slice(linkStart + 1, labelClose);
4062
+ const nextSource2 = source.slice(0, linkStart) + label2 + source.slice(urlClose + 1);
4063
+ const newState = state.runtime.createState(nextSource2);
4064
+ const labelEndSource = linkStart + label2.length;
4065
+ const newCursor = newState.map.sourceToCursor(labelEndSource, "forward");
4066
+ return {
4067
+ source: nextSource2,
4068
+ selection: {
4069
+ start: newCursor.cursorOffset,
4070
+ end: newCursor.cursorOffset,
4071
+ affinity: "forward"
4072
+ }
4073
+ };
4074
+ }
3919
4075
  if (command.type !== "wrap-link") {
3920
4076
  return null;
3921
4077
  }
@@ -4042,13 +4198,15 @@ const linkExtension = {
4042
4198
  {
4043
4199
  container: context.container,
4044
4200
  contentRoot: context.contentRoot,
4045
- toOverlayRect: context.toOverlayRect
4201
+ toOverlayRect: context.toOverlayRect,
4202
+ getSelection: context.getSelection,
4203
+ executeCommand: context.executeCommand
4046
4204
  }
4047
4205
  );
4048
4206
  }
4049
- };
4207
+ });
4050
4208
  const PIPE_LINK_KIND = "pipe-link";
4051
- const pipeLinkExtension = {
4209
+ const pipeLinkExtension = defineExtension({
4052
4210
  name: "pipe-link",
4053
4211
  parseInline(source, start, end) {
4054
4212
  if (source[start] !== "|") {
@@ -4119,10 +4277,10 @@ const pipeLinkExtension = {
4119
4277
  }
4120
4278
  return element;
4121
4279
  }
4122
- };
4280
+ });
4123
4281
  const BLOCKQUOTE_KIND = "blockquote";
4124
4282
  const PREFIX = "> ";
4125
- const blockquoteExtension = {
4283
+ const blockquoteExtension = defineExtension({
4126
4284
  name: "blockquote",
4127
4285
  parseBlock(source, start, context) {
4128
4286
  if (source.slice(start, start + PREFIX.length) !== PREFIX) {
@@ -4191,9 +4349,9 @@ const blockquoteExtension = {
4191
4349
  }
4192
4350
  return element;
4193
4351
  }
4194
- };
4352
+ });
4195
4353
  const ITALIC_KIND = "italic";
4196
- const italicExtension = {
4354
+ const italicExtension = defineExtension({
4197
4355
  name: "italic",
4198
4356
  toggleInline: { kind: ITALIC_KIND, markers: ["*", "_"] },
4199
4357
  keybindings: [
@@ -4263,7 +4421,7 @@ const italicExtension = {
4263
4421
  }
4264
4422
  return element;
4265
4423
  }
4266
- };
4424
+ });
4267
4425
  const HEADING_KIND = "heading";
4268
4426
  const HEADING_PATTERN = /^(#{1,3}) /;
4269
4427
  function findLineStartInSource(source, sourceOffset) {
@@ -4364,7 +4522,7 @@ function shouldExitHeadingOnLineBreak(state) {
4364
4522
  const contentStart = lineStart + marker.length;
4365
4523
  return sourcePos >= contentStart && sourcePos <= lineEnd;
4366
4524
  }
4367
- const headingExtension = {
4525
+ const headingExtension = defineExtension({
4368
4526
  name: "heading",
4369
4527
  onEdit(command, state) {
4370
4528
  if (command.type === "delete-backward") {
@@ -4505,7 +4663,7 @@ const headingExtension = {
4505
4663
  }
4506
4664
  return lineElement;
4507
4665
  }
4508
- };
4666
+ });
4509
4667
  const IMAGE_KIND = "image";
4510
4668
  const IMAGE_PATTERN = /^!\[([^\]]*)\]\(([^)]*)\)$/;
4511
4669
  const UPLOADING_PATTERN = /^!\[uploading:([^\]]+)\]\(\)$/;
@@ -4523,7 +4681,7 @@ function isImageData(data) {
4523
4681
  }
4524
4682
  return false;
4525
4683
  }
4526
- const imageExtension = {
4684
+ const imageExtension = defineExtension({
4527
4685
  name: "image",
4528
4686
  parseBlock(source, start) {
4529
4687
  let lineEnd = source.indexOf("\n", start);
@@ -4618,7 +4776,7 @@ const imageExtension = {
4618
4776
  }
4619
4777
  return element;
4620
4778
  }
4621
- };
4779
+ });
4622
4780
  const LIST_LINE_REGEX$2 = /^(\s*)([-*+]|\d+\.)( )(.*)$/;
4623
4781
  const INDENT_SIZE = 2;
4624
4782
  function parseMarkerType(marker) {
@@ -5437,7 +5595,7 @@ function getParagraphText(block) {
5437
5595
  }
5438
5596
  return text;
5439
5597
  }
5440
- const listExtension = {
5598
+ const listExtension = defineExtension({
5441
5599
  name: "list",
5442
5600
  keybindings: [
5443
5601
  {
@@ -5530,7 +5688,7 @@ const listExtension = {
5530
5688
  }
5531
5689
  return element;
5532
5690
  }
5533
- };
5691
+ });
5534
5692
  const THUMB_MIN_HEIGHT = 30;
5535
5693
  const SCROLL_HIDE_DELAY = 500;
5536
5694
  const TRACK_PADDING = 8;
@@ -5767,14 +5925,14 @@ function ScrollbarOverlay({ container }) {
5767
5925
  }
5768
5926
  ) });
5769
5927
  }
5770
- const scrollbarExtension = {
5928
+ const scrollbarExtension = defineExtension({
5771
5929
  name: "scrollbar",
5772
5930
  renderOverlay(context) {
5773
5931
  return /* @__PURE__ */ jsxRuntimeExports.jsx(ScrollbarOverlay, { container: context.container });
5774
5932
  }
5775
- };
5933
+ });
5776
5934
  const STRIKE_KIND = "strikethrough";
5777
- const strikethroughExtension = {
5935
+ const strikethroughExtension = defineExtension({
5778
5936
  name: "strikethrough",
5779
5937
  toggleInline: { kind: STRIKE_KIND, markers: ["~~"] },
5780
5938
  keybindings: [
@@ -5847,8 +6005,8 @@ const strikethroughExtension = {
5847
6005
  }
5848
6006
  return element;
5849
6007
  }
5850
- };
5851
- const bundledExtensions = [
6008
+ });
6009
+ const bundledExtensionsWithoutImage = [
5852
6010
  blockquoteExtension,
5853
6011
  headingExtension,
5854
6012
  listExtension,
@@ -5857,7 +6015,10 @@ const bundledExtensions = [
5857
6015
  italicExtension,
5858
6016
  strikethroughExtension,
5859
6017
  pipeLinkExtension,
5860
- linkExtension,
6018
+ linkExtension
6019
+ ];
6020
+ const bundledExtensions = [
6021
+ ...bundledExtensionsWithoutImage,
5861
6022
  imageExtension
5862
6023
  ];
5863
6024
  function toLayoutRect(params) {
@@ -5917,6 +6078,46 @@ function cursorOffsetToCodeUnit(cursorToCodeUnit, offset) {
5917
6078
  const clamped = Math.max(0, Math.min(offset, cursorToCodeUnit.length - 1));
5918
6079
  return cursorToCodeUnit[clamped] ?? 0;
5919
6080
  }
6081
+ function createDomPositionResolver(lineElement) {
6082
+ const textNodes = [];
6083
+ const cumulativeEnds = [];
6084
+ const walker = createTextWalker(lineElement);
6085
+ let current = walker.nextNode();
6086
+ let total = 0;
6087
+ while (current) {
6088
+ if (current instanceof Text) {
6089
+ const length = current.data.length;
6090
+ textNodes.push(current);
6091
+ total += length;
6092
+ cumulativeEnds.push(total);
6093
+ }
6094
+ current = walker.nextNode();
6095
+ }
6096
+ if (textNodes.length === 0) {
6097
+ return () => {
6098
+ if (!lineElement.textContent) {
6099
+ return { node: lineElement, offset: 0 };
6100
+ }
6101
+ return { node: lineElement, offset: lineElement.childNodes.length };
6102
+ };
6103
+ }
6104
+ return (offsetInLine) => {
6105
+ const clamped = Math.max(0, Math.min(offsetInLine, total));
6106
+ let low = 0;
6107
+ let high = cumulativeEnds.length - 1;
6108
+ while (low < high) {
6109
+ const mid = low + high >>> 1;
6110
+ if ((cumulativeEnds[mid] ?? 0) < clamped) {
6111
+ low = mid + 1;
6112
+ } else {
6113
+ high = mid;
6114
+ }
6115
+ }
6116
+ const node = textNodes[low] ?? lineElement;
6117
+ const prevEnd = low > 0 ? cumulativeEnds[low - 1] ?? 0 : 0;
6118
+ return { node, offset: clamped - prevEnd };
6119
+ };
6120
+ }
5920
6121
  function measureCharacterRect(params) {
5921
6122
  if (params.lineLength <= 0) {
5922
6123
  return null;
@@ -5929,101 +6130,155 @@ function measureCharacterRect(params) {
5929
6130
  params.cursorToCodeUnit,
5930
6131
  Math.min(params.offset + 1, params.lineLength)
5931
6132
  );
5932
- const startPosition = resolveDomPosition(params.lineElement, startCodeUnit);
5933
- const endPosition = resolveDomPosition(params.lineElement, endCodeUnit);
5934
- const range = document.createRange();
5935
- range.setStart(startPosition.node, startPosition.offset);
5936
- range.setEnd(endPosition.node, endPosition.offset);
5937
- const rects = range.getClientRects();
6133
+ const startPosition = params.resolveDomPosition(startCodeUnit);
6134
+ const endPosition = params.resolveDomPosition(endCodeUnit);
6135
+ params.range.setStart(startPosition.node, startPosition.offset);
6136
+ params.range.setEnd(endPosition.node, endPosition.offset);
6137
+ const rects = params.range.getClientRects();
5938
6138
  if (rects.length > 0) {
5939
6139
  return rects[0] ?? null;
5940
6140
  }
5941
- const rect = range.getBoundingClientRect();
6141
+ const rect = params.range.getBoundingClientRect();
5942
6142
  if (rect.width === 0 && rect.height === 0) {
5943
6143
  return null;
5944
6144
  }
5945
6145
  return rect;
5946
6146
  }
5947
6147
  function measureLineRows(params) {
6148
+ var _a, _b;
6149
+ const fallbackLineBox = toLayoutRect({
6150
+ rect: params.lineRect,
6151
+ containerRect: params.containerRect,
6152
+ scroll: params.scroll
6153
+ });
5948
6154
  if (params.lineLength === 0) {
5949
- const lineBox = toLayoutRect({
5950
- rect: params.lineRect,
5951
- containerRect: params.containerRect,
5952
- scroll: params.scroll
5953
- });
5954
6155
  return [
5955
6156
  {
5956
6157
  startOffset: 0,
5957
6158
  endOffset: 0,
5958
- rect: { ...lineBox, width: 0 }
6159
+ rect: { ...fallbackLineBox, width: 0 }
5959
6160
  }
5960
6161
  ];
5961
6162
  }
5962
- const fullLineRange = document.createRange();
5963
- const fullLineStart = resolveDomPosition(params.lineElement, 0);
5964
- const fullLineEnd = resolveDomPosition(
5965
- params.lineElement,
5966
- params.codeUnitLength
5967
- );
5968
- fullLineRange.setStart(fullLineStart.node, fullLineStart.offset);
5969
- fullLineRange.setEnd(fullLineEnd.node, fullLineEnd.offset);
5970
- const fullLineRects = groupDomRectsByRow(
5971
- Array.from(fullLineRange.getClientRects())
5972
- );
5973
- const fallbackLineBox = toLayoutRect({
5974
- rect: params.lineRect,
5975
- containerRect: params.containerRect,
5976
- scroll: params.scroll
5977
- });
5978
- const charRect = measureCharacterRect({
5979
- lineElement: params.lineElement,
5980
- offset: 0,
5981
- lineLength: params.lineLength,
5982
- cursorToCodeUnit: params.cursorToCodeUnit
5983
- });
5984
- const charWidth = (charRect == null ? void 0 : charRect.width) ?? 0;
5985
- if (fullLineRects.length === 0 || charWidth <= 0) {
5986
- const rows2 = [
6163
+ const WRAP_THRESHOLD_PX = 3;
6164
+ const resolvePosition = createDomPositionResolver(params.lineElement);
6165
+ const scratchRange = document.createRange();
6166
+ const topCache = /* @__PURE__ */ new Map();
6167
+ const fullLineStart = resolvePosition(0);
6168
+ const fullLineEnd = resolvePosition(params.codeUnitLength);
6169
+ scratchRange.setStart(fullLineStart.node, fullLineStart.offset);
6170
+ scratchRange.setEnd(fullLineEnd.node, fullLineEnd.offset);
6171
+ const fullLineRects = groupDomRectsByRow(Array.from(scratchRange.getClientRects()));
6172
+ if (fullLineRects.length === 0) {
6173
+ return [
5987
6174
  {
5988
6175
  startOffset: 0,
5989
6176
  endOffset: params.lineLength,
5990
6177
  rect: fallbackLineBox
5991
6178
  }
5992
6179
  ];
5993
- return rows2.length === 1 ? [
6180
+ }
6181
+ function offsetToTop(offset) {
6182
+ if (topCache.has(offset)) {
6183
+ return topCache.get(offset) ?? null;
6184
+ }
6185
+ const rect = measureCharacterRect({
6186
+ lineElement: params.lineElement,
6187
+ offset,
6188
+ lineLength: params.lineLength,
6189
+ cursorToCodeUnit: params.cursorToCodeUnit,
6190
+ resolveDomPosition: resolvePosition,
6191
+ range: scratchRange
6192
+ });
6193
+ const top = rect ? rect.top : null;
6194
+ topCache.set(offset, top);
6195
+ return top;
6196
+ }
6197
+ function findFirstMeasurableOffset(from) {
6198
+ for (let offset = Math.max(0, from); offset < params.lineLength; offset++) {
6199
+ if (offsetToTop(offset) !== null) {
6200
+ return offset;
6201
+ }
6202
+ }
6203
+ return null;
6204
+ }
6205
+ function findNextRowStartOffset(fromExclusive, rowTop) {
6206
+ const lastIndex = params.lineLength - 1;
6207
+ if (fromExclusive > lastIndex) {
6208
+ return null;
6209
+ }
6210
+ const isNewRowAt = (offset) => {
6211
+ const top = offsetToTop(offset);
6212
+ return top !== null && Math.abs(top - rowTop) > WRAP_THRESHOLD_PX;
6213
+ };
6214
+ let step = 1;
6215
+ let lastSame = fromExclusive - 1;
6216
+ let probe = fromExclusive;
6217
+ while (probe <= lastIndex) {
6218
+ if (isNewRowAt(probe)) {
6219
+ break;
6220
+ }
6221
+ lastSame = probe;
6222
+ probe += step;
6223
+ step *= 2;
6224
+ }
6225
+ if (probe > lastIndex) {
6226
+ probe = lastIndex;
6227
+ if (!isNewRowAt(probe)) {
6228
+ return null;
6229
+ }
6230
+ }
6231
+ let low = Math.max(fromExclusive, lastSame + 1);
6232
+ let high = probe;
6233
+ while (low < high) {
6234
+ const mid = low + high >>> 1;
6235
+ if (isNewRowAt(mid)) {
6236
+ high = mid;
6237
+ } else {
6238
+ low = mid + 1;
6239
+ }
6240
+ }
6241
+ return low < params.lineLength ? low : null;
6242
+ }
6243
+ const rows = [];
6244
+ const firstMeasurable = findFirstMeasurableOffset(0);
6245
+ if (firstMeasurable === null) {
6246
+ return [
5994
6247
  {
5995
- ...rows2[0],
5996
- rect: {
5997
- ...rows2[0].rect,
5998
- top: fallbackLineBox.top,
5999
- height: fallbackLineBox.height
6000
- }
6248
+ startOffset: 0,
6249
+ endOffset: params.lineLength,
6250
+ rect: fallbackLineBox
6001
6251
  }
6002
- ] : rows2;
6252
+ ];
6003
6253
  }
6004
- let startOffset = 0;
6005
- const rows = fullLineRects.map((rect, index) => {
6006
- const remaining = params.lineLength - startOffset;
6007
- const isLast = index === fullLineRects.length - 1;
6008
- const estimatedLength = Math.max(1, Math.round(rect.width / charWidth));
6009
- const rowLength = isLast ? remaining : Math.min(remaining, estimatedLength);
6010
- const row = {
6011
- startOffset,
6012
- endOffset: startOffset + rowLength,
6254
+ let currentRowStart = 0;
6255
+ let currentRowTop = ((_a = fullLineRects[0]) == null ? void 0 : _a.top) ?? params.lineRect.top;
6256
+ let searchFrom = firstMeasurable + 1;
6257
+ let rowIndex = 0;
6258
+ while (currentRowStart < params.lineLength) {
6259
+ const nextRowStart = findNextRowStartOffset(searchFrom, currentRowTop);
6260
+ const currentRowEnd = nextRowStart ?? params.lineLength;
6261
+ const domRect = fullLineRects[rowIndex] ?? params.lineRect;
6262
+ rows.push({
6263
+ startOffset: currentRowStart,
6264
+ endOffset: currentRowEnd,
6013
6265
  rect: toLayoutRect({
6014
- rect,
6266
+ rect: domRect,
6015
6267
  containerRect: params.containerRect,
6016
6268
  scroll: params.scroll
6017
6269
  })
6018
- };
6019
- startOffset += rowLength;
6020
- return row;
6021
- });
6022
- if (rows.length > 0) {
6023
- rows[rows.length - 1] = {
6024
- ...rows[rows.length - 1],
6025
- endOffset: params.lineLength
6026
- };
6270
+ });
6271
+ if (nextRowStart === null) {
6272
+ break;
6273
+ }
6274
+ currentRowStart = nextRowStart;
6275
+ rowIndex += 1;
6276
+ const nextMeasurable = findFirstMeasurableOffset(currentRowStart);
6277
+ if (nextMeasurable === null) {
6278
+ break;
6279
+ }
6280
+ currentRowTop = ((_b = fullLineRects[rowIndex]) == null ? void 0 : _b.top) ?? currentRowTop;
6281
+ searchFrom = nextMeasurable + 1;
6027
6282
  }
6028
6283
  if (rows.length === 1) {
6029
6284
  rows[0] = {
@@ -6095,6 +6350,44 @@ function measureLayoutModelFromDom(params) {
6095
6350
  }
6096
6351
  return buildLayoutModel(params.lines, measurer);
6097
6352
  }
6353
+ function measureLayoutModelRangeFromDom(params) {
6354
+ const measurer = createDomLayoutMeasurer({
6355
+ root: params.root,
6356
+ container: params.container,
6357
+ lines: params.lines
6358
+ });
6359
+ if (!measurer) {
6360
+ return null;
6361
+ }
6362
+ const clampedStart = Math.max(0, Math.min(params.startLineIndex, params.lines.length - 1));
6363
+ const clampedEnd = Math.max(clampedStart, Math.min(params.endLineIndex, params.lines.length - 1));
6364
+ const lineOffsets = getLineOffsets(params.lines);
6365
+ let lineStartOffset = lineOffsets[clampedStart] ?? 0;
6366
+ const layouts = [];
6367
+ for (let lineIndex = clampedStart; lineIndex <= clampedEnd; lineIndex += 1) {
6368
+ const lineInfo = params.lines[lineIndex];
6369
+ if (!lineInfo) {
6370
+ continue;
6371
+ }
6372
+ const measurement = measurer.measureLine({
6373
+ lineIndex: lineInfo.lineIndex,
6374
+ lineText: lineInfo.text,
6375
+ lineLength: lineInfo.cursorLength,
6376
+ lineHasNewline: lineInfo.hasNewline,
6377
+ top: 0
6378
+ });
6379
+ layouts.push({
6380
+ lineIndex: lineInfo.lineIndex,
6381
+ lineStartOffset,
6382
+ lineLength: lineInfo.cursorLength,
6383
+ lineHasNewline: lineInfo.hasNewline,
6384
+ lineBox: measurement.lineBox,
6385
+ rows: measurement.rows
6386
+ });
6387
+ lineStartOffset += lineInfo.cursorLength + (lineInfo.hasNewline ? 1 : 0);
6388
+ }
6389
+ return { container: measurer.container, lines: layouts };
6390
+ }
6098
6391
  function getLineElement(root, lineIndex) {
6099
6392
  return root.querySelector(`[data-line-index="${lineIndex}"]`);
6100
6393
  }
@@ -6310,7 +6603,17 @@ function getSelectionGeometry(params) {
6310
6603
  end: selection.start,
6311
6604
  affinity: selection.affinity
6312
6605
  };
6313
- const layout = shouldMeasureLayout(normalized) ? measureLayoutModelFromDom({ lines: docLines, root, container }) : null;
6606
+ const layout = shouldMeasureLayout(normalized) ? (() => {
6607
+ const startLine2 = resolveOffsetToLine(docLines, normalized.start);
6608
+ const endLine = resolveOffsetToLine(docLines, normalized.end);
6609
+ return measureLayoutModelRangeFromDom({
6610
+ lines: docLines,
6611
+ root,
6612
+ container,
6613
+ startLineIndex: startLine2.lineIndex,
6614
+ endLineIndex: endLine.lineIndex
6615
+ });
6616
+ })() : null;
6314
6617
  const containerRect = container.getBoundingClientRect();
6315
6618
  const scroll = { top: container.scrollTop, left: container.scrollLeft };
6316
6619
  const startLine = resolveOffsetToLine(docLines, normalized.start);
@@ -7318,10 +7621,13 @@ class CakeEngine {
7318
7621
  this.caretBlinkTimeoutId = null;
7319
7622
  this.overlayUpdateId = null;
7320
7623
  this.scrollCaretIntoViewId = null;
7624
+ this.selectionRectElements = [];
7625
+ this.lastSelectionRects = null;
7321
7626
  this.extensionsRoot = null;
7322
7627
  this.placeholderRoot = null;
7323
7628
  this.lastFocusRect = null;
7324
7629
  this.verticalNavGoalX = null;
7630
+ this.lastRenderPerf = null;
7325
7631
  this.history = {
7326
7632
  undoStack: [],
7327
7633
  redoStack: [],
@@ -7354,6 +7660,7 @@ class CakeEngine {
7354
7660
  this.selectionDragState = null;
7355
7661
  this.pointerDownPosition = null;
7356
7662
  this.hasMovedSincePointerDown = false;
7663
+ this.lastTouchTime = 0;
7357
7664
  this.container = options.container;
7358
7665
  this.extensions = options.extensions ?? bundledExtensions;
7359
7666
  this.runtime = createRuntime(this.extensions);
@@ -7375,6 +7682,9 @@ class CakeEngine {
7375
7682
  set state(value) {
7376
7683
  this._state = value;
7377
7684
  }
7685
+ getLastRenderPerf() {
7686
+ return this.lastRenderPerf;
7687
+ }
7378
7688
  isEventTargetInContentRoot(target) {
7379
7689
  if (target === this.container) {
7380
7690
  return true;
@@ -7411,6 +7721,9 @@ class CakeEngine {
7411
7721
  getSelection() {
7412
7722
  return this.state.selection;
7413
7723
  }
7724
+ getCursorLength() {
7725
+ return this.state.map.cursorLength;
7726
+ }
7414
7727
  getFocusRect() {
7415
7728
  return this.lastFocusRect;
7416
7729
  }
@@ -7465,6 +7778,7 @@ class CakeEngine {
7465
7778
  if (!this.isComposing) {
7466
7779
  this.applySelection(this.state.selection);
7467
7780
  }
7781
+ this.scheduleScrollCaretIntoView();
7468
7782
  }
7469
7783
  setValue({ value, selection }) {
7470
7784
  const valueChanged = value !== this.state.source;
@@ -7540,7 +7854,7 @@ class CakeEngine {
7540
7854
  }
7541
7855
  executeCommand(command) {
7542
7856
  var _a;
7543
- const shouldOpenLinkPopover = command.type === "wrap-link" && command.openPopover;
7857
+ const shouldOpenLinkPopover = "openPopover" in command && command.openPopover === true;
7544
7858
  const nextState = this.runtime.applyEdit(command, this.state);
7545
7859
  if (nextState === this.state) {
7546
7860
  return false;
@@ -7715,6 +8029,14 @@ class CakeEngine {
7715
8029
  this.patchedCaretRangeFromPoint = null;
7716
8030
  }
7717
8031
  render() {
8032
+ const perfEnabled = this.container.dataset.cakePerf === "1";
8033
+ let perfStart = 0;
8034
+ let renderStart = 0;
8035
+ let renderAndMapMs = 0;
8036
+ let applySelectionMs = 0;
8037
+ if (perfEnabled) {
8038
+ perfStart = performance.now();
8039
+ }
7718
8040
  if (!this.contentRoot) {
7719
8041
  const containerPosition = window.getComputedStyle(
7720
8042
  this.container
@@ -7730,6 +8052,9 @@ class CakeEngine {
7730
8052
  this.container.replaceChildren(this.contentRoot, overlay, extensionsRoot);
7731
8053
  this.attachDragListeners();
7732
8054
  }
8055
+ if (perfEnabled) {
8056
+ renderStart = performance.now();
8057
+ }
7733
8058
  const { content, map } = renderDocContent(
7734
8059
  this.state.doc,
7735
8060
  this.extensions,
@@ -7741,9 +8066,26 @@ class CakeEngine {
7741
8066
  this.contentRoot.replaceChildren(...content);
7742
8067
  }
7743
8068
  this.domMap = map;
7744
- this.updateExtensionsOverlayPosition();
7745
- if (!this.isComposing) {
8069
+ if (perfEnabled) {
8070
+ renderAndMapMs = performance.now() - renderStart;
8071
+ }
8072
+ if (!this.isComposing && this.hasFocus()) {
8073
+ const selectionStart = perfEnabled ? performance.now() : 0;
7746
8074
  this.applySelection(this.state.selection);
8075
+ if (perfEnabled) {
8076
+ applySelectionMs = performance.now() - selectionStart;
8077
+ }
8078
+ }
8079
+ if (perfEnabled) {
8080
+ const totalMs = performance.now() - perfStart;
8081
+ this.lastRenderPerf = {
8082
+ totalMs,
8083
+ renderAndMapMs,
8084
+ applySelectionMs,
8085
+ didUpdateDom: needsUpdate,
8086
+ blockCount: this.state.doc.blocks.length,
8087
+ runCount: map.runs.length
8088
+ };
7747
8089
  }
7748
8090
  this.updatePlaceholder();
7749
8091
  this.scheduleOverlayUpdate();
@@ -7993,22 +8335,31 @@ class CakeEngine {
7993
8335
  this.suppressSelectionChange = false;
7994
8336
  return;
7995
8337
  }
7996
- const hit2 = this.pendingClickHit ?? this.hitTestFromClientPoint(event.clientX, event.clientY);
7997
- this.pendingClickHit = null;
7998
- if (hit2) {
8338
+ const pendingHit = this.pendingClickHit;
8339
+ const selection = this.state.selection;
8340
+ if (pendingHit && selection.start !== selection.end) {
7999
8341
  const newSelection = {
8000
- start: hit2.cursorOffset,
8001
- end: hit2.cursorOffset,
8002
- affinity: hit2.affinity
8342
+ start: pendingHit.cursorOffset,
8343
+ end: pendingHit.cursorOffset,
8344
+ affinity: pendingHit.affinity
8003
8345
  };
8346
+ this.pendingClickHit = null;
8347
+ this.suppressSelectionChange = true;
8004
8348
  this.state = this.runtime.updateSelection(this.state, newSelection, {
8005
8349
  kind: "dom"
8006
8350
  });
8007
8351
  this.applySelection(this.state.selection);
8008
8352
  (_b = this.onSelectionChange) == null ? void 0 : _b.call(this, this.state.selection);
8009
8353
  this.scheduleOverlayUpdate();
8354
+ setTimeout(() => {
8355
+ this.suppressSelectionChange = false;
8356
+ }, 0);
8357
+ return;
8010
8358
  }
8011
- this.suppressSelectionChange = false;
8359
+ this.pendingClickHit = null;
8360
+ setTimeout(() => {
8361
+ this.suppressSelectionChange = false;
8362
+ }, 0);
8012
8363
  return;
8013
8364
  }
8014
8365
  this.pendingClickHit = null;
@@ -8386,7 +8737,7 @@ class CakeEngine {
8386
8737
  if (!command) {
8387
8738
  continue;
8388
8739
  }
8389
- if (command.type === "insert" || command.type === "insert-line-break" || command.type === "delete-backward" || command.type === "delete-forward") {
8740
+ if (isApplyEditCommand(command)) {
8390
8741
  this.applyEdit(command);
8391
8742
  } else {
8392
8743
  this.executeCommand(command);
@@ -8906,6 +9257,39 @@ class CakeEngine {
8906
9257
  return { start: target, end: target, affinity: direction };
8907
9258
  }
8908
9259
  const currentPos = selection.start;
9260
+ const currentAffinity = selection.affinity ?? "forward";
9261
+ const measurement = this.getLayoutForNavigation();
9262
+ if (measurement) {
9263
+ const { lines, layout } = measurement;
9264
+ const { rowStart, rowEnd } = getVisualRowBoundaries({
9265
+ lines,
9266
+ layout,
9267
+ offset: currentPos,
9268
+ affinity: currentAffinity
9269
+ });
9270
+ if (direction === "backward" && currentPos === rowStart && currentAffinity === "forward" && currentPos > 0) {
9271
+ const prevBoundaries = getVisualRowBoundaries({
9272
+ lines,
9273
+ layout,
9274
+ offset: currentPos,
9275
+ affinity: "backward"
9276
+ });
9277
+ if (prevBoundaries.rowEnd !== rowEnd || prevBoundaries.rowStart !== rowStart) {
9278
+ return { start: currentPos, end: currentPos, affinity: "backward" };
9279
+ }
9280
+ }
9281
+ if (direction === "forward" && currentPos === rowEnd && currentAffinity === "backward" && currentPos < this.state.map.cursorLength) {
9282
+ const nextBoundaries = getVisualRowBoundaries({
9283
+ lines,
9284
+ layout,
9285
+ offset: currentPos,
9286
+ affinity: "forward"
9287
+ });
9288
+ if (nextBoundaries.rowEnd !== rowEnd || nextBoundaries.rowStart !== rowStart) {
9289
+ return { start: currentPos, end: currentPos, affinity: "forward" };
9290
+ }
9291
+ }
9292
+ }
8909
9293
  const nextPos = this.moveOffsetByChar(currentPos, direction);
8910
9294
  if (nextPos === null) {
8911
9295
  return null;
@@ -9595,6 +9979,7 @@ class CakeEngine {
9595
9979
  }
9596
9980
  this.overlayUpdateId = window.requestAnimationFrame(() => {
9597
9981
  this.overlayUpdateId = null;
9982
+ this.updateExtensionsOverlayPosition();
9598
9983
  this.updateSelectionOverlay();
9599
9984
  });
9600
9985
  }
@@ -9624,12 +10009,34 @@ class CakeEngine {
9624
10009
  overlay.style.zIndex = "2";
9625
10010
  const caret = document.createElement("div");
9626
10011
  caret.className = "cake-caret";
10012
+ caret.style.position = "absolute";
9627
10013
  caret.style.display = "none";
9628
10014
  overlay.append(caret);
9629
10015
  this.overlayRoot = overlay;
9630
10016
  this.caretElement = caret;
10017
+ this.selectionRectElements = [];
10018
+ this.lastSelectionRects = null;
9631
10019
  return overlay;
9632
10020
  }
10021
+ selectionRectsEqual(prev, next) {
10022
+ if (!prev) {
10023
+ return false;
10024
+ }
10025
+ if (prev.length !== next.length) {
10026
+ return false;
10027
+ }
10028
+ for (let index = 0; index < prev.length; index += 1) {
10029
+ const a = prev[index];
10030
+ const b = next[index];
10031
+ if (!a || !b) {
10032
+ return false;
10033
+ }
10034
+ if (a.top !== b.top || a.left !== b.left || a.width !== b.width || a.height !== b.height) {
10035
+ return false;
10036
+ }
10037
+ }
10038
+ return true;
10039
+ }
9633
10040
  ensureExtensionsRoot() {
9634
10041
  if (this.extensionsRoot) {
9635
10042
  return this.extensionsRoot;
@@ -9666,6 +10073,14 @@ class CakeEngine {
9666
10073
  if (!this.overlayRoot || !this.contentRoot) {
9667
10074
  return;
9668
10075
  }
10076
+ const isRecentTouch = Date.now() - this.lastTouchTime < 2e3;
10077
+ if (isRecentTouch) {
10078
+ this.contentRoot.classList.add("cake-touch-mode");
10079
+ this.updateCaret(null);
10080
+ this.syncSelectionRects([]);
10081
+ return;
10082
+ }
10083
+ this.contentRoot.classList.remove("cake-touch-mode");
9669
10084
  const lines = getDocLines(this.state.doc);
9670
10085
  const geometry = getSelectionGeometry({
9671
10086
  root: this.contentRoot,
@@ -9690,21 +10105,34 @@ class CakeEngine {
9690
10105
  if (!this.overlayRoot || !this.caretElement) {
9691
10106
  return;
9692
10107
  }
9693
- const existing = Array.from(
9694
- this.overlayRoot.querySelectorAll(".cake-selection-rect")
9695
- );
9696
- existing.forEach((node) => node.remove());
9697
- const fragment = document.createDocumentFragment();
9698
- rects.forEach((rect) => {
9699
- const element = document.createElement("div");
9700
- element.className = "cake-selection-rect";
10108
+ if (this.selectionRectsEqual(this.lastSelectionRects, rects)) {
10109
+ return;
10110
+ }
10111
+ this.lastSelectionRects = rects;
10112
+ while (this.selectionRectElements.length > rects.length) {
10113
+ const element = this.selectionRectElements.pop();
10114
+ element == null ? void 0 : element.remove();
10115
+ }
10116
+ if (this.selectionRectElements.length < rects.length) {
10117
+ const fragment = document.createDocumentFragment();
10118
+ while (this.selectionRectElements.length < rects.length) {
10119
+ const element = document.createElement("div");
10120
+ element.className = "cake-selection-rect";
10121
+ fragment.append(element);
10122
+ this.selectionRectElements.push(element);
10123
+ }
10124
+ this.overlayRoot.insertBefore(fragment, this.caretElement);
10125
+ }
10126
+ rects.forEach((rect, index) => {
10127
+ const element = this.selectionRectElements[index];
10128
+ if (!element) {
10129
+ return;
10130
+ }
9701
10131
  element.style.top = `${rect.top}px`;
9702
10132
  element.style.left = `${rect.left}px`;
9703
10133
  element.style.width = `${rect.width}px`;
9704
10134
  element.style.height = `${rect.height}px`;
9705
- fragment.append(element);
9706
10135
  });
9707
- this.overlayRoot.insertBefore(fragment, this.caretElement);
9708
10136
  }
9709
10137
  updateCaret(position) {
9710
10138
  if (!this.caretElement) {
@@ -9871,7 +10299,7 @@ class CakeEngine {
9871
10299
  return { cursorOffset: cursor.cursorOffset, affinity: "backward" };
9872
10300
  }
9873
10301
  handlePointerDown(event) {
9874
- var _a;
10302
+ var _a, _b, _c;
9875
10303
  if (!this.isEventTargetInContentRoot(event.target)) {
9876
10304
  return;
9877
10305
  }
@@ -9888,9 +10316,7 @@ class CakeEngine {
9888
10316
  return;
9889
10317
  }
9890
10318
  if (event.pointerType === "touch") {
9891
- this.ignoreTouchNativeSelectionUntil = performance.now() + 750;
9892
- this.suppressSelectionChangeForTick();
9893
- event.preventDefault();
10319
+ this.lastTouchTime = Date.now();
9894
10320
  return;
9895
10321
  }
9896
10322
  const selection = this.state.selection;
@@ -9936,13 +10362,51 @@ class CakeEngine {
9936
10362
  }
9937
10363
  }
9938
10364
  }
9939
- if (selection.start === selection.end && !event.shiftKey) {
9940
- this.suppressSelectionChange = true;
10365
+ if (!event.shiftKey) {
9941
10366
  const hit2 = this.hitTestFromClientPoint(event.clientX, event.clientY);
9942
- if (hit2) {
10367
+ if (!hit2) {
10368
+ return;
10369
+ }
10370
+ if (selection.start !== selection.end) {
10371
+ const selStart2 = Math.min(selection.start, selection.end);
10372
+ const selEnd2 = Math.max(selection.start, selection.end);
10373
+ const clickedInsideSelection2 = hit2.cursorOffset >= selStart2 && hit2.cursorOffset <= selEnd2;
10374
+ if (clickedInsideSelection2) {
10375
+ this.suppressSelectionChange = false;
10376
+ this.blockTrustedTextDrag = true;
10377
+ this.pendingClickHit = hit2;
10378
+ } else {
10379
+ this.suppressSelectionChange = true;
10380
+ this.pendingClickHit = hit2;
10381
+ const newSelection = {
10382
+ start: hit2.cursorOffset,
10383
+ end: hit2.cursorOffset,
10384
+ affinity: hit2.affinity
10385
+ };
10386
+ this.state = this.runtime.updateSelection(this.state, newSelection, {
10387
+ kind: "dom"
10388
+ });
10389
+ this.applySelection(this.state.selection);
10390
+ (_b = this.onSelectionChange) == null ? void 0 : _b.call(this, this.state.selection);
10391
+ this.scheduleOverlayUpdate();
10392
+ return;
10393
+ }
10394
+ } else {
10395
+ this.suppressSelectionChange = true;
9943
10396
  this.pendingClickHit = hit2;
10397
+ const newSelection = {
10398
+ start: hit2.cursorOffset,
10399
+ end: hit2.cursorOffset,
10400
+ affinity: hit2.affinity
10401
+ };
10402
+ this.state = this.runtime.updateSelection(this.state, newSelection, {
10403
+ kind: "dom"
10404
+ });
10405
+ this.applySelection(this.state.selection);
10406
+ (_c = this.onSelectionChange) == null ? void 0 : _c.call(this, this.state.selection);
10407
+ this.scheduleOverlayUpdate();
10408
+ return;
9944
10409
  }
9945
- return;
9946
10410
  }
9947
10411
  this.suppressSelectionChange = false;
9948
10412
  const selStart = Math.min(selection.start, selection.end);
@@ -10054,10 +10518,6 @@ class CakeEngine {
10054
10518
  }
10055
10519
  handlePointerUp(event) {
10056
10520
  this.blockTrustedTextDrag = false;
10057
- if (this.pendingClickHit) {
10058
- this.pendingClickHit = null;
10059
- this.suppressSelectionChange = false;
10060
- }
10061
10521
  if (this.selectionDragState) {
10062
10522
  if (event.pointerId !== this.selectionDragState.pointerId) {
10063
10523
  return;
@@ -10557,7 +11017,7 @@ function findOffsetInTextNode(textNode, clientX, clientY) {
10557
11017
  let closestDistance = Infinity;
10558
11018
  let closestYDistance = Infinity;
10559
11019
  let closestCaretX = 0;
10560
- let closestRowTop = 0;
11020
+ let selectedViaRightEdge = false;
10561
11021
  const rowInfo = /* @__PURE__ */ new Map();
10562
11022
  const range = document.createRange();
10563
11023
  let lastCharRect = null;
@@ -10626,15 +11086,28 @@ function findOffsetInTextNode(textNode, clientX, clientY) {
10626
11086
  closestDistance = xDistance;
10627
11087
  closestOffset = i;
10628
11088
  closestCaretX = caretX;
10629
- closestRowTop = bestRect.top;
11089
+ bestRect.top;
10630
11090
  closestYDistance = yDistance;
11091
+ selectedViaRightEdge = false;
11092
+ }
11093
+ if (i < text.length) {
11094
+ const rightEdgeX = bestRect.right;
11095
+ const rightEdgeDistance = Math.abs(clientX - rightEdgeX);
11096
+ if (rightEdgeDistance < closestDistance) {
11097
+ closestDistance = rightEdgeDistance;
11098
+ closestOffset = i + 1;
11099
+ closestCaretX = rightEdgeX;
11100
+ bestRect.top;
11101
+ closestYDistance = yDistance;
11102
+ selectedViaRightEdge = true;
11103
+ }
10631
11104
  }
10632
11105
  } else if (yDistance < closestYDistance || yDistance === closestYDistance && xDistance < closestDistance) {
10633
11106
  closestYDistance = yDistance;
10634
11107
  closestDistance = xDistance;
10635
11108
  closestOffset = i;
10636
11109
  closestCaretX = caretX;
10637
- closestRowTop = bestRect.top;
11110
+ bestRect.top;
10638
11111
  }
10639
11112
  }
10640
11113
  let closestRow = null;
@@ -10659,11 +11132,11 @@ function findOffsetInTextNode(textNode, clientX, clientY) {
10659
11132
  if (clientX < closestRow.left) {
10660
11133
  closestOffset = closestRow.startOffset;
10661
11134
  closestCaretX = closestRow.left;
10662
- closestRowTop = closestRow.top;
11135
+ closestRow.top;
10663
11136
  } else if (clientX > closestRow.right) {
10664
- closestOffset = closestRow.endOffset;
11137
+ closestOffset = Math.min(closestRow.endOffset + 1, text.length);
10665
11138
  closestCaretX = closestRow.right;
10666
- closestRowTop = closestRow.top;
11139
+ closestRow.top;
10667
11140
  } else if (clientY < closestRow.top || clientY > closestRow.bottom) {
10668
11141
  let bestXDistance = Infinity;
10669
11142
  const range2 = document.createRange();
@@ -10683,12 +11156,12 @@ function findOffsetInTextNode(textNode, clientX, clientY) {
10683
11156
  bestXDistance = xDist;
10684
11157
  closestOffset = i;
10685
11158
  closestCaretX = rect.left;
10686
- closestRowTop = closestRow.top;
11159
+ closestRow.top;
10687
11160
  }
10688
11161
  }
10689
11162
  }
10690
11163
  }
10691
- const pastRowEnd = closestRow !== null && clientX > closestRow.right && Math.abs(clientY - closestRowTop) < 1;
11164
+ const pastRowEnd = selectedViaRightEdge || closestRow !== null && clientX > closestRow.right;
10692
11165
  if (Math.abs(clientX - closestCaretX) <= 2 && closestOffset < text.length && text[closestOffset] === "\n") {
10693
11166
  closestOffset = Math.max(0, closestOffset - 1);
10694
11167
  }
@@ -11141,8 +11614,9 @@ const CakeEditor = forwardRef(
11141
11614
  const lastEmittedSelectionRef = useRef(null);
11142
11615
  const [overlayRoot, setOverlayRoot] = useState(null);
11143
11616
  const [contentRoot, setContentRoot] = useState(null);
11617
+ const baseExtensions = props.disableImageExtension ? bundledExtensionsWithoutImage : bundledExtensions;
11144
11618
  const allExtensionsRef = useRef([
11145
- ...bundledExtensions,
11619
+ ...baseExtensions,
11146
11620
  ...props.extensions ?? []
11147
11621
  ]);
11148
11622
  useEffect(() => {
@@ -11300,6 +11774,10 @@ const CakeEditor = forwardRef(
11300
11774
  }
11301
11775
  return { start: selection.start, end: selection.end };
11302
11776
  },
11777
+ getCursorLength: () => {
11778
+ var _a;
11779
+ return ((_a = engineRef.current) == null ? void 0 : _a.getCursorLength()) ?? 0;
11780
+ },
11303
11781
  insertText: (text) => {
11304
11782
  var _a;
11305
11783
  (_a = engineRef.current) == null ? void 0 : _a.insertText(text);
@@ -11353,6 +11831,10 @@ const CakeEditor = forwardRef(
11353
11831
  }
11354
11832
  const focus = selection.start === selection.end ? selection.start : Math.max(selection.start, selection.end);
11355
11833
  return { start: focus, end: focus };
11834
+ },
11835
+ executeCommand: (command) => {
11836
+ var _a;
11837
+ return ((_a = engineRef.current) == null ? void 0 : _a.executeCommand(command)) ?? false;
11356
11838
  }
11357
11839
  } : null;
11358
11840
  const hasOverlayExtensions = allExtensionsRef.current.some(
@@ -11385,6 +11867,7 @@ export {
11385
11867
  blockquoteExtension,
11386
11868
  boldExtension,
11387
11869
  bundledExtensions,
11870
+ bundledExtensionsWithoutImage,
11388
11871
  combinedEmphasisExtension,
11389
11872
  headingExtension,
11390
11873
  imageExtension,