@markput/react 0.12.1 → 0.14.1

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/index.js CHANGED
@@ -26,6 +26,12 @@ function merge(...objects) {
26
26
  return Object.keys(result).length > 0 ? result : void 0;
27
27
  }
28
28
  //#endregion
29
+ //#region ../../core/src/shared/utils/replaceInString.ts
30
+ function replaceInString(current, range, replacement) {
31
+ if (range.start < 0 || range.end < range.start || range.end > current.length) return void 0;
32
+ return current.slice(0, range.start) + replacement + current.slice(range.end);
33
+ }
34
+ //#endregion
29
35
  //#region ../../core/src/shared/constants.ts
30
36
  const KEYBOARD = {
31
37
  UP: "ArrowUp",
@@ -54,17 +60,21 @@ const DEFAULT_OPTIONS = [{
54
60
  //#endregion
55
61
  //#region ../../core/src/shared/classes/MarkputHandler.ts
56
62
  var MarkputHandler = class {
57
- constructor(store) {
58
- this.store = store;
63
+ constructor(dom, overlayFeature, parsing) {
64
+ this.dom = dom;
65
+ this.overlayFeature = overlayFeature;
66
+ this.parsing = parsing;
59
67
  }
60
68
  get container() {
61
- return this.store.slots.container();
69
+ return this.dom.container();
62
70
  }
63
71
  get overlay() {
64
- return this.store.overlay.element();
72
+ return this.overlayFeature.element();
65
73
  }
66
74
  focus() {
67
- this.store.nodes.focus.head?.focus();
75
+ const firstAddress = this.parsing.index().addressFor([0]);
76
+ if (firstAddress && this.dom.focusAddress(firstAddress).ok) return;
77
+ this.container?.focus();
68
78
  }
69
79
  };
70
80
  //#endregion
@@ -79,192 +89,6 @@ var KeyGenerator = class {
79
89
  }
80
90
  };
81
91
  //#endregion
82
- //#region ../../core/src/shared/checkers/domGuards.ts
83
- /** Type guard: checks if a value is an HTMLElement. */
84
- function isHtmlElement(el) {
85
- return typeof HTMLElement !== "undefined" && el instanceof HTMLElement;
86
- }
87
- /** Type guard: checks if a value is a Text node. */
88
- function isTextNode(node) {
89
- return node instanceof Text;
90
- }
91
- /** Get the i-th child of an element as HTMLElement, or undefined if out of bounds or wrong type. */
92
- function childAt(parent, index) {
93
- const child = parent?.children[index];
94
- return child instanceof HTMLElement ? child : void 0;
95
- }
96
- /** Get all children of an element as HTMLElement[], filtering out non-HTML elements. */
97
- function htmlChildren(parent) {
98
- if (!parent) return [];
99
- return Array.from(parent.children).filter((child) => child instanceof HTMLElement);
100
- }
101
- /** Get the first element child as HTMLElement, or null. */
102
- function firstHtmlChild(parent) {
103
- const child = parent?.firstElementChild;
104
- return child instanceof HTMLElement ? child : null;
105
- }
106
- /** Get the last element child as HTMLElement, or null. */
107
- function lastHtmlChild(parent) {
108
- const child = parent?.lastElementChild;
109
- return child instanceof HTMLElement ? child : null;
110
- }
111
- /** Safely narrow an event's target to Node. */
112
- function nodeTarget(event) {
113
- const { target } = event;
114
- return target instanceof Node ? target : null;
115
- }
116
- /** Get the next node from a TreeWalker as Text, or null. */
117
- function nextText(walker) {
118
- const node = walker.nextNode();
119
- return node?.nodeType === 3 ? node : null;
120
- }
121
- //#endregion
122
- //#region ../../core/src/features/caret/Caret.ts
123
- var Caret = class {
124
- static get isSelectedPosition() {
125
- const selection = window.getSelection();
126
- if (!selection) return;
127
- return selection.isCollapsed;
128
- }
129
- static getCurrentPosition() {
130
- return window.getSelection()?.anchorOffset ?? 0;
131
- }
132
- static getFocusedSpan() {
133
- return window.getSelection()?.anchorNode?.textContent ?? "";
134
- }
135
- static getSelectedNode() {
136
- const node = window.getSelection()?.anchorNode;
137
- if (node && document.contains(node)) return node;
138
- throw new Error("Anchor node of selection is not exists!");
139
- }
140
- static getAbsolutePosition() {
141
- const rect = window.getSelection()?.getRangeAt(0).getBoundingClientRect();
142
- if (rect) return {
143
- left: rect.left,
144
- top: rect.top + rect.height + 1
145
- };
146
- return {
147
- left: 0,
148
- top: 0
149
- };
150
- }
151
- /** Returns the raw DOMRect of the current caret position, or null if unavailable. */
152
- static getCaretRect() {
153
- try {
154
- return (window.getSelection()?.getRangeAt(0))?.getBoundingClientRect() ?? null;
155
- } catch {
156
- return null;
157
- }
158
- }
159
- /**
160
- * Returns true if the caret is on the first visual line of the element.
161
- */
162
- static isCaretOnFirstLine(element) {
163
- const caretRect = this.getCaretRect();
164
- if (!caretRect || caretRect.height === 0) return true;
165
- const elRect = element.getBoundingClientRect();
166
- return caretRect.top < elRect.top + caretRect.height + 2;
167
- }
168
- /**
169
- * Returns true if the caret is on the last visual line of the element.
170
- */
171
- static isCaretOnLastLine(element) {
172
- const caretRect = this.getCaretRect();
173
- if (!caretRect || caretRect.height === 0) return true;
174
- const elRect = element.getBoundingClientRect();
175
- return caretRect.bottom > elRect.bottom - caretRect.height - 2;
176
- }
177
- /**
178
- * Positions the caret in `element` at the character closest to the given x coordinate.
179
- * `y` defaults to the vertical center of the element.
180
- */
181
- static setAtX(element, x, y) {
182
- const elRect = element.getBoundingClientRect();
183
- const targetY = y ?? elRect.top + elRect.height / 2;
184
- const caretDoc = document;
185
- const caretPos = caretDoc.caretRangeFromPoint?.(x, targetY) ?? caretDoc.caretPositionFromPoint?.(x, targetY);
186
- if (!caretPos) return;
187
- const sel = window.getSelection();
188
- if (!sel) return;
189
- let domRange;
190
- if (caretPos instanceof Range) domRange = caretPos;
191
- else if ("offsetNode" in caretPos) {
192
- domRange = document.createRange();
193
- domRange.setStart(caretPos.offsetNode, caretPos.offset);
194
- domRange.collapse(true);
195
- } else return;
196
- if (!element.contains(domRange.startContainer)) {
197
- this.setIndex(element, Infinity);
198
- return;
199
- }
200
- sel.removeAllRanges();
201
- sel.addRange(domRange);
202
- }
203
- static trySetIndex(element, offset) {
204
- try {
205
- this.setIndex(element, offset);
206
- } catch (e) {
207
- console.error(e);
208
- }
209
- }
210
- /**
211
- * Sets the caret at character `offset` within `element` by walking text nodes.
212
- * Use Infinity to position at the very end of all text.
213
- */
214
- static setIndex(element, offset) {
215
- const selection = window.getSelection();
216
- if (!selection) return;
217
- const walker = document.createTreeWalker(element, 4);
218
- let node = nextText(walker);
219
- if (!node) return;
220
- let remaining = isFinite(offset) ? Math.max(0, offset) : Infinity;
221
- for (;;) {
222
- const next = nextText(walker);
223
- if (!next || remaining <= node.length) {
224
- const charOffset = isFinite(remaining) ? Math.min(remaining, node.length) : node.length;
225
- const range = document.createRange();
226
- range.setStart(node, charOffset);
227
- range.collapse(true);
228
- selection.removeAllRanges();
229
- selection.addRange(range);
230
- return;
231
- }
232
- remaining -= node.length;
233
- node = next;
234
- }
235
- }
236
- static getCaretIndex(element) {
237
- let position = 0;
238
- const selection = window.getSelection();
239
- if (!selection?.rangeCount) return position;
240
- const range = selection.getRangeAt(0);
241
- const preCaretRange = range.cloneRange();
242
- preCaretRange.selectNodeContents(element);
243
- preCaretRange.setEnd(range.endContainer, range.endOffset);
244
- position = preCaretRange.toString().length;
245
- return position;
246
- }
247
- static setCaretToEnd(element) {
248
- if (!element) return;
249
- this.setIndex(element, Infinity);
250
- }
251
- static getIndex() {
252
- return window.getSelection()?.anchorOffset ?? NaN;
253
- }
254
- static setIndex1(offset) {
255
- const selection = window.getSelection();
256
- if (!selection?.anchorNode || !selection.rangeCount) return;
257
- const range = selection.getRangeAt(0);
258
- range.setStart(range.startContainer.firstChild ?? range.startContainer, offset);
259
- range.setEnd(range.startContainer.firstChild ?? range.startContainer, offset);
260
- }
261
- setCaretRightTo(element, offset) {
262
- const range = window.getSelection()?.getRangeAt(0);
263
- range?.setStart(range.endContainer, offset);
264
- range?.setEnd(range.endContainer, offset);
265
- }
266
- };
267
- //#endregion
268
92
  //#region ../../core/src/shared/signals/alien-signals/system.ts
269
93
  let ReactiveFlags = /* @__PURE__ */ function(ReactiveFlags) {
270
94
  ReactiveFlags[ReactiveFlags["None"] = 0] = "None";
@@ -666,7 +490,8 @@ function signal(initial, opts) {
666
490
  };
667
491
  return signalOper.bind(node);
668
492
  }
669
- function computed(getter, opts) {
493
+ function computed(getterOrOpts, opts) {
494
+ const isWritable = typeof getterOrOpts !== "function";
670
495
  const node = {
671
496
  value: void 0,
672
497
  subs: void 0,
@@ -674,10 +499,19 @@ function computed(getter, opts) {
674
499
  deps: void 0,
675
500
  depsTail: void 0,
676
501
  flags: ReactiveFlags.None,
677
- getter,
678
- equalsFn: opts?.equals ?? void 0
502
+ getter: isWritable ? getterOrOpts.get : getterOrOpts,
503
+ equalsFn: (isWritable ? getterOrOpts.equals : opts?.equals) ?? void 0
504
+ };
505
+ const readFn = computedOper.bind(node);
506
+ if (!isWritable) return readFn;
507
+ const writableComputed = function writableComputedOper(...args) {
508
+ if (args.length === 0) return readFn();
509
+ const next = args[0];
510
+ if (next === void 0) return;
511
+ getterOrOpts.set(next);
679
512
  };
680
- return computedOper.bind(node);
513
+ Object.defineProperty(writableComputed, "name", { value: "bound " + computedOper.name });
514
+ return writableComputed;
681
515
  }
682
516
  function event() {
683
517
  const node = {
@@ -700,7 +534,7 @@ function event() {
700
534
  callable.read = eventReadOper.bind(node);
701
535
  return callable;
702
536
  }
703
- function alienEffect(fn) {
537
+ function effect(fn) {
704
538
  const e = {
705
539
  fn,
706
540
  cleanup: void 0,
@@ -741,7 +575,7 @@ function effectScope(fn) {
741
575
  function watch(dep, fn) {
742
576
  let initialized = false;
743
577
  let oldValue;
744
- return alienEffect(() => {
578
+ return effect(() => {
745
579
  const newValue = "read" in dep ? dep.read() : dep();
746
580
  if (!initialized) {
747
581
  initialized = true;
@@ -764,30 +598,6 @@ function batch(fn, opts) {
764
598
  mutableScope = prevMutable;
765
599
  }
766
600
  }
767
- function trigger(fn) {
768
- const sub = {
769
- deps: void 0,
770
- depsTail: void 0,
771
- flags: ReactiveFlags.Watching
772
- };
773
- const prevSub = setActiveSub(sub);
774
- try {
775
- fn();
776
- } finally {
777
- activeSub = prevSub;
778
- let dep = sub.deps;
779
- while (dep !== void 0) {
780
- const subs = dep.dep.subs;
781
- dep = unlink(dep, sub);
782
- if (subs !== void 0) {
783
- sub.flags = ReactiveFlags.None;
784
- propagate(subs);
785
- shallowPropagate(subs);
786
- }
787
- }
788
- if (!batchDepth) flush();
789
- }
790
- }
791
601
  function untracked(fn) {
792
602
  const prev = setActiveSub(void 0);
793
603
  try {
@@ -796,443 +606,136 @@ function untracked(fn) {
796
606
  setActiveSub(prev);
797
607
  }
798
608
  }
609
+ function model(opts) {
610
+ let internal;
611
+ const ensureInternal = () => {
612
+ if (internal !== void 0) return internal;
613
+ internal = signal(opts.default !== void 0 ? untracked(opts.default) : void 0, { equals: opts.equals });
614
+ return internal;
615
+ };
616
+ const getFn = opts.get ?? ((value) => value);
617
+ const setFn = opts.set ?? ((next, previous) => next ?? previous);
618
+ const reader = computed(() => getFn(ensureInternal()()));
619
+ const callable = function modelOper(...args) {
620
+ if (args.length === 0) return reader();
621
+ const sig = ensureInternal();
622
+ sig(setFn(args[0], sig()));
623
+ };
624
+ Object.defineProperty(callable, "name", { value: "bound " + computedOper.name });
625
+ return callable;
626
+ }
799
627
  function listen(target, event, handler, options) {
800
- return alienEffect(() => {
628
+ return effect(() => {
801
629
  target.addEventListener(event, handler, options);
802
630
  return () => target.removeEventListener(event, handler, options);
803
631
  });
804
632
  }
805
633
  //#endregion
806
- //#region ../../core/src/features/caret/focus.ts
807
- function enableFocus(store) {
808
- const container = store.slots.container();
809
- if (!container) return () => {};
810
- const scope = effectScope(() => {
811
- listen(container, "focusin", (e) => {
812
- const target = isHtmlElement(e.target) ? e.target : void 0;
813
- store.nodes.focus.target = target;
814
- });
815
- listen(container, "focusout", () => {
816
- store.nodes.focus.target = void 0;
817
- });
818
- listen(container, "click", () => {
819
- const tokens = store.parsing.tokens();
820
- if (tokens.length === 1 && tokens[0].type === "text" && tokens[0].content === "") {
821
- const container = store.slots.container();
822
- (container ? firstHtmlChild(container) : null)?.focus();
823
- }
824
- });
825
- watch(store.lifecycle.rendered, () => {
826
- store.dom.reconcile();
827
- if (!store.props.Mark()) return;
828
- recover(store);
829
- });
830
- });
831
- return () => {
832
- scope();
833
- store.nodes.focus.clear();
834
- };
835
- }
836
- function recover(store) {
837
- const recovery = store.caret.recovery();
838
- if (!recovery) return;
839
- const { anchor, caret, isNext } = recovery;
840
- const isStale = !anchor.target || !anchor.target.isConnected;
841
- let target;
842
- switch (true) {
843
- case isNext && isStale: {
844
- const container = store.slots.container();
845
- target = (recovery.childIndex != null ? childAt(container, recovery.childIndex + 2) : void 0) ?? store.nodes.focus.tail ?? void 0;
846
- break;
847
- }
848
- case isNext:
849
- target = anchor.prev.target;
850
- break;
851
- case isStale:
852
- target = store.nodes.focus.head ?? void 0;
853
- break;
854
- default: target = anchor.next.target;
855
- }
856
- store.nodes.focus.target = target;
857
- target?.focus();
858
- queueMicrotask(() => {
859
- if (!target?.isConnected) return;
860
- store.nodes.focus.target = target;
861
- store.nodes.focus.caret = caret;
862
- });
863
- store.caret.recovery(void 0);
864
- }
634
+ //#region ../../core/src/features/parsing/parser/constants.ts
635
+ /**
636
+ * Constants for ParserV2 - modern markup parser with nesting support
637
+ *
638
+ * This module contains the placeholder constants used by ParserV2.
639
+ * Unlike the legacy Parser, ParserV2 supports:
640
+ * - `__value__` - main content (replaces `__label__`)
641
+ * - `__meta__` - metadata (replaces `__value__`)
642
+ * - `__slot__` - nested content (new feature)
643
+ *
644
+ * For legacy Parser compatibility, see ../Parser/constants.ts
645
+ * For Markup types, see ./types.ts
646
+ */
647
+ const PLACEHOLDER = {
648
+ Value: "__value__",
649
+ Meta: "__meta__",
650
+ Slot: "__slot__"
651
+ };
652
+ /**
653
+ * Gap types used in markup descriptors
654
+ * Represents the content type in gaps between segments
655
+ */
656
+ const GAP_TYPE = {
657
+ Value: "value",
658
+ Meta: "meta",
659
+ Slot: "slot"
660
+ };
865
661
  //#endregion
866
- //#region ../../core/src/features/caret/selection.ts
867
- function enableSelection(store) {
868
- let pressedNode = null;
869
- let isPressed = false;
870
- const scope = effectScope(() => {
871
- listen(document, "mousedown", (e) => {
872
- pressedNode = nodeTarget(e);
873
- isPressed = true;
874
- });
875
- listen(document, "mousemove", (e) => {
876
- const container = store.slots.container();
877
- if (!container) return;
878
- const currentIsPressed = isPressed;
879
- const isNotInnerSome = !container.contains(pressedNode) || pressedNode !== e.target;
880
- const isInside = window.getSelection()?.containsNode(container, true);
881
- if (currentIsPressed && isNotInnerSome && isInside) {
882
- if (store.caret.selecting() !== "drag") store.caret.selecting("drag");
883
- }
884
- });
885
- listen(document, "mouseup", () => {
886
- isPressed = false;
887
- pressedNode = null;
888
- if (store.caret.selecting() === "drag") {
889
- const sel = window.getSelection();
890
- if (!sel || sel.isCollapsed) store.caret.selecting(void 0);
891
- }
892
- });
893
- listen(document, "selectionchange", () => {
894
- if (store.caret.selecting() !== "drag") return;
895
- const sel = window.getSelection();
896
- if (!sel || sel.isCollapsed) store.caret.selecting(void 0);
897
- });
898
- alienEffect(() => {
899
- if (store.caret.selecting() !== "drag") return;
900
- const container = store.slots.container();
901
- if (!container) return;
902
- container.querySelectorAll("[contenteditable=\"true\"]").forEach((el) => el.contentEditable = "false");
903
- });
904
- });
905
- return () => {
906
- if (store.caret.selecting() === "drag") store.caret.selecting(void 0);
907
- scope();
908
- pressedNode = null;
909
- isPressed = false;
662
+ //#region ../../core/src/features/parsing/parser/core/MarkupDescriptor.ts
663
+ /**
664
+ * Creates a segment-based markup descriptor from a markup template
665
+ *
666
+ * Examples:
667
+ * - `#[__value__]` -> segments: ["#[", "]"], gapTypes: ["value"]
668
+ * - `#[__slot__]` -> segments: ["#[", "]"], gapTypes: ["slot"]
669
+ * - `@[__value__](__meta__)` -> segments: ["@[", "](", ")"], gapTypes: ["value", "meta"]
670
+ * - `@[__slot__](__meta__)` -> segments: ["@[", "](", ")"], gapTypes: ["slot", "meta"]
671
+ * - `@[__value__](__slot__)` -> segments: ["@[", "](", ")"], gapTypes: ["value", "slot"]
672
+ * - `<__value__>__meta__</__value__>` -> segments: [{pattern: '<([^>]+)>'}, {pattern: '</([^>]+)>'}], gapTypes: ["value", "meta", "value"] (dynamic)
673
+ * - `<__value__ __meta__>__slot__</__value__>` -> segments: [{pattern: '<([^> ]+) '}, " ", {pattern: '>__slot__</([^>]+)>'}], gapTypes: ["value", "meta", "slot", "value"] (dynamic)
674
+ */
675
+ function createMarkupDescriptor(markup, index) {
676
+ const { segments: rawSegments, gapTypes: rawGapTypes, counts, valueGapIndices } = scanMarkupStructure(markup);
677
+ validateMarkup(counts, markup);
678
+ const hasTwoValues = counts.value === 2;
679
+ const { segments, gapTypes } = hasTwoValues ? convertTwoValuePattern(rawSegments, rawGapTypes, valueGapIndices) : {
680
+ segments: rawSegments,
681
+ gapTypes: rawGapTypes
682
+ };
683
+ return {
684
+ markup,
685
+ index,
686
+ segments,
687
+ gapTypes,
688
+ hasSlot: counts.slot === 1,
689
+ hasTwoValues,
690
+ segmentGlobalIndices: Array.from({ length: segments.length })
910
691
  };
911
692
  }
912
- //#endregion
913
- //#region ../../core/src/features/caret/CaretFeature.ts
914
- var CaretFeature = class {
915
- #disposers = [];
916
- constructor(_store) {
917
- this._store = _store;
918
- this.recovery = signal(void 0);
919
- this.selecting = signal(void 0);
920
- }
921
- enable() {
922
- if (this.#disposers.length) return;
923
- this.#disposers = [enableFocus(this._store), enableSelection(this._store)];
924
- }
925
- disable() {
926
- this.#disposers.forEach((d) => d());
927
- this.#disposers = [];
928
- }
693
+ /**
694
+ * Maps placeholder types to their text representations
695
+ */
696
+ const PLACEHOLDER_TEXT = {
697
+ [GAP_TYPE.Value]: PLACEHOLDER.Value,
698
+ [GAP_TYPE.Meta]: PLACEHOLDER.Meta,
699
+ [GAP_TYPE.Slot]: PLACEHOLDER.Slot
929
700
  };
930
- //#endregion
931
- //#region ../../core/src/features/caret/selectionHelpers.ts
932
- function isFullSelection(store) {
933
- const sel = window.getSelection();
934
- const container = store.slots.container();
935
- if (!sel?.rangeCount || !container?.firstChild || !container.lastChild) return false;
936
- try {
937
- const range = sel.getRangeAt(0);
938
- return container.contains(range.startContainer) && container.contains(range.endContainer) && range.toString().length > 0;
939
- } catch {
940
- return false;
701
+ /**
702
+ * Parses markup template into segments, gap types and placeholder counts
703
+ */
704
+ function scanMarkupStructure(markup) {
705
+ const segments = [];
706
+ const gapTypes = [];
707
+ const valueGapIndices = [];
708
+ const counts = {
709
+ value: 0,
710
+ meta: 0,
711
+ slot: 0
712
+ };
713
+ const placeholders = [];
714
+ const placeholderTypes = [
715
+ GAP_TYPE.Value,
716
+ GAP_TYPE.Meta,
717
+ GAP_TYPE.Slot
718
+ ];
719
+ for (const type of placeholderTypes) {
720
+ const text = PLACEHOLDER_TEXT[type];
721
+ let position = markup.indexOf(text);
722
+ while (position !== -1) {
723
+ placeholders.push({
724
+ type,
725
+ position
726
+ });
727
+ position = markup.indexOf(text, position + text.length);
728
+ }
941
729
  }
942
- }
943
- function selectAllText(store, event) {
944
- if ((event.ctrlKey || event.metaKey) && event.code === "KeyA") {
945
- if (store.slots.isBlock()) return;
946
- event.preventDefault();
947
- const selection = window.getSelection();
948
- const anchorNode = store.slots.container()?.firstChild;
949
- const focusNode = store.slots.container()?.lastChild;
950
- if (!selection || !anchorNode || !focusNode) return;
951
- selection.setBaseAndExtent(anchorNode, 0, focusNode, 1);
952
- store.caret.selecting("all");
953
- }
954
- }
955
- //#endregion
956
- //#region ../../core/src/shared/escape.ts
957
- const escape = (str) => {
958
- return str.replace(/[.*+?^${}()|[\]\\\\]/g, "\\$&");
959
- };
960
- //#endregion
961
- //#region ../../core/src/features/caret/TriggerFinder.ts
962
- /** Regex to match word characters from the start of a string */
963
- const wordRegex = /* @__PURE__ */ new RegExp(/^\w*/);
964
- var TriggerFinder = class TriggerFinder {
965
- constructor() {
966
- const caretPosition = Caret.getCurrentPosition();
967
- this.node = Caret.getSelectedNode();
968
- this.span = Caret.getFocusedSpan();
969
- this.dividedText = this.getDividedTextBy(caretPosition);
970
- }
971
- /**
972
- * Find overlay match in text using provided options and trigger extractor.
973
- * @template T - Type of option objects
974
- * @param options - Array of options to search through
975
- * @param getTrigger - Function that extracts trigger from each option
976
- * @returns OverlayMatch with correct option type or undefined
977
- *
978
- * @example
979
- * // React usage
980
- * TriggerFinder.find(options, (opt) => opt.slotProps?.overlay?.trigger ?? '@')
981
- *
982
- * @example
983
- * // Other framework usage
984
- * TriggerFinder.find(vueOptions, (opt) => opt.overlay?.trigger ?? '@')
985
- */
986
- static find(options, getTrigger) {
987
- if (!options) return;
988
- if (!Caret.isSelectedPosition) return;
989
- try {
990
- return new TriggerFinder().find(options, getTrigger);
991
- } catch {
992
- return;
993
- }
994
- }
995
- getDividedTextBy(position) {
996
- return {
997
- left: this.span.slice(0, position),
998
- right: this.span.slice(position)
999
- };
1000
- }
1001
- /**
1002
- * Find overlay match in provided options.
1003
- * @template T - Type of option objects
1004
- * @param options - Array of options
1005
- * @param getTrigger - Function to extract trigger from each option
1006
- */
1007
- find(options, getTrigger) {
1008
- for (let i = 0; i < options.length; i++) {
1009
- const option = options[i];
1010
- const trigger = getTrigger(option, i);
1011
- if (!trigger) continue;
1012
- const match = this.matchInTextVia(trigger);
1013
- if (match) return {
1014
- value: match.word,
1015
- source: match.annotation,
1016
- index: match.index,
1017
- span: this.span,
1018
- node: this.node,
1019
- option
1020
- };
1021
- }
1022
- }
1023
- matchInTextVia(trigger = "@") {
1024
- const rightMatch = this.matchRightPart();
1025
- const leftMatch = this.matchLeftPart(trigger);
1026
- if (leftMatch) return {
1027
- word: leftMatch.word + rightMatch.word,
1028
- annotation: leftMatch.annotation + rightMatch.word,
1029
- index: leftMatch.index
1030
- };
1031
- }
1032
- matchRightPart() {
1033
- const { right } = this.dividedText;
1034
- return { word: right.match(wordRegex)?.[0] };
1035
- }
1036
- matchLeftPart(trigger) {
1037
- const regex = this.makeTriggerRegex(trigger);
1038
- const { left } = this.dividedText;
1039
- const match = left.match(regex);
1040
- if (!match) return;
1041
- const [annotation, word] = match;
1042
- return {
1043
- word,
1044
- annotation,
1045
- index: match.index ?? 0
1046
- };
1047
- }
1048
- makeTriggerRegex(trigger) {
1049
- const patten = escape(trigger) + "(\\w*)$";
1050
- return new RegExp(patten);
1051
- }
1052
- };
1053
- //#endregion
1054
- //#region ../../core/src/shared/classes/NodeProxy.ts
1055
- var NodeProxy = class NodeProxy {
1056
- #target;
1057
- #store;
1058
- get target() {
1059
- return this.#target;
1060
- }
1061
- set target(value) {
1062
- this.#target = isHtmlElement(value) ? value : void 0;
1063
- }
1064
- get next() {
1065
- return new NodeProxy(this.target?.nextSibling, this.#store);
1066
- }
1067
- get prev() {
1068
- return new NodeProxy(this.target?.previousSibling, this.#store);
1069
- }
1070
- get isSpan() {
1071
- return this.index % 2 === 0;
1072
- }
1073
- get isMark() {
1074
- return !this.isSpan;
1075
- }
1076
- get isEditable() {
1077
- return this.target?.isContentEditable ?? false;
1078
- }
1079
- get isCaretAtBeginning() {
1080
- if (!this.target) return false;
1081
- return Caret.getCaretIndex(this.target) === 0;
1082
- }
1083
- get isCaretAtEnd() {
1084
- if (!this.target) return false;
1085
- return Caret.getCaretIndex(this.target) === this.target.textContent.length;
1086
- }
1087
- get index() {
1088
- if (!this.target?.parentElement) return -1;
1089
- return [...this.target.parentElement.children].indexOf(this.target);
1090
- }
1091
- get caret() {
1092
- if (this.target) return Caret.getCaretIndex(this.target);
1093
- return -1;
1094
- }
1095
- set caret(value) {
1096
- if (this.target) Caret.trySetIndex(this.target, value);
1097
- }
1098
- get length() {
1099
- return this.target?.textContent.length ?? -1;
1100
- }
1101
- get content() {
1102
- return this.target?.textContent ?? "";
1103
- }
1104
- set content(value) {
1105
- if (this.target) this.target.textContent = value ?? "";
1106
- }
1107
- get head() {
1108
- return firstHtmlChild(this.#store.slots.container() ?? void 0);
1109
- }
1110
- get tail() {
1111
- return lastHtmlChild(this.#store.slots.container() ?? void 0);
1112
- }
1113
- get isFocused() {
1114
- return this.target === document.activeElement;
1115
- }
1116
- constructor(target, store) {
1117
- this.target = target;
1118
- this.#store = store;
1119
- }
1120
- setCaretToEnd() {
1121
- Caret.setCaretToEnd(this.target);
1122
- }
1123
- focus() {
1124
- this.target?.focus();
1125
- }
1126
- clear() {
1127
- this.target = void 0;
1128
- }
1129
- };
1130
- //#endregion
1131
- //#region ../../core/src/features/parsing/parser/constants.ts
1132
- /**
1133
- * Constants for ParserV2 - modern markup parser with nesting support
1134
- *
1135
- * This module contains the placeholder constants used by ParserV2.
1136
- * Unlike the legacy Parser, ParserV2 supports:
1137
- * - `__value__` - main content (replaces `__label__`)
1138
- * - `__meta__` - metadata (replaces `__value__`)
1139
- * - `__slot__` - nested content (new feature)
1140
- *
1141
- * For legacy Parser compatibility, see ../Parser/constants.ts
1142
- * For Markup types, see ./types.ts
1143
- */
1144
- const PLACEHOLDER = {
1145
- Value: "__value__",
1146
- Meta: "__meta__",
1147
- Slot: "__slot__"
1148
- };
1149
- /**
1150
- * Gap types used in markup descriptors
1151
- * Represents the content type in gaps between segments
1152
- */
1153
- const GAP_TYPE = {
1154
- Value: "value",
1155
- Meta: "meta",
1156
- Slot: "slot"
1157
- };
1158
- //#endregion
1159
- //#region ../../core/src/features/parsing/parser/core/MarkupDescriptor.ts
1160
- /**
1161
- * Creates a segment-based markup descriptor from a markup template
1162
- *
1163
- * Examples:
1164
- * - `#[__value__]` -> segments: ["#[", "]"], gapTypes: ["value"]
1165
- * - `#[__slot__]` -> segments: ["#[", "]"], gapTypes: ["slot"]
1166
- * - `@[__value__](__meta__)` -> segments: ["@[", "](", ")"], gapTypes: ["value", "meta"]
1167
- * - `@[__slot__](__meta__)` -> segments: ["@[", "](", ")"], gapTypes: ["slot", "meta"]
1168
- * - `@[__value__](__slot__)` -> segments: ["@[", "](", ")"], gapTypes: ["value", "slot"]
1169
- * - `<__value__>__meta__</__value__>` -> segments: [{pattern: '<([^>]+)>'}, {pattern: '</([^>]+)>'}], gapTypes: ["value", "meta", "value"] (dynamic)
1170
- * - `<__value__ __meta__>__slot__</__value__>` -> segments: [{pattern: '<([^> ]+) '}, " ", {pattern: '>__slot__</([^>]+)>'}], gapTypes: ["value", "meta", "slot", "value"] (dynamic)
1171
- */
1172
- function createMarkupDescriptor(markup, index) {
1173
- const { segments: rawSegments, gapTypes: rawGapTypes, counts, valueGapIndices } = scanMarkupStructure(markup);
1174
- validateMarkup(counts, markup);
1175
- const hasTwoValues = counts.value === 2;
1176
- const { segments, gapTypes } = hasTwoValues ? convertTwoValuePattern(rawSegments, rawGapTypes, valueGapIndices) : {
1177
- segments: rawSegments,
1178
- gapTypes: rawGapTypes
1179
- };
1180
- return {
1181
- markup,
1182
- index,
1183
- segments,
1184
- gapTypes,
1185
- hasSlot: counts.slot === 1,
1186
- hasTwoValues,
1187
- segmentGlobalIndices: Array.from({ length: segments.length })
1188
- };
1189
- }
1190
- /**
1191
- * Maps placeholder types to their text representations
1192
- */
1193
- const PLACEHOLDER_TEXT = {
1194
- [GAP_TYPE.Value]: PLACEHOLDER.Value,
1195
- [GAP_TYPE.Meta]: PLACEHOLDER.Meta,
1196
- [GAP_TYPE.Slot]: PLACEHOLDER.Slot
1197
- };
1198
- /**
1199
- * Parses markup template into segments, gap types and placeholder counts
1200
- */
1201
- function scanMarkupStructure(markup) {
1202
- const segments = [];
1203
- const gapTypes = [];
1204
- const valueGapIndices = [];
1205
- const counts = {
1206
- value: 0,
1207
- meta: 0,
1208
- slot: 0
1209
- };
1210
- const placeholders = [];
1211
- const placeholderTypes = [
1212
- GAP_TYPE.Value,
1213
- GAP_TYPE.Meta,
1214
- GAP_TYPE.Slot
1215
- ];
1216
- for (const type of placeholderTypes) {
1217
- const text = PLACEHOLDER_TEXT[type];
1218
- let position = markup.indexOf(text);
1219
- while (position !== -1) {
1220
- placeholders.push({
1221
- type,
1222
- position
1223
- });
1224
- position = markup.indexOf(text, position + text.length);
1225
- }
1226
- }
1227
- placeholders.sort((a, b) => a.position - b.position);
1228
- let currentParsePosition = 0;
1229
- for (const placeholder of placeholders) {
1230
- const segment = markup.substring(currentParsePosition, placeholder.position);
1231
- if (segment.length > 0) segments.push(segment);
1232
- gapTypes.push(placeholder.type);
1233
- counts[placeholder.type]++;
1234
- if (placeholder.type === GAP_TYPE.Value) valueGapIndices.push(gapTypes.length - 1);
1235
- currentParsePosition = placeholder.position + PLACEHOLDER_TEXT[placeholder.type].length;
730
+ placeholders.sort((a, b) => a.position - b.position);
731
+ let currentParsePosition = 0;
732
+ for (const placeholder of placeholders) {
733
+ const segment = markup.substring(currentParsePosition, placeholder.position);
734
+ if (segment.length > 0) segments.push(segment);
735
+ gapTypes.push(placeholder.type);
736
+ counts[placeholder.type]++;
737
+ if (placeholder.type === GAP_TYPE.Value) valueGapIndices.push(gapTypes.length - 1);
738
+ currentParsePosition = placeholder.position + PLACEHOLDER_TEXT[placeholder.type].length;
1236
739
  }
1237
740
  const finalSegment = markup.substring(currentParsePosition);
1238
741
  if (finalSegment.length > 0) segments.push(finalSegment);
@@ -1640,6 +1143,11 @@ var PatternMatcher = class {
1640
1143
  }
1641
1144
  };
1642
1145
  //#endregion
1146
+ //#region ../../core/src/shared/escape.ts
1147
+ const escape = (str) => {
1148
+ return str.replace(/[.*+?^${}()|[\]\\\\]/g, "\\$&");
1149
+ };
1150
+ //#endregion
1643
1151
  //#region ../../core/src/features/parsing/parser/core/SegmentMatcher.ts
1644
1152
  /**
1645
1153
  * Computes regex pattern for dynamic segment using pre-computed exclusions
@@ -1986,11 +1494,7 @@ function processTokensWithCallback(tokens, callback) {
1986
1494
  * ```
1987
1495
  */
1988
1496
  function annotate(markup, params) {
1989
- let annotation = markup;
1990
- if (params.value !== void 0) annotation = annotation.replaceAll(PLACEHOLDER.Value, params.value);
1991
- if (params.meta !== void 0) annotation = annotation.replaceAll(PLACEHOLDER.Meta, params.meta);
1992
- if (params.slot !== void 0) annotation = annotation.replaceAll(PLACEHOLDER.Slot, params.slot);
1993
- return annotation;
1497
+ return markup.replaceAll(PLACEHOLDER.Value, params.value ?? "").replaceAll(PLACEHOLDER.Meta, params.meta ?? "").replaceAll(PLACEHOLDER.Slot, params.slot ?? "");
1994
1498
  }
1995
1499
  //#endregion
1996
1500
  //#region ../../core/src/features/parsing/parser/utils/toString.ts
@@ -2259,162 +1763,493 @@ function findToken(tokens, target, depth = 0, parent) {
2259
1763
  }
2260
1764
  }
2261
1765
  //#endregion
2262
- //#region ../../core/src/features/parsing/preparsing/utils/findGap.ts
2263
- function findGap(previous = "", current = "") {
2264
- if (previous === current) return {};
2265
- let left;
2266
- for (let i = 0; i < previous.length; i++) if (previous[i] !== current[i]) {
2267
- left = i;
2268
- break;
2269
- }
2270
- let right;
2271
- for (let i = 1; i <= previous.length; i++) if (previous.at(-i) !== current.at(-i)) {
2272
- right = previous.length - i + 1;
2273
- break;
2274
- }
2275
- return {
2276
- left,
2277
- right
2278
- };
1766
+ //#region ../../core/src/features/parsing/tokenIndex.ts
1767
+ function pathEquals(a, b) {
1768
+ return a.length === b.length && a.every((part, index) => part === b[index]);
2279
1769
  }
2280
- //#endregion
2281
- //#region ../../core/src/features/parsing/preparsing/utils/getClosestIndexes.ts
2282
- function getClosestIndexes(array, target) {
2283
- let left = -1, right = array.length;
2284
- while (right - left > 1) {
2285
- const middle = Math.round((left + right) / 2);
2286
- if (array[middle] <= target) left = middle;
2287
- else right = middle;
2288
- }
2289
- if (array[left] == target) right = left;
2290
- return [left, right].filter((v) => array[v] !== void 0);
1770
+ function pathKey(path) {
1771
+ return path.join(".");
2291
1772
  }
2292
- //#endregion
2293
- //#region ../../core/src/features/parsing/utils/valueParser.ts
2294
- function getTokensByUI(store) {
2295
- const { focus } = store.nodes;
2296
- const parser = store.parsing.parser();
2297
- const tokens = store.parsing.tokens();
2298
- if (!parser) return tokens;
2299
- const parsed = parser.parse(focus.content);
2300
- if (parsed.length <= 1) return tokens;
2301
- return tokens.toSpliced(focus.index, 1, ...parsed);
2302
- }
2303
- function computeTokensFromValue(store) {
2304
- const value = store.props.value();
2305
- const previousValue = store.value.last();
2306
- const gap = findGap(previousValue, value);
2307
- if (!gap.left && !gap.right) {
2308
- store.value.last(value);
2309
- return store.parsing.tokens();
2310
- }
2311
- if (gap.left === 0 && previousValue !== void 0 && gap.right !== void 0 && gap.right >= previousValue.length) {
2312
- store.value.last(value);
2313
- return parseWithParser(store, value ?? "");
2314
- }
2315
- store.value.last(value);
2316
- const ranges = getRangeMap(store);
2317
- const tokens = store.parsing.tokens();
2318
- if (gap.left !== void 0 && ranges.includes(gap.left) && gap.right !== void 0 && Math.abs(gap.left - gap.right) > 1) {
2319
- const updatedIndex = ranges.indexOf(gap.left);
2320
- if (updatedIndex > 0) {
2321
- const parsed = parseUnionLabels(store, updatedIndex - 1, updatedIndex);
2322
- return tokens.toSpliced(updatedIndex - 1, 2, ...parsed);
2323
- }
2324
- }
2325
- if (gap.left !== void 0) {
2326
- const [updatedIndex] = getClosestIndexes(ranges, gap.left);
2327
- const parsed = parseUnionLabels(store, updatedIndex);
2328
- if (parsed.length === 1) return tokens;
2329
- return tokens.toSpliced(updatedIndex, 1, ...parsed);
2330
- }
2331
- if (gap.right !== void 0) {
2332
- const [updatedIndex] = getClosestIndexes(ranges, gap.right);
2333
- const parsed = parseUnionLabels(store, updatedIndex);
2334
- if (parsed.length === 1) return tokens;
2335
- return tokens.toSpliced(updatedIndex, 1, ...parsed);
2336
- }
2337
- return parseWithParser(store, value ?? "");
1773
+ function resolvePath(tokens, path) {
1774
+ if (path.length === 0) return void 0;
1775
+ let current = tokens;
1776
+ let token;
1777
+ for (const index of path) {
1778
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) return void 0;
1779
+ token = current[index];
1780
+ current = token.type === "mark" ? token.children : [];
1781
+ }
1782
+ return token;
2338
1783
  }
2339
- function parseUnionLabels(store, ...indexes) {
2340
- let span = "";
2341
- const tokens = store.parsing.tokens();
2342
- for (const index of indexes) {
2343
- const token = tokens[index];
2344
- span += token.content;
2345
- }
2346
- return parseWithParser(store, span);
1784
+ function snapshotTokenShape(token) {
1785
+ if (token.type === "text") return { kind: "text" };
1786
+ return {
1787
+ kind: "mark",
1788
+ descriptor: token.descriptor,
1789
+ descriptorIndex: token.descriptor.index
1790
+ };
2347
1791
  }
2348
- function getRangeMap(store) {
2349
- let position = 0;
2350
- return store.parsing.tokens().map((token) => {
2351
- const length = token.content.length;
2352
- position += length;
2353
- return position - length;
2354
- });
1792
+ function shapeMatches(token, expected) {
1793
+ if (expected.kind === "text") return token.type === "text";
1794
+ return token.type === "mark" && token.descriptor === expected.descriptor && token.descriptor.index === expected.descriptorIndex;
2355
1795
  }
2356
- function parseWithParser(store, value) {
2357
- const parser = store.parsing.parser();
2358
- if (!parser) return [{
2359
- type: "text",
2360
- content: value,
2361
- position: {
2362
- start: 0,
2363
- end: value.length
2364
- }
2365
- }];
2366
- return parser.parse(value);
1796
+ function createTokenIndex(tokens, generation) {
1797
+ const paths = /* @__PURE__ */ new WeakMap();
1798
+ const visit = (items, parent) => {
1799
+ items.forEach((token, index) => {
1800
+ const path = [...parent, index];
1801
+ paths.set(token, path);
1802
+ if (token.type === "mark") visit(token.children, path);
1803
+ });
1804
+ };
1805
+ visit(tokens, []);
1806
+ return {
1807
+ generation,
1808
+ pathFor: (token) => paths.get(token),
1809
+ addressFor: (path) => resolvePath(tokens, path) ? {
1810
+ path: [...path],
1811
+ parseGeneration: generation
1812
+ } : void 0,
1813
+ resolve: (path) => resolvePath(tokens, path),
1814
+ resolveAddress(address, expected) {
1815
+ if (address.parseGeneration !== generation) return {
1816
+ ok: false,
1817
+ reason: "stale"
1818
+ };
1819
+ const token = resolvePath(tokens, address.path);
1820
+ if (!token) return {
1821
+ ok: false,
1822
+ reason: "stale"
1823
+ };
1824
+ if (expected && !shapeMatches(token, expected)) return {
1825
+ ok: false,
1826
+ reason: "stale"
1827
+ };
1828
+ return {
1829
+ ok: true,
1830
+ value: token
1831
+ };
1832
+ },
1833
+ key: pathKey,
1834
+ equals: pathEquals
1835
+ };
2367
1836
  }
2368
1837
  //#endregion
2369
- //#region ../../core/src/features/parsing/ParseFeature.ts
2370
- var ParsingFeature = class {
1838
+ //#region ../../core/src/features/parsing/ParseController.ts
1839
+ var ParseController = class {
1840
+ #generation = signal(0);
2371
1841
  #scope;
2372
- constructor(_store) {
2373
- this._store = _store;
1842
+ constructor(lifecycle, value, mark, props, slots) {
1843
+ this.lifecycle = lifecycle;
1844
+ this.value = value;
1845
+ this.mark = mark;
1846
+ this.props = props;
1847
+ this.slots = slots;
2374
1848
  this.tokens = signal([]);
1849
+ this.index = computed(() => createTokenIndex(this.tokens(), this.#generation()));
2375
1850
  this.parser = computed(() => {
2376
- if (!this._store.mark.enabled()) return;
2377
- const markups = this._store.props.options().map((opt) => opt.markup);
1851
+ if (!this.mark.enabled()) return;
1852
+ const markups = this.props.options().map((opt) => opt.markup);
2378
1853
  if (!markups.some(Boolean)) return;
2379
- return new Parser(markups, this._store.slots.isBlock() ? { skipEmptyText: true } : void 0);
1854
+ return new Parser(markups, this.slots.isBlock() ? { skipEmptyText: true } : void 0);
2380
1855
  });
2381
1856
  this.reparse = event();
2382
- }
2383
- enable() {
2384
- if (this.#scope) return;
2385
- this.sync();
2386
- this.#scope = effectScope(() => {
2387
- this.#subscribeParse();
2388
- this.#subscribeReactiveParse();
1857
+ lifecycle.onMounted(() => {
1858
+ this.acceptTokens(this.#parseValue(value.current()));
1859
+ this.#subscribeValue();
2389
1860
  });
1861
+ const toggle = (enabled) => {
1862
+ if (enabled && !this.#scope) this.#scope = effectScope(() => {
1863
+ this.#subscribeReactiveParse();
1864
+ this.#subscribeReparse();
1865
+ });
1866
+ if (!enabled && this.#scope) {
1867
+ this.#scope();
1868
+ this.#scope = void 0;
1869
+ }
1870
+ };
1871
+ watch(this.mark.enabled, toggle);
1872
+ toggle(this.mark.enabled());
2390
1873
  }
2391
- disable() {
2392
- this.#scope?.();
2393
- this.#scope = void 0;
2394
- }
2395
- sync() {
2396
- const inputValue = this._store.props.value() ?? this._store.props.defaultValue() ?? "";
2397
- this.tokens(parseWithParser(this._store, inputValue));
2398
- this._store.value.last(inputValue);
1874
+ acceptTokens(tokens) {
1875
+ batch(() => {
1876
+ this.tokens(tokens);
1877
+ this.#generation(this.#generation() + 1);
1878
+ }, { mutable: true });
1879
+ }
1880
+ #parseValue(value) {
1881
+ const parser = this.parser();
1882
+ if (!parser) return [{
1883
+ type: "text",
1884
+ content: value,
1885
+ position: {
1886
+ start: 0,
1887
+ end: value.length
1888
+ }
1889
+ }];
1890
+ return parser.parse(value);
1891
+ }
1892
+ #subscribeValue() {
1893
+ watch(this.value.current, (v) => {
1894
+ this.acceptTokens(this.#parseValue(v));
1895
+ });
1896
+ }
1897
+ #subscribeReactiveParse() {
1898
+ watch(computed(() => this.parser()), () => {
1899
+ this.acceptTokens(this.#parseValue(this.value.current()));
1900
+ });
2399
1901
  }
2400
- #subscribeParse() {
1902
+ #subscribeReparse() {
2401
1903
  watch(this.reparse, () => {
2402
- if (this._store.caret.recovery()) {
2403
- const text = toString(this.tokens());
2404
- this.tokens(parseWithParser(this._store, text));
2405
- this._store.value.last(text);
1904
+ this.acceptTokens(this.#parseValue(this.value.current()));
1905
+ });
1906
+ }
1907
+ };
1908
+ //#endregion
1909
+ //#region ../../core/src/shared/checkers/domGuards.ts
1910
+ /** Type guard: checks if a value is an HTMLElement. */
1911
+ function isHtmlElement(el) {
1912
+ return typeof HTMLElement !== "undefined" && el instanceof HTMLElement;
1913
+ }
1914
+ /** Get all children of an element as HTMLElement[], filtering out non-HTML elements. */
1915
+ function htmlChildren(parent) {
1916
+ if (!parent) return [];
1917
+ return Array.from(parent.children).filter((child) => child instanceof HTMLElement);
1918
+ }
1919
+ /** Get the first element child as HTMLElement, or null. */
1920
+ function firstHtmlChild(parent) {
1921
+ const child = parent?.firstElementChild;
1922
+ return child instanceof HTMLElement ? child : null;
1923
+ }
1924
+ /** Safely narrow an event's target to Node. */
1925
+ function nodeTarget(event) {
1926
+ const { target } = event;
1927
+ return target instanceof Node ? target : null;
1928
+ }
1929
+ /** Get the next node from a TreeWalker as Text, or null. */
1930
+ function nextText(walker) {
1931
+ const node = walker.nextNode();
1932
+ return node?.nodeType === 3 ? node : null;
1933
+ }
1934
+ //#endregion
1935
+ //#region ../../core/src/features/caret/CaretModel.ts
1936
+ var CaretModel = class {
1937
+ constructor(lifecycle, dom, value) {
1938
+ this.lifecycle = lifecycle;
1939
+ this.dom = dom;
1940
+ this.value = value;
1941
+ this.selection = signal(void 0, { equals: shallow });
1942
+ this.position = computed({
1943
+ get: () => this.selection()?.start,
1944
+ set: (value) => this.selection(value !== void 0 ? {
1945
+ start: value,
1946
+ end: value
1947
+ } : void 0)
1948
+ });
1949
+ this.isUserSelecting = signal(false);
1950
+ this.isAllSelected = computed(() => {
1951
+ const s = this.selection();
1952
+ const v = this.value.current();
1953
+ return s?.start === 0 && s.end === v.length && v.length > 0;
1954
+ });
1955
+ lifecycle.onMounted(() => {
1956
+ this.#enableFocusTracking();
1957
+ this.#enableSelectionTracking();
1958
+ watch(dom.indexed, () => {
1959
+ dom.reconcile({ isUserSelecting: this.isUserSelecting() });
1960
+ this.#applyRangeToDOM();
1961
+ });
1962
+ effect(() => {
1963
+ const isUserSelecting = this.isUserSelecting();
1964
+ dom.readOnly();
1965
+ dom.reconcile({ isUserSelecting });
1966
+ });
1967
+ });
1968
+ }
1969
+ selectAll() {
1970
+ this.selection({
1971
+ start: 0,
1972
+ end: this.value.current().length
1973
+ });
1974
+ this.#applyRangeToDOM();
1975
+ }
1976
+ #enableFocusTracking() {
1977
+ const container = this.dom.container();
1978
+ if (!container) return;
1979
+ listen(container, "focusin", (e) => {
1980
+ const target = e.target instanceof HTMLElement ? e.target : void 0;
1981
+ if (!target) {
1982
+ this.selection(void 0);
1983
+ return;
1984
+ }
1985
+ const result = this.dom.locateNode(target);
1986
+ if (!result.ok) {
1987
+ if (result.reason === "control") return;
1988
+ this.selection(void 0);
2406
1989
  return;
2407
1990
  }
2408
- this.tokens(this._store.nodes.focus.target ? getTokensByUI(this._store) : computeTokensFromValue(this._store));
1991
+ const rawSel = this.dom.readRawSelection();
1992
+ if (rawSel.ok) this.selection(rawSel.value.range);
1993
+ });
1994
+ listen(container, "focusout", () => {
1995
+ queueMicrotask(() => {
1996
+ if (!container.contains(document.activeElement)) this.selection(void 0);
1997
+ });
2409
1998
  });
2410
1999
  }
2411
- #subscribeReactiveParse() {
2412
- watch(computed(() => [this._store.props.value(), this.parser()]), () => {
2413
- if (!this._store.caret.recovery()) this.reparse();
2000
+ #enableSelectionTracking() {
2001
+ let pressedAt = null;
2002
+ listen(document, "mousedown", (e) => {
2003
+ pressedAt = nodeTarget(e);
2004
+ });
2005
+ listen(document, "mousemove", (e) => {
2006
+ if (pressedAt === null) return;
2007
+ const container = this.dom.container();
2008
+ if (!container) return;
2009
+ const startedOutsideEditor = !container.contains(pressedAt);
2010
+ const sweepingAcrossNodes = pressedAt !== e.target;
2011
+ const selectionIntersectsEditor = window.getSelection()?.containsNode(container, true) ?? false;
2012
+ if ((startedOutsideEditor || sweepingAcrossNodes) && selectionIntersectsEditor) this.isUserSelecting(true);
2013
+ });
2014
+ listen(document, "mouseup", () => {
2015
+ pressedAt = null;
2016
+ if (!this.isUserSelecting()) return;
2017
+ const sel = window.getSelection();
2018
+ if (!sel || sel.isCollapsed) this.isUserSelecting(false);
2019
+ });
2020
+ listen(document, "selectionchange", () => {
2021
+ const sel = window.getSelection();
2022
+ if (this.isUserSelecting() && (!sel || sel.isCollapsed)) this.isUserSelecting(false);
2023
+ if (!sel?.focusNode) return;
2024
+ const result = this.dom.locateNode(sel.focusNode);
2025
+ if (!result.ok) {
2026
+ if (result.reason === "control") return;
2027
+ this.selection(void 0);
2028
+ return;
2029
+ }
2030
+ const rawSel = this.dom.readRawSelection();
2031
+ if (rawSel.ok) this.selection(rawSel.value.range);
2032
+ else this.selection(void 0);
2414
2033
  });
2415
2034
  }
2035
+ #applyRangeToDOM() {
2036
+ if (this.isUserSelecting()) return;
2037
+ const sel = this.selection();
2038
+ if (sel === void 0) return;
2039
+ if (sel.start === sel.end) {
2040
+ const result = this.dom.placeAt(sel.start);
2041
+ if (!result.ok) {
2042
+ this.selection(void 0);
2043
+ return;
2044
+ }
2045
+ const applied = result.value.applied;
2046
+ if (applied !== sel.start) this.selection({
2047
+ start: applied,
2048
+ end: applied
2049
+ });
2050
+ return;
2051
+ }
2052
+ const result = this.dom.placeRange(sel);
2053
+ if (!result.ok) {
2054
+ this.selection(void 0);
2055
+ return;
2056
+ }
2057
+ this.selection(result.value.applied);
2058
+ }
2416
2059
  };
2417
2060
  //#endregion
2061
+ //#region ../../core/src/features/caret/TriggerFinder.ts
2062
+ /** Regex to match word characters from the start of a string */
2063
+ const wordRegex = /* @__PURE__ */ new RegExp(/^\w*/);
2064
+ var TriggerFinder = class TriggerFinder {
2065
+ constructor(dom) {
2066
+ this.dom = dom;
2067
+ const sel = window.getSelection();
2068
+ const node = sel?.anchorNode;
2069
+ if (!sel || !node || !document.contains(node)) throw new Error("Anchor node of selection is not exists!");
2070
+ this.node = node;
2071
+ this.span = node.textContent ?? "";
2072
+ this.dividedText = this.getDividedTextBy(sel.anchorOffset);
2073
+ }
2074
+ /**
2075
+ * Find overlay match in text using provided options and trigger extractor.
2076
+ * @template T - Type of option objects
2077
+ * @param options - Array of options to search through
2078
+ * @param getTrigger - Function that extracts trigger from each option
2079
+ * @returns OverlayMatch with correct option type or undefined
2080
+ *
2081
+ * @example
2082
+ * // React usage
2083
+ * TriggerFinder.find(options, (opt) => opt.slotProps?.overlay?.trigger ?? '@')
2084
+ *
2085
+ * @example
2086
+ * // Other framework usage
2087
+ * TriggerFinder.find(vueOptions, (opt) => opt.overlay?.trigger ?? '@')
2088
+ */
2089
+ static find(options, getTrigger, dom) {
2090
+ if (!options) return;
2091
+ if (!window.getSelection()?.isCollapsed) return;
2092
+ try {
2093
+ return new TriggerFinder(dom).find(options, getTrigger);
2094
+ } catch {
2095
+ return;
2096
+ }
2097
+ }
2098
+ getDividedTextBy(position) {
2099
+ return {
2100
+ left: this.span.slice(0, position),
2101
+ right: this.span.slice(position)
2102
+ };
2103
+ }
2104
+ /**
2105
+ * Find overlay match in provided options.
2106
+ * @template T - Type of option objects
2107
+ * @param options - Array of options
2108
+ * @param getTrigger - Function to extract trigger from each option
2109
+ */
2110
+ find(options, getTrigger) {
2111
+ for (let i = 0; i < options.length; i++) {
2112
+ const option = options[i];
2113
+ const trigger = getTrigger(option, i);
2114
+ if (!trigger) continue;
2115
+ const match = this.matchInTextVia(trigger);
2116
+ if (match) {
2117
+ const range = this.#rawRangeForMatch(match.annotation, match.index);
2118
+ if (!range) return void 0;
2119
+ return {
2120
+ value: match.word,
2121
+ source: match.annotation,
2122
+ range,
2123
+ span: this.span,
2124
+ node: this.node,
2125
+ option
2126
+ };
2127
+ }
2128
+ }
2129
+ }
2130
+ #rawRangeForMatch(source, index) {
2131
+ if (!this.dom) return {
2132
+ start: index,
2133
+ end: index + source.length
2134
+ };
2135
+ const boundary = this.dom.rawPositionFromBoundary(this.node, index + source.length, "after");
2136
+ if (!boundary.ok) return void 0;
2137
+ return {
2138
+ start: boundary.value - source.length,
2139
+ end: boundary.value
2140
+ };
2141
+ }
2142
+ matchInTextVia(trigger = "@") {
2143
+ const rightMatch = this.matchRightPart();
2144
+ const leftMatch = this.matchLeftPart(trigger);
2145
+ if (leftMatch) return {
2146
+ word: leftMatch.word + rightMatch.word,
2147
+ annotation: leftMatch.annotation + rightMatch.word,
2148
+ index: leftMatch.index
2149
+ };
2150
+ }
2151
+ matchRightPart() {
2152
+ const { right } = this.dividedText;
2153
+ return { word: right.match(wordRegex)?.[0] };
2154
+ }
2155
+ matchLeftPart(trigger) {
2156
+ const regex = this.makeTriggerRegex(trigger);
2157
+ const { left } = this.dividedText;
2158
+ const match = left.match(regex);
2159
+ if (!match) return;
2160
+ const [annotation, word] = match;
2161
+ return {
2162
+ word,
2163
+ annotation,
2164
+ index: match.index ?? 0
2165
+ };
2166
+ }
2167
+ makeTriggerRegex(trigger) {
2168
+ const patten = escape(trigger) + "(\\w*)$";
2169
+ return new RegExp(patten);
2170
+ }
2171
+ };
2172
+ //#endregion
2173
+ //#region ../../core/src/features/caret/caretDom.ts
2174
+ function getCaretIndex(element) {
2175
+ let position = 0;
2176
+ const selection = window.getSelection();
2177
+ if (!selection?.rangeCount) return position;
2178
+ const range = selection.getRangeAt(0);
2179
+ const preCaretRange = range.cloneRange();
2180
+ preCaretRange.selectNodeContents(element);
2181
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
2182
+ position = preCaretRange.toString().length;
2183
+ return position;
2184
+ }
2185
+ function getRect() {
2186
+ try {
2187
+ return (window.getSelection()?.getRangeAt(0))?.getBoundingClientRect() ?? null;
2188
+ } catch {
2189
+ return null;
2190
+ }
2191
+ }
2192
+ function isOnFirstLine(element) {
2193
+ const caretRect = getRect();
2194
+ if (!caretRect || caretRect.height === 0) return true;
2195
+ const elRect = element.getBoundingClientRect();
2196
+ return caretRect.top < elRect.top + caretRect.height + 2;
2197
+ }
2198
+ function isOnLastLine(element) {
2199
+ const caretRect = getRect();
2200
+ if (!caretRect || caretRect.height === 0) return true;
2201
+ const elRect = element.getBoundingClientRect();
2202
+ return caretRect.bottom > elRect.bottom - caretRect.height - 2;
2203
+ }
2204
+ function setAtElement(element, offset) {
2205
+ try {
2206
+ const selection = window.getSelection();
2207
+ if (!selection) return;
2208
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
2209
+ let node = nextText(walker);
2210
+ if (!node) return;
2211
+ let remaining = isFinite(offset) ? Math.max(0, offset) : Infinity;
2212
+ for (;;) {
2213
+ const next = nextText(walker);
2214
+ if (!next || remaining <= node.length) {
2215
+ const charOffset = isFinite(remaining) ? Math.min(remaining, node.length) : node.length;
2216
+ const range = document.createRange();
2217
+ range.setStart(node, charOffset);
2218
+ range.collapse(true);
2219
+ selection.removeAllRanges();
2220
+ selection.addRange(range);
2221
+ return;
2222
+ }
2223
+ remaining -= node.length;
2224
+ node = next;
2225
+ }
2226
+ } catch (e) {
2227
+ console.error(e);
2228
+ }
2229
+ }
2230
+ function setAtX(element, x, y) {
2231
+ const elRect = element.getBoundingClientRect();
2232
+ const targetY = y ?? elRect.top + elRect.height / 2;
2233
+ const caretDoc = document;
2234
+ const caretPos = caretDoc.caretRangeFromPoint?.(x, targetY) ?? caretDoc.caretPositionFromPoint?.(x, targetY);
2235
+ if (!caretPos) return;
2236
+ const sel = window.getSelection();
2237
+ if (!sel) return;
2238
+ let domRange;
2239
+ if (caretPos instanceof Range) domRange = caretPos;
2240
+ else if ("offsetNode" in caretPos) {
2241
+ domRange = document.createRange();
2242
+ domRange.setStart(caretPos.offsetNode, caretPos.offset);
2243
+ domRange.collapse(true);
2244
+ } else return;
2245
+ if (!element.contains(domRange.startContainer)) {
2246
+ setAtElement(element, Infinity);
2247
+ return;
2248
+ }
2249
+ sel.removeAllRanges();
2250
+ sel.addRange(domRange);
2251
+ }
2252
+ //#endregion
2418
2253
  //#region ../../core/src/features/clipboard/pasteMarkup.ts
2419
2254
  /** Custom MIME type for markput markup syntax. */
2420
2255
  const MARKPUT_MIME = "application/x-markput";
@@ -2442,157 +2277,57 @@ function consumeMarkupPaste(container) {
2442
2277
  return markup;
2443
2278
  }
2444
2279
  //#endregion
2445
- //#region ../../core/src/features/clipboard/selectionToTokens.ts
2446
- /**
2447
- * Walk up the DOM from `node` until reaching a direct child of `container`.
2448
- * Returns the index of that child in container.children, or -1 if not found.
2449
- *
2450
- * Works for both drag and non-drag modes:
2451
- * - Non-drag: container children are Token-rendered elements (1:1 with tokens)
2452
- * - Drag: container children are Block wrappers (1:1 with tokens)
2453
- * - Nested marks: walks past inner mark elements to the top-level container child
2454
- */
2455
- function findContainerChildIndex(node, container) {
2456
- let current = node instanceof HTMLElement ? node : node.parentElement;
2457
- while (current && current.parentElement !== container) current = current.parentElement;
2458
- if (!current) return -1;
2459
- return Array.from(container.children).indexOf(current);
2280
+ //#region ../../core/src/features/clipboard/ClipboardController.ts
2281
+ function htmlFromRange(range) {
2282
+ const fragment = range.cloneContents();
2283
+ const div = document.createElement("div");
2284
+ div.appendChild(fragment);
2285
+ return div.innerHTML;
2460
2286
  }
2461
- /**
2462
- * Returns the character offset of a range boundary within a container child.
2463
- * Walks all text nodes in document order to compute a cumulative character
2464
- * offset, which correctly handles nested marks with multiple text nodes.
2465
- * Falls back to 0 (start) or full text length (end) for out-of-child boundaries.
2466
- *
2467
- * Spec: selectionToTokens.spec.ts (unit) · Clipboard.react.spec.tsx (integration)
2468
- */
2469
- function getBoundaryOffset(range, child, isStart) {
2470
- const targetNode = isStart ? range.startContainer : range.endContainer;
2471
- const targetOffset = isStart ? range.startOffset : range.endOffset;
2472
- if (!child.contains(targetNode)) return isStart ? 0 : child.textContent.length;
2473
- let charOffset = 0;
2474
- const walker = document.createTreeWalker(child, NodeFilter.SHOW_TEXT);
2475
- let current = walker.nextNode();
2476
- while (current) {
2477
- if (current === targetNode) return charOffset + targetOffset;
2478
- charOffset += current.length;
2479
- current = walker.nextNode();
2480
- }
2481
- return isStart ? 0 : child.textContent.length;
2482
- }
2483
- /**
2484
- * Map a browser Selection to the subset of tokens it covers.
2485
- * Returns null if selection is collapsed, empty, or outside the container.
2486
- */
2487
- function selectionToTokens(store) {
2488
- const container = store.slots.container();
2489
- if (!container) return null;
2490
- const sel = window.getSelection();
2491
- if (!sel || sel.isCollapsed || !sel.rangeCount) return null;
2492
- const range = sel.getRangeAt(0);
2493
- if (!container.contains(range.startContainer) || !container.contains(range.endContainer)) return null;
2494
- const tokens = store.parsing.tokens();
2495
- let startIndex = findContainerChildIndex(range.startContainer, container);
2496
- let endIndex = findContainerChildIndex(range.endContainer, container);
2497
- if (startIndex === -1 || endIndex === -1) return null;
2498
- if (startIndex > endIndex) [startIndex, endIndex] = [endIndex, startIndex];
2499
- const startChild = container.children.item(startIndex);
2500
- const endChild = container.children.item(endIndex);
2501
- return {
2502
- tokens: tokens.slice(startIndex, endIndex + 1),
2503
- startOffset: startChild ? getBoundaryOffset(range, startChild, true) : 0,
2504
- endOffset: endChild ? getBoundaryOffset(range, endChild, false) : 0
2505
- };
2287
+ function serializeRawRange(tokens, range) {
2288
+ return toString(trimTokensForRawRange(tokens, range));
2506
2289
  }
2507
- //#endregion
2508
- //#region ../../core/src/features/clipboard/ClipboardFeature.ts
2509
- /**
2510
- * Trim boundary text tokens to the selected portion.
2511
- * Mark tokens are always kept in full — partial mark selection expands to full mark.
2512
- *
2513
- * NOTE: Returned text tokens have stale `position` fields — the start/end positions
2514
- * still reflect the original token, not the trimmed content. Only `content` is
2515
- * authoritative on the returned tokens. `toString` is safe because it reads `content`
2516
- * directly; do not use `position` on the returned tokens for any other purpose.
2517
- */
2518
- function trimBoundaryTokens({ tokens, startOffset, endOffset }) {
2519
- if (tokens.length === 0) return tokens;
2520
- return tokens.map((token, i) => {
2521
- if (token.type !== "text") return token;
2522
- const isFirst = i === 0;
2523
- const isLast = i === tokens.length - 1;
2524
- if (isFirst && isLast) return {
2525
- ...token,
2526
- content: token.content.slice(startOffset, endOffset)
2527
- };
2528
- if (isFirst) return {
2529
- ...token,
2530
- content: token.content.slice(startOffset)
2531
- };
2532
- if (isLast) return {
2533
- ...token,
2534
- content: token.content.slice(0, endOffset)
2535
- };
2536
- return token;
2290
+ function trimTokensForRawRange(tokens, range) {
2291
+ return tokens.filter((token) => token.position.end > range.start && token.position.start < range.end).map((token) => {
2292
+ if (token.type === "text") {
2293
+ const start = Math.max(0, range.start - token.position.start);
2294
+ const end = Math.min(token.content.length, range.end - token.position.start);
2295
+ return Object.assign({}, token, { content: token.content.slice(start, end) });
2296
+ }
2297
+ if (token.children.length === 0) return token;
2298
+ return Object.assign({}, token, { children: trimTokensForRawRange(token.children, range) });
2537
2299
  });
2538
2300
  }
2539
- var ClipboardFeature = class {
2540
- #scope;
2541
- constructor(store) {
2542
- this.store = store;
2543
- }
2544
- enable() {
2545
- if (this.#scope) return;
2546
- const container = this.store.slots.container();
2547
- if (!container) return;
2548
- this.#scope = effectScope(() => {
2301
+ var ClipboardController = class {
2302
+ constructor(lifecycle, edit, dom, parsing) {
2303
+ this.lifecycle = lifecycle;
2304
+ this.edit = edit;
2305
+ this.dom = dom;
2306
+ this.parsing = parsing;
2307
+ lifecycle.onMounted(() => {
2308
+ const container = dom.container();
2309
+ if (!container) return;
2549
2310
  listen(container, "copy", (e) => {
2550
2311
  this.#handleCopy(e);
2551
2312
  });
2552
2313
  listen(container, "cut", (e) => {
2553
2314
  if (!this.#handleCopy(e)) return;
2554
- const result = selectionToTokens(this.store);
2555
- if (!result || result.tokens.length === 0) return;
2556
- const first = result.tokens[0];
2557
- const last = result.tokens[result.tokens.length - 1];
2558
- const rawStart = first.type === "text" ? first.position.start + result.startOffset : first.position.start;
2559
- const rawEnd = last.type === "text" ? last.position.start + result.endOffset : last.position.end;
2560
- const value = this.store.value.current();
2561
- if (rawStart === rawEnd) return;
2562
- const newValue = value.slice(0, rawStart) + value.slice(rawEnd);
2563
- this.store.value.next(newValue);
2564
- const newTokens = this.store.parsing.tokens();
2565
- let targetIdx = newTokens.findIndex((t) => t.type === "text" && rawStart >= t.position.start && rawStart <= t.position.end);
2566
- if (targetIdx === -1) targetIdx = newTokens.length - 1;
2567
- const caretWithinToken = rawStart - newTokens[targetIdx].position.start;
2568
- this.store.caret.recovery({
2569
- anchor: this.store.nodes.focus,
2570
- caret: caretWithinToken,
2571
- isNext: true,
2572
- childIndex: targetIdx - 2
2573
- });
2315
+ const raw = dom.readRawSelection();
2316
+ if (!raw.ok || raw.value.range.start === raw.value.range.end) return;
2317
+ edit.replace(raw.value.range, "");
2574
2318
  });
2575
2319
  });
2576
2320
  }
2577
- disable() {
2578
- this.#scope?.();
2579
- this.#scope = void 0;
2580
- }
2581
2321
  #handleCopy(e) {
2582
- const container = this.store.slots.container();
2583
- if (!container) return false;
2322
+ if (!this.dom.container()) return false;
2323
+ const raw = this.dom.readRawSelection();
2324
+ if (!raw.ok || raw.value.range.start === raw.value.range.end) return false;
2584
2325
  const sel = window.getSelection();
2585
- if (!sel || sel.isCollapsed || !sel.rangeCount) return false;
2586
- const range = sel.getRangeAt(0);
2587
- if (!container.contains(range.startContainer) || !container.contains(range.endContainer)) return false;
2588
- const result = selectionToTokens(this.store);
2589
- if (!result) return false;
2326
+ const range = sel?.rangeCount ? sel.getRangeAt(0) : void 0;
2327
+ if (!range) return false;
2590
2328
  const plainText = range.toString();
2591
- const fragment = range.cloneContents();
2592
- const div = document.createElement("div");
2593
- div.appendChild(fragment);
2594
- const html = div.innerHTML;
2595
- const markup = toString(trimBoundaryTokens(result));
2329
+ const html = htmlFromRange(range);
2330
+ const markup = serializeRawRange(this.parsing.tokens(), raw.value.range);
2596
2331
  e.preventDefault();
2597
2332
  e.clipboardData?.setData("text/plain", plainText);
2598
2333
  e.clipboardData?.setData("text/html", html);
@@ -2601,120 +2336,698 @@ var ClipboardFeature = class {
2601
2336
  }
2602
2337
  };
2603
2338
  //#endregion
2604
- //#region ../../core/src/features/dom/isTextTokenSpan.ts
2605
- function isTextTokenSpan(el) {
2606
- return el.tagName === "SPAN" && (el.attributes.length === 0 || el.attributes.length === 1 && el.hasAttribute("contenteditable"));
2339
+ //#region ../../core/src/features/dom/DomController.ts
2340
+ function nextTextNode(walker) {
2341
+ const node = walker.nextNode();
2342
+ return node instanceof Text ? node : null;
2607
2343
  }
2608
- //#endregion
2609
- //#region ../../core/src/features/dom/DomFeature.ts
2610
- var DomFeature = class {
2611
- #scope;
2612
- constructor(_store) {
2613
- this._store = _store;
2614
- }
2615
- enable() {
2616
- if (this.#scope) return;
2617
- this.#scope = effectScope(() => {
2618
- alienEffect(() => {
2619
- this._store.props.readOnly();
2620
- this.reconcile();
2344
+ function splitsSurrogatePair(text, offset) {
2345
+ if (offset <= 0 || offset >= text.length) return false;
2346
+ const prev = text.charCodeAt(offset - 1);
2347
+ const next = text.charCodeAt(offset);
2348
+ return prev >= 55296 && prev <= 56319 && next >= 56320 && next <= 57343;
2349
+ }
2350
+ function textOffsetWithin(surface, node, offset) {
2351
+ if (node.nodeType === Node.TEXT_NODE) {
2352
+ if (splitsSurrogatePair(node.textContent ?? "", offset)) return void 0;
2353
+ return node instanceof Text ? textOffsetFromTreeWalker(surface, node, offset) : void 0;
2354
+ }
2355
+ if (node === surface) return elementBoundaryOffset(surface, offset);
2356
+ }
2357
+ function textOffsetFromTreeWalker(surface, target, targetOffset) {
2358
+ let total = 0;
2359
+ const walker = document.createTreeWalker(surface, NodeFilter.SHOW_TEXT);
2360
+ let current = nextTextNode(walker);
2361
+ while (current) {
2362
+ if (current === target) return total + targetOffset;
2363
+ total += current.length;
2364
+ current = nextTextNode(walker);
2365
+ }
2366
+ }
2367
+ function textLength(surface) {
2368
+ let total = 0;
2369
+ const walker = document.createTreeWalker(surface, NodeFilter.SHOW_TEXT);
2370
+ let current = nextTextNode(walker);
2371
+ while (current) {
2372
+ total += current.length;
2373
+ current = nextTextNode(walker);
2374
+ }
2375
+ return total;
2376
+ }
2377
+ function elementBoundaryOffset(surface, offset) {
2378
+ if (offset <= 0) return 0;
2379
+ if (offset >= surface.childNodes.length) return textLength(surface);
2380
+ let total = 0;
2381
+ for (let i = 0; i < offset; i++) {
2382
+ const child = surface.childNodes.item(i);
2383
+ if (child.nodeType === Node.TEXT_NODE && child instanceof Text) {
2384
+ total += child.length;
2385
+ continue;
2386
+ }
2387
+ if (child instanceof HTMLElement) total += textLength(child);
2388
+ }
2389
+ return total;
2390
+ }
2391
+ function hasEditableAncestorBefore(node, boundary) {
2392
+ let current = node instanceof HTMLElement ? node : node.parentElement;
2393
+ while (current && current !== boundary) {
2394
+ if (current.isContentEditable || current.contentEditable === "true" || current.contentEditable === "plaintext-only") return true;
2395
+ current = current.parentElement;
2396
+ }
2397
+ return false;
2398
+ }
2399
+ var DomController = class {
2400
+ #domIndex = signal(void 0, { readonly: true });
2401
+ #pendingControls = /* @__PURE__ */ new Map();
2402
+ #pendingChildSequences = /* @__PURE__ */ new Map();
2403
+ #nextControlId = 0;
2404
+ #nextChildSequenceId = 0;
2405
+ #elementRoles = /* @__PURE__ */ new WeakMap();
2406
+ #pathElements = /* @__PURE__ */ new Map();
2407
+ #generation = 0;
2408
+ #rendering = false;
2409
+ #isComposing = false;
2410
+ #queuedRender = false;
2411
+ constructor(lifecycle, props, parsing, value) {
2412
+ this.lifecycle = lifecycle;
2413
+ this.props = props;
2414
+ this.parsing = parsing;
2415
+ this.value = value;
2416
+ this.index = computed(() => this.#domIndex());
2417
+ this.container = signal(null);
2418
+ this.diagnostics = event();
2419
+ this.indexed = event();
2420
+ this.readOnly = computed(() => this.props.readOnly());
2421
+ lifecycle.onMounted(() => {
2422
+ const container = this.container();
2423
+ if (container) listen(container, "click", () => {
2424
+ const tokens = this.parsing.tokens();
2425
+ if (tokens.length === 1 && tokens[0].type === "text" && tokens[0].content === "") {
2426
+ const c = this.container();
2427
+ (c ? firstHtmlChild(c) : null)?.focus();
2428
+ }
2621
2429
  });
2622
- alienEffect(() => {
2623
- if (this._store.caret.selecting() === void 0) this.reconcile();
2430
+ watch(lifecycle.rendered, () => {
2431
+ this.#handleRendered();
2624
2432
  });
2433
+ watch(computed(() => props.readOnly()), () => this.reconcile());
2625
2434
  });
2626
2435
  }
2627
- disable() {
2628
- this.#scope?.();
2629
- this.#scope = void 0;
2436
+ compositionStarted() {
2437
+ this.#isComposing = true;
2630
2438
  }
2631
- reconcile() {
2632
- const container = this._store.slots.container();
2633
- if (!container) return;
2634
- const readOnly = this._store.props.readOnly();
2635
- const value = readOnly ? "false" : "true";
2636
- const children = container.children;
2637
- const isBlock = this._store.slots.isBlock();
2638
- if (isBlock) {
2639
- const tokens = this._store.parsing.tokens();
2640
- for (let i = 0; i < tokens.length && i < children.length; i++) {
2641
- const el = childAt(container, i);
2642
- if (!el) continue;
2643
- if (tokens[i].type === "mark") {
2644
- if (!readOnly) el.tabIndex = 0;
2645
- } else el.contentEditable = value;
2439
+ compositionEnded() {
2440
+ if (!this.#isComposing) return;
2441
+ this.#isComposing = false;
2442
+ }
2443
+ controlFor(ownerPath) {
2444
+ const key = `control:${ownerPath ? pathKey(ownerPath) : "global"}:${++this.#nextControlId}`;
2445
+ const callback = (element) => {
2446
+ if (element) this.#pendingControls.set(key, {
2447
+ ownerPath: ownerPath ? [...ownerPath] : void 0,
2448
+ element
2449
+ });
2450
+ else this.#pendingControls.delete(key);
2451
+ };
2452
+ return callback;
2453
+ }
2454
+ childrenFor(ownerPath) {
2455
+ const key = `children:${pathKey(ownerPath)}:${++this.#nextChildSequenceId}`;
2456
+ const callback = (element) => {
2457
+ if (element) this.#pendingChildSequences.set(key, {
2458
+ ownerPath: [...ownerPath],
2459
+ element
2460
+ });
2461
+ else this.#pendingChildSequences.delete(key);
2462
+ };
2463
+ return callback;
2464
+ }
2465
+ reconcile(opts) {
2466
+ this.#reconcileStructuralTextSurfaces(opts?.isUserSelecting);
2467
+ }
2468
+ locateNode(node) {
2469
+ if (!this.index()) return {
2470
+ ok: false,
2471
+ reason: "notIndexed"
2472
+ };
2473
+ const container = this.container();
2474
+ if (!container || !container.contains(node)) return {
2475
+ ok: false,
2476
+ reason: "outsideEditor"
2477
+ };
2478
+ let current = node;
2479
+ while (current) {
2480
+ if (current instanceof HTMLElement) {
2481
+ const role = this.#elementRoles.get(current);
2482
+ if (role?.role === "control") return {
2483
+ ok: false,
2484
+ reason: "control"
2485
+ };
2486
+ if (role) {
2487
+ const elements = this.#pathElements.get(pathKey(role.path));
2488
+ if (!elements?.tokenElement) return {
2489
+ ok: false,
2490
+ reason: "notIndexed"
2491
+ };
2492
+ return {
2493
+ ok: true,
2494
+ value: {
2495
+ address: role.address,
2496
+ tokenElement: elements.tokenElement,
2497
+ textElement: elements.textElement,
2498
+ rowElement: elements.rowElement
2499
+ }
2500
+ };
2501
+ }
2646
2502
  }
2647
- } else for (let i = 0; i < children.length; i += 2) {
2648
- const el = childAt(container, i);
2649
- if (el) el.contentEditable = value;
2503
+ if (current === container) break;
2504
+ current = current.parentNode;
2650
2505
  }
2651
- const tokens = this._store.parsing.tokens();
2652
- if (isBlock) this.#reconcileDragTextContent(tokens, container, readOnly);
2653
- else this.#reconcileTextContent(tokens, container);
2654
- }
2655
- #reconcileTextContent(tokens, parent) {
2656
- for (let i = 0; i < tokens.length; i++) {
2657
- const token = tokens[i];
2658
- const el = childAt(parent, i);
2659
- if (!el) continue;
2660
- if (token.type === "text") {
2661
- if (el.textContent !== token.content) el.textContent = token.content;
2662
- } else if (token.children.length > 0) this.#reconcileMarkChildren(token.children, el);
2506
+ return {
2507
+ ok: false,
2508
+ reason: "outsideEditor"
2509
+ };
2510
+ }
2511
+ placeAt(rawPosition, affinity = "after") {
2512
+ if (!this.index()) return {
2513
+ ok: false,
2514
+ reason: "notIndexed"
2515
+ };
2516
+ const maxPos = this.value.current().length;
2517
+ const clamped = Math.min(rawPosition, maxPos);
2518
+ const target = this.#findTextTargetForRawPosition(clamped, affinity);
2519
+ if (!target) {
2520
+ const boundary = this.#focusMarkBoundaryForRawPosition(clamped);
2521
+ if (!boundary.ok) return boundary;
2522
+ return {
2523
+ ok: true,
2524
+ value: { applied: clamped }
2525
+ };
2663
2526
  }
2527
+ target.element.focus();
2528
+ this.#placeCaretInTextSurface(target.element, clamped - target.start);
2529
+ return {
2530
+ ok: true,
2531
+ value: { applied: clamped }
2532
+ };
2664
2533
  }
2665
- #reconcileMarkChildren(tokens, parent, editable) {
2666
- const children = parent.children;
2667
- let childIdx = 0;
2668
- for (const token of tokens) if (token.type === "text") {
2669
- while (childIdx < children.length) {
2670
- const el = childAt(parent, childIdx);
2671
- if (el && isTextTokenSpan(el)) break;
2672
- childIdx++;
2673
- }
2674
- const el = childAt(parent, childIdx);
2675
- if (el) {
2676
- if (el.textContent !== token.content) el.textContent = token.content;
2677
- if (editable !== void 0) el.contentEditable = editable;
2678
- childIdx++;
2679
- }
2680
- } else if (token.children.length > 0) {
2681
- while (childIdx < children.length) {
2682
- const el = childAt(parent, childIdx);
2683
- if (el && !isTextTokenSpan(el)) break;
2684
- childIdx++;
2534
+ placeRange(range) {
2535
+ const maxPos = this.value.current().length;
2536
+ const clamped = {
2537
+ start: Math.min(range.start, maxPos),
2538
+ end: Math.min(range.end, maxPos)
2539
+ };
2540
+ const result = this.#placeSelection({
2541
+ range: clamped,
2542
+ direction: void 0
2543
+ });
2544
+ if (!result.ok) return result;
2545
+ return {
2546
+ ok: true,
2547
+ value: { applied: clamped }
2548
+ };
2549
+ }
2550
+ focusAddress(address, boundary = "start") {
2551
+ if (!this.index()) return {
2552
+ ok: false,
2553
+ reason: "notIndexed"
2554
+ };
2555
+ if (!this.parsing.index().resolveAddress(address).ok) return {
2556
+ ok: false,
2557
+ reason: "stale"
2558
+ };
2559
+ const elements = this.#pathElements.get(pathKey(address.path));
2560
+ const target = elements?.textElement ?? elements?.tokenElement ?? elements?.rowElement;
2561
+ if (!target) return {
2562
+ ok: false,
2563
+ reason: "notIndexed"
2564
+ };
2565
+ target.focus();
2566
+ if ((target === elements?.textElement ? "text" : target === elements?.rowElement ? "row" : "markDescendant") === "markDescendant") this.#placeCollapsedBoundary(target, boundary === "end" ? target.childNodes.length : 0);
2567
+ return {
2568
+ ok: true,
2569
+ value: void 0
2570
+ };
2571
+ }
2572
+ rawPositionFromBoundary(node, offset, affinity = "after") {
2573
+ if (!this.index()) return {
2574
+ ok: false,
2575
+ reason: "notIndexed"
2576
+ };
2577
+ if (this.#isComposing) return {
2578
+ ok: false,
2579
+ reason: "composing"
2580
+ };
2581
+ const container = this.container();
2582
+ if (container && node === container) return this.#rawPositionFromContainerBoundary(offset, affinity);
2583
+ const location = this.locateNode(node);
2584
+ if (!location.ok) return location.reason === "control" ? {
2585
+ ok: false,
2586
+ reason: "control"
2587
+ } : location;
2588
+ const token = this.parsing.index().resolveAddress(location.value.address);
2589
+ if (!token.ok) return {
2590
+ ok: false,
2591
+ reason: "notIndexed"
2592
+ };
2593
+ if (node instanceof HTMLElement) {
2594
+ if (this.#elementRoles.get(node)?.role === "childSequence") {
2595
+ const childCount = node.childNodes.length;
2596
+ if (offset <= 0) return {
2597
+ ok: true,
2598
+ value: token.value.position.start
2599
+ };
2600
+ if (offset >= childCount) return {
2601
+ ok: true,
2602
+ value: token.value.position.end
2603
+ };
2604
+ return this.#rawPositionFromTokenChildBoundary(node, offset, token.value, affinity);
2685
2605
  }
2686
- const el = childAt(parent, childIdx);
2687
- if (el) {
2688
- this.#reconcileMarkChildren(token.children, el, editable);
2689
- childIdx++;
2606
+ }
2607
+ const textElement = location.value.textElement;
2608
+ if (textElement?.contains(node)) {
2609
+ const local = textOffsetWithin(textElement, node, offset);
2610
+ if (local === void 0) return {
2611
+ ok: false,
2612
+ reason: "invalidBoundary"
2613
+ };
2614
+ return {
2615
+ ok: true,
2616
+ value: token.value.position.start + local
2617
+ };
2618
+ }
2619
+ if (node === location.value.tokenElement) {
2620
+ const childCount = location.value.tokenElement.childNodes.length;
2621
+ if (offset <= 0) return {
2622
+ ok: true,
2623
+ value: token.value.position.start
2624
+ };
2625
+ if (offset >= childCount) return {
2626
+ ok: true,
2627
+ value: token.value.position.end
2628
+ };
2629
+ return this.#rawPositionFromTokenChildBoundary(location.value.tokenElement, offset, token.value, affinity);
2630
+ }
2631
+ if (token.value.type === "mark" && location.value.tokenElement.contains(node)) {
2632
+ if (hasEditableAncestorBefore(node, location.value.tokenElement)) return {
2633
+ ok: false,
2634
+ reason: "invalidBoundary"
2635
+ };
2636
+ return {
2637
+ ok: true,
2638
+ value: affinity === "after" ? token.value.position.start : token.value.position.end
2639
+ };
2640
+ }
2641
+ if (location.value.rowElement && node === location.value.rowElement) return {
2642
+ ok: true,
2643
+ value: offset <= 0 ? token.value.position.start : token.value.position.end
2644
+ };
2645
+ return {
2646
+ ok: false,
2647
+ reason: "invalidBoundary"
2648
+ };
2649
+ }
2650
+ readRawSelection() {
2651
+ if (!this.index()) return {
2652
+ ok: false,
2653
+ reason: "notIndexed"
2654
+ };
2655
+ const selection = window.getSelection();
2656
+ if (!selection || selection.rangeCount === 0) return {
2657
+ ok: false,
2658
+ reason: "invalidBoundary"
2659
+ };
2660
+ const range = selection.getRangeAt(0);
2661
+ const start = this.rawPositionFromBoundary(range.startContainer, range.startOffset, "after");
2662
+ const end = this.rawPositionFromBoundary(range.endContainer, range.endOffset, "before");
2663
+ if (!start.ok) {
2664
+ const reason = start.reason === "composing" ? "invalidBoundary" : start.reason;
2665
+ return {
2666
+ ok: false,
2667
+ reason: reason === "control" || reason === "outsideEditor" ? "mixedBoundary" : reason
2668
+ };
2669
+ }
2670
+ if (!end.ok) {
2671
+ const reason = end.reason === "composing" ? "invalidBoundary" : end.reason;
2672
+ return {
2673
+ ok: false,
2674
+ reason: reason === "control" || reason === "outsideEditor" ? "mixedBoundary" : reason
2675
+ };
2676
+ }
2677
+ const rangeValue = start.value <= end.value ? {
2678
+ start: start.value,
2679
+ end: end.value
2680
+ } : {
2681
+ start: end.value,
2682
+ end: start.value
2683
+ };
2684
+ const direction = rangeValue.start === rangeValue.end ? void 0 : selection.anchorNode === range.endContainer && selection.anchorOffset === range.endOffset ? "backward" : "forward";
2685
+ return {
2686
+ ok: true,
2687
+ value: direction ? {
2688
+ range: rangeValue,
2689
+ direction
2690
+ } : { range: rangeValue }
2691
+ };
2692
+ }
2693
+ #handleRendered() {
2694
+ if (this.#rendering) {
2695
+ this.#queuedRender = true;
2696
+ this.diagnostics({
2697
+ kind: "renderReentry",
2698
+ reason: "rendered event queued during DOM indexing"
2699
+ });
2700
+ return;
2701
+ }
2702
+ this.#rendering = true;
2703
+ try {
2704
+ this.#commitRendered();
2705
+ } finally {
2706
+ this.#rendering = false;
2707
+ const queued = this.#queuedRender;
2708
+ this.#queuedRender = false;
2709
+ if (queued) this.#handleRendered();
2710
+ }
2711
+ }
2712
+ #commitRendered() {
2713
+ const container = this.container();
2714
+ if (!container) {
2715
+ this.diagnostics({
2716
+ kind: "missingContainer",
2717
+ reason: "container is not registered"
2718
+ });
2719
+ return;
2720
+ }
2721
+ const tokenIndex = this.parsing.index();
2722
+ const pathElements = /* @__PURE__ */ new Map();
2723
+ const elementRoles = /* @__PURE__ */ new WeakMap();
2724
+ const controlElements = /* @__PURE__ */ new Set();
2725
+ for (const { element } of this.#pendingControls.values()) {
2726
+ controlElements.add(element);
2727
+ elementRoles.set(element, { role: "control" });
2728
+ }
2729
+ const tokens = this.parsing.tokens();
2730
+ if (this.props.layout() === "block") this.#indexBlockTokens(container, tokens, tokenIndex, controlElements, pathElements, elementRoles);
2731
+ else this.#indexTokenSequence(container, tokens, [], void 0, tokenIndex, controlElements, pathElements, elementRoles);
2732
+ this.#pathElements = pathElements;
2733
+ this.#elementRoles = elementRoles;
2734
+ this.#reconcileStructuralTextSurfaces();
2735
+ batch(() => this.#domIndex({ generation: ++this.#generation }), { mutable: true });
2736
+ this.indexed();
2737
+ }
2738
+ #elementChildren(element) {
2739
+ return Array.from(element.children).filter((child) => child instanceof HTMLElement);
2740
+ }
2741
+ #isControlRoot(element, controlElements) {
2742
+ if (controlElements.has(element)) return true;
2743
+ for (const control of controlElements) if (element.contains(control)) return true;
2744
+ return false;
2745
+ }
2746
+ #childSequenceHostsFor(ownerPath) {
2747
+ const hosts = [];
2748
+ for (const registration of this.#pendingChildSequences.values()) if (pathEquals(registration.ownerPath, ownerPath)) hosts.push(registration.element);
2749
+ return hosts;
2750
+ }
2751
+ #indexNestedTokenSequence(token, path, address, ownerElement, rowElement, tokenIndex, controlElements, pathElements, elementRoles) {
2752
+ if (token.type !== "mark" || token.children.length === 0) return;
2753
+ const hosts = this.#childSequenceHostsFor(path);
2754
+ if (hosts.length === 0) {
2755
+ this.#indexTokenSequence(ownerElement, token.children, path, rowElement, tokenIndex, controlElements, pathElements, elementRoles);
2756
+ return;
2757
+ }
2758
+ const ownerKey = pathKey(path);
2759
+ if (hosts.length !== 1) {
2760
+ this.diagnostics({
2761
+ kind: "ambiguousStructure",
2762
+ path,
2763
+ reason: `expected exactly 1 child sequence host for owner path ${ownerKey} but found ${hosts.length}`
2764
+ });
2765
+ return;
2766
+ }
2767
+ const host = hosts[0];
2768
+ if (!ownerElement.contains(host)) {
2769
+ this.diagnostics({
2770
+ kind: "ambiguousStructure",
2771
+ path,
2772
+ reason: `child sequence host for owner path ${ownerKey} is not contained by owner token element`
2773
+ });
2774
+ return;
2775
+ }
2776
+ elementRoles.set(host, {
2777
+ role: "childSequence",
2778
+ path,
2779
+ address
2780
+ });
2781
+ this.#indexTokenSequence(host, token.children, path, rowElement, tokenIndex, controlElements, pathElements, elementRoles);
2782
+ }
2783
+ #indexBlockTokens(container, tokens, tokenIndex, controlElements, pathElements, elementRoles) {
2784
+ const rows = this.#elementChildren(container);
2785
+ if (rows.length !== tokens.length) this.diagnostics({
2786
+ kind: "ambiguousStructure",
2787
+ reason: `expected ${tokens.length} block rows but found ${rows.length}`
2788
+ });
2789
+ tokens.forEach((token, i) => {
2790
+ const row = rows.at(i);
2791
+ if (!row) return;
2792
+ const candidates = this.#elementChildren(row).filter((child) => !this.#isControlRoot(child, controlElements));
2793
+ if (candidates.length !== 1) {
2794
+ this.diagnostics({
2795
+ kind: "ambiguousStructure",
2796
+ path: [i],
2797
+ reason: `expected 1 block token element but found ${candidates.length}`
2798
+ });
2799
+ return;
2690
2800
  }
2801
+ this.#indexTokenElement(token, [i], candidates[0], row, tokenIndex, controlElements, pathElements, elementRoles);
2802
+ });
2803
+ }
2804
+ #indexTokenSequence(parent, tokens, basePath, rowElement, tokenIndex, controlElements, pathElements, elementRoles) {
2805
+ const elements = this.#elementChildren(parent).filter((child) => !this.#isControlRoot(child, controlElements));
2806
+ if (elements.length !== tokens.length) {
2807
+ this.diagnostics({
2808
+ kind: "ambiguousStructure",
2809
+ path: basePath.length ? basePath : void 0,
2810
+ reason: `expected ${tokens.length} child token elements but found ${elements.length}`
2811
+ });
2812
+ return;
2691
2813
  }
2814
+ tokens.forEach((token, i) => {
2815
+ const element = elements.at(i);
2816
+ if (!element) return;
2817
+ this.#indexTokenElement(token, [...basePath, i], element, rowElement, tokenIndex, controlElements, pathElements, elementRoles);
2818
+ });
2692
2819
  }
2693
- #reconcileDragTextContent(tokens, container, readOnly) {
2694
- const editable = readOnly ? "false" : "true";
2695
- for (let ri = 0; ri < tokens.length; ri++) {
2696
- const token = tokens[ri];
2697
- const blockEl = childAt(container, ri);
2698
- if (!blockEl) continue;
2699
- if (token.type === "mark") {
2700
- if (token.children.length > 0) {
2701
- const markEl = blockEl.hasAttribute("data-testid") ? childAt(blockEl, readOnly ? 0 : 1) : blockEl;
2702
- if (markEl) this.#reconcileMarkChildren(token.children, markEl, editable);
2820
+ #indexTokenElement(token, path, element, rowElement, tokenIndex, controlElements, pathElements, elementRoles) {
2821
+ const address = tokenIndex.addressFor(path);
2822
+ if (!address) {
2823
+ this.diagnostics({
2824
+ kind: "stalePath",
2825
+ path,
2826
+ reason: "structural path no longer resolves"
2827
+ });
2828
+ return;
2829
+ }
2830
+ const record = {
2831
+ path: [...path],
2832
+ address,
2833
+ tokenElement: element,
2834
+ textElement: token.type === "text" ? element : void 0,
2835
+ rowElement
2836
+ };
2837
+ pathElements.set(tokenIndex.key(path), record);
2838
+ elementRoles.set(element, {
2839
+ role: token.type === "text" ? "text" : "token",
2840
+ path,
2841
+ address
2842
+ });
2843
+ if (rowElement && path.length === 1) elementRoles.set(rowElement, {
2844
+ role: "row",
2845
+ path,
2846
+ address
2847
+ });
2848
+ this.#indexNestedTokenSequence(token, path, address, element, rowElement, tokenIndex, controlElements, pathElements, elementRoles);
2849
+ }
2850
+ #reconcileStructuralTextSurfaces(isUserSelecting) {
2851
+ const tokenIndex = this.parsing.index();
2852
+ const editable = this.props.readOnly() || isUserSelecting ? "false" : "true";
2853
+ for (const record of this.#pathElements.values()) {
2854
+ const resolved = tokenIndex.resolveAddress(record.address);
2855
+ if (!resolved.ok) {
2856
+ this.diagnostics({
2857
+ kind: "stalePath",
2858
+ path: record.path,
2859
+ reason: "structural path became stale during reconciliation"
2860
+ });
2861
+ continue;
2862
+ }
2863
+ if (record.textElement) {
2864
+ if (resolved.value.type !== "text") {
2865
+ this.diagnostics({
2866
+ kind: "missingRole",
2867
+ path: record.path,
2868
+ reason: "text role registered for non-text token"
2869
+ });
2870
+ continue;
2703
2871
  }
2872
+ if (record.textElement.textContent !== resolved.value.content) record.textElement.textContent = resolved.value.content;
2873
+ record.textElement.contentEditable = editable;
2704
2874
  continue;
2705
2875
  }
2706
- const el = childAt(blockEl, readOnly ? 0 : 1);
2707
- if (!el) continue;
2708
- if (el.textContent !== token.content) el.textContent = token.content;
2876
+ if (resolved.value.type === "mark") if (this.props.readOnly()) record.tokenElement.removeAttribute("tabindex");
2877
+ else record.tokenElement.tabIndex = 0;
2709
2878
  }
2710
2879
  }
2880
+ #rawPositionFromContainerBoundary(offset, affinity) {
2881
+ const tokens = this.parsing.tokens();
2882
+ if (tokens.length === 0) return {
2883
+ ok: true,
2884
+ value: 0
2885
+ };
2886
+ if (offset <= 0) return {
2887
+ ok: true,
2888
+ value: tokens[0].position.start
2889
+ };
2890
+ if (offset >= tokens.length) return {
2891
+ ok: true,
2892
+ value: tokens[tokens.length - 1].position.end
2893
+ };
2894
+ const before = tokens[offset - 1];
2895
+ const after = tokens[offset];
2896
+ return {
2897
+ ok: true,
2898
+ value: affinity === "before" ? before.position.end : after.position.start
2899
+ };
2900
+ }
2901
+ #rawPositionFromTokenChildBoundary(tokenElement, offset, token, affinity) {
2902
+ if (token.type === "text") {
2903
+ const textElement = this.#pathElements.get(pathKey(this.parsing.index().pathFor(token) ?? []))?.textElement;
2904
+ if (!textElement || textLength(textElement) === 0) return {
2905
+ ok: true,
2906
+ value: token.position.start
2907
+ };
2908
+ }
2909
+ const before = this.#locateRegisteredDescendant(tokenElement.childNodes.item(offset - 1));
2910
+ const after = this.#locateRegisteredDescendant(tokenElement.childNodes.item(offset));
2911
+ if (before?.ok && after?.ok) {
2912
+ const beforeToken = this.parsing.index().resolveAddress(before.value.address);
2913
+ const afterToken = this.parsing.index().resolveAddress(after.value.address);
2914
+ if (beforeToken.ok && afterToken.ok) return {
2915
+ ok: true,
2916
+ value: affinity === "before" ? beforeToken.value.position.end : afterToken.value.position.start
2917
+ };
2918
+ }
2919
+ return {
2920
+ ok: true,
2921
+ value: affinity === "before" ? token.position.start : token.position.end
2922
+ };
2923
+ }
2924
+ #locateRegisteredDescendant(node) {
2925
+ if (!node) return void 0;
2926
+ return this.locateNode(node);
2927
+ }
2928
+ #findTextTargetForRawPosition(rawPosition, affinity) {
2929
+ const candidates = [];
2930
+ const tokenIndex = this.parsing.index();
2931
+ for (const record of this.#pathElements.values()) {
2932
+ if (!record.textElement) continue;
2933
+ const resolved = tokenIndex.resolveAddress(record.address);
2934
+ if (!resolved.ok || resolved.value.type !== "text") continue;
2935
+ candidates.push({
2936
+ element: record.textElement,
2937
+ start: resolved.value.position.start,
2938
+ end: resolved.value.position.end
2939
+ });
2940
+ }
2941
+ candidates.sort((a, b) => a.start - b.start);
2942
+ const containing = candidates.find((candidate) => rawPosition >= candidate.start && rawPosition <= candidate.end);
2943
+ if (containing) return containing;
2944
+ if (affinity === "before") return [...candidates].toReversed().find((candidate) => candidate.end <= rawPosition);
2945
+ return candidates.find((candidate) => candidate.start >= rawPosition);
2946
+ }
2947
+ #focusMarkBoundaryForRawPosition(rawPosition) {
2948
+ const tokenIndex = this.parsing.index();
2949
+ for (const record of this.#pathElements.values()) {
2950
+ const resolved = tokenIndex.resolveAddress(record.address);
2951
+ if (!resolved.ok || resolved.value.type !== "mark") continue;
2952
+ if (rawPosition !== resolved.value.position.start && rawPosition !== resolved.value.position.end) continue;
2953
+ const boundary = rawPosition === resolved.value.position.end ? "end" : "start";
2954
+ record.tokenElement.focus();
2955
+ this.#placeCollapsedBoundary(record.tokenElement, boundary === "end" ? record.tokenElement.childNodes.length : 0);
2956
+ return {
2957
+ ok: true,
2958
+ value: void 0
2959
+ };
2960
+ }
2961
+ return {
2962
+ ok: false,
2963
+ reason: "invalidBoundary"
2964
+ };
2965
+ }
2966
+ #placeCaretInTextSurface(surface, offset) {
2967
+ const selection = window.getSelection();
2968
+ if (!selection) return;
2969
+ const boundary = this.#boundaryInTextSurface(surface, offset);
2970
+ if (!boundary) return;
2971
+ const range = document.createRange();
2972
+ range.setStart(boundary.node, boundary.offset);
2973
+ range.collapse(true);
2974
+ selection.removeAllRanges();
2975
+ selection.addRange(range);
2976
+ }
2977
+ #placeCollapsedBoundary(element, offset) {
2978
+ const selection = window.getSelection();
2979
+ if (!selection) return;
2980
+ const range = document.createRange();
2981
+ range.setStart(element, Math.min(Math.max(offset, 0), element.childNodes.length));
2982
+ range.collapse(true);
2983
+ selection.removeAllRanges();
2984
+ selection.addRange(range);
2985
+ }
2986
+ #placeSelection(selection) {
2987
+ const start = this.#findTextTargetForRawPosition(selection.range.start, "after");
2988
+ const end = this.#findTextTargetForRawPosition(selection.range.end, "before");
2989
+ const browserSelection = window.getSelection();
2990
+ if (!start || !end || !browserSelection) return {
2991
+ ok: false,
2992
+ reason: "invalidBoundary"
2993
+ };
2994
+ const startBoundary = this.#boundaryInTextSurface(start.element, selection.range.start - start.start);
2995
+ const endBoundary = this.#boundaryInTextSurface(end.element, selection.range.end - end.start);
2996
+ if (!startBoundary || !endBoundary) return {
2997
+ ok: false,
2998
+ reason: "invalidBoundary"
2999
+ };
3000
+ const range = document.createRange();
3001
+ range.setStart(startBoundary.node, startBoundary.offset);
3002
+ range.setEnd(endBoundary.node, endBoundary.offset);
3003
+ browserSelection.removeAllRanges();
3004
+ browserSelection.addRange(range);
3005
+ return {
3006
+ ok: true,
3007
+ value: void 0
3008
+ };
3009
+ }
3010
+ #boundaryInTextSurface(surface, offset) {
3011
+ const walker = document.createTreeWalker(surface, NodeFilter.SHOW_TEXT);
3012
+ let remaining = Math.max(0, offset);
3013
+ let node = nextTextNode(walker);
3014
+ while (node) {
3015
+ if (remaining <= node.length) return {
3016
+ node,
3017
+ offset: remaining
3018
+ };
3019
+ remaining -= node.length;
3020
+ node = nextTextNode(walker);
3021
+ }
3022
+ const text = surface.firstChild instanceof Text ? surface.firstChild : document.createTextNode("");
3023
+ if (!text.parentNode) surface.append(text);
3024
+ return {
3025
+ node: text,
3026
+ offset: text.length
3027
+ };
3028
+ }
2711
3029
  };
2712
3030
  //#endregion
2713
- //#region ../../core/src/features/editing/utils/createNewSpan.ts
2714
- function createNewSpan(span, annotation, index, source) {
2715
- return span.slice(0, index) + annotation + span.slice(index + source.length);
2716
- }
2717
- //#endregion
2718
3031
  //#region ../../core/src/features/editing/createRowContent.ts
2719
3032
  function createRowContent(options) {
2720
3033
  const firstOption = options[0];
@@ -2726,47 +3039,6 @@ function createRowContent(options) {
2726
3039
  });
2727
3040
  }
2728
3041
  //#endregion
2729
- //#region ../../core/src/features/editing/utils/deleteMark.ts
2730
- function deleteMark(place, store) {
2731
- const placeIndex = {
2732
- prev: 2,
2733
- self: 1,
2734
- next: 0
2735
- }[place];
2736
- const { focus } = store.nodes;
2737
- const targetIndex = Math.max(0, focus.index - placeIndex);
2738
- const tokens = store.parsing.tokens();
2739
- const spliced = tokens.splice(focus.index - placeIndex, 3);
2740
- const span1 = spliced.at(0);
2741
- const span2 = spliced.at(2);
2742
- const content1 = span1?.content ?? "";
2743
- const content2 = span2?.content ?? "";
2744
- store.parsing.tokens(tokens.toSpliced(focus.index - placeIndex, 0, {
2745
- type: "text",
2746
- content: content1 + content2,
2747
- position: {
2748
- start: span1?.position.start ?? 0,
2749
- end: span2?.position.end ?? (content1 + content2).length
2750
- }
2751
- }));
2752
- let caretAnchor = focus;
2753
- for (let i = 0; i < placeIndex; i++) caretAnchor = caretAnchor.prev;
2754
- const caret = caretAnchor.length;
2755
- store.caret.recovery({
2756
- anchor: caretAnchor.prev,
2757
- caret
2758
- });
2759
- store.value.change();
2760
- queueMicrotask(() => {
2761
- const container = store.slots.container();
2762
- const target = container ? childAt(container, targetIndex) : null;
2763
- if (!target) return;
2764
- store.nodes.focus.target = target;
2765
- target.focus();
2766
- store.nodes.focus.caret = caret;
2767
- });
2768
- }
2769
- //#endregion
2770
3042
  //#region ../../core/src/features/drag/operations.ts
2771
3043
  function gapText(value, a, b) {
2772
3044
  return value.substring(a.position.end, b.position.start);
@@ -2864,66 +3136,96 @@ const EMPTY_TEXT_TOKEN = {
2864
3136
  }
2865
3137
  };
2866
3138
  //#endregion
2867
- //#region ../../core/src/features/drag/DragFeature.ts
2868
- var DragFeature = class {
2869
- constructor(store) {
2870
- this.store = store;
2871
- this.action = event();
2872
- }
3139
+ //#region ../../core/src/features/drag/DragController.ts
3140
+ var DragController = class {
2873
3141
  #unsub;
2874
- enable() {
2875
- if (this.#unsub) return;
2876
- this.#unsub = watch(this.action, (action) => {
2877
- switch (action.type) {
2878
- case "reorder":
2879
- this.#reorder(action.source, action.target);
2880
- break;
2881
- case "add":
2882
- this.#add(action.afterIndex);
2883
- break;
2884
- case "delete":
2885
- this.#delete(action.index);
2886
- break;
2887
- case "duplicate":
2888
- this.#duplicate(action.index);
2889
- break;
3142
+ constructor(props, value, parsing, caret) {
3143
+ this.props = props;
3144
+ this.value = value;
3145
+ this.parsing = parsing;
3146
+ this.caret = caret;
3147
+ this.action = event();
3148
+ const isDragEnabled = computed(() => this.props.layout() === "block" && !!this.props.draggable());
3149
+ const toggle = (enabled) => {
3150
+ if (enabled && !this.#unsub) this.#unsub = watch(this.action, (action) => {
3151
+ switch (action.type) {
3152
+ case "reorder":
3153
+ this.#reorder(action);
3154
+ break;
3155
+ case "add":
3156
+ this.#add(action);
3157
+ break;
3158
+ case "delete":
3159
+ this.#delete(action);
3160
+ break;
3161
+ case "duplicate":
3162
+ this.#duplicate(action);
3163
+ break;
3164
+ }
3165
+ });
3166
+ if (!enabled && this.#unsub) {
3167
+ this.#unsub();
3168
+ this.#unsub = void 0;
2890
3169
  }
2891
- });
2892
- }
2893
- disable() {
2894
- this.#unsub?.();
2895
- this.#unsub = void 0;
2896
- }
2897
- #reorder(sourceIndex, targetIndex) {
2898
- const value = this.store.props.value();
2899
- if (value == null || !this.store.props.onChange()) return;
2900
- const newValue = reorderDragRows(value, this.store.parsing.tokens(), sourceIndex, targetIndex);
2901
- if (newValue !== value) this.store.value.next(newValue);
3170
+ };
3171
+ watch(isDragEnabled, toggle);
3172
+ toggle(isDragEnabled());
3173
+ }
3174
+ #reorder(action) {
3175
+ const value = this.value.current();
3176
+ const rows = this.parsing.tokens();
3177
+ const newValue = reorderDragRows(value, rows, action.source, action.target);
3178
+ if (newValue !== value) {
3179
+ const range = this.#rangeAfterDrag(action, rows, newValue);
3180
+ if (range) this.caret.selection(range);
3181
+ this.value.current(newValue);
3182
+ }
2902
3183
  }
2903
- #add(afterIndex) {
2904
- const value = this.store.props.value();
2905
- if (value == null || !this.store.props.onChange()) return;
2906
- const rawRows = this.store.parsing.tokens();
3184
+ #add(action) {
3185
+ const value = this.value.current();
3186
+ const rawRows = this.parsing.tokens();
2907
3187
  const rows = rawRows.length > 0 ? rawRows : [EMPTY_TEXT_TOKEN];
2908
- const newRowContent = createRowContent(this.store.props.options());
2909
- this.store.value.next(addDragRow(value, rows, afterIndex, newRowContent));
2910
- queueMicrotask(() => {
2911
- const container = this.store.slots.container();
2912
- if (!container) return;
2913
- childAt(container, afterIndex + 1)?.focus();
2914
- });
2915
- }
2916
- #delete(index) {
2917
- const value = this.store.props.value();
2918
- if (value == null || !this.store.props.onChange()) return;
2919
- const rows = this.store.parsing.tokens();
2920
- this.store.value.next(deleteDragRow(value, rows, index));
2921
- }
2922
- #duplicate(index) {
2923
- const value = this.store.props.value();
2924
- if (value == null || !this.store.props.onChange()) return;
2925
- const rows = this.store.parsing.tokens();
2926
- this.store.value.next(duplicateDragRow(value, rows, index));
3188
+ const newRowContent = createRowContent(this.props.options());
3189
+ const newValue = addDragRow(value, rows, action.afterIndex, newRowContent);
3190
+ const range = this.#rangeAfterDrag(action, rows, newValue);
3191
+ if (range) this.caret.selection(range);
3192
+ this.value.current(newValue);
3193
+ }
3194
+ #delete(action) {
3195
+ const value = this.value.current();
3196
+ const rows = this.parsing.tokens();
3197
+ const newValue = deleteDragRow(value, rows, action.index);
3198
+ const range = this.#rangeAfterDrag(action, rows, newValue);
3199
+ if (range) this.caret.selection(range);
3200
+ this.value.current(newValue);
3201
+ }
3202
+ #duplicate(action) {
3203
+ const value = this.value.current();
3204
+ const rows = this.parsing.tokens();
3205
+ const newValue = duplicateDragRow(value, rows, action.index);
3206
+ const range = this.#rangeAfterDrag(action, rows, newValue);
3207
+ if (range) this.caret.selection(range);
3208
+ this.value.current(newValue);
3209
+ }
3210
+ #rangeAfterDrag(action, previousRows, nextValue) {
3211
+ let rawPosition;
3212
+ if (action.type === "add") {
3213
+ const after = previousRows.at(action.afterIndex);
3214
+ rawPosition = after ? after.position.end : nextValue.length;
3215
+ } else if (action.type === "duplicate") {
3216
+ const row = previousRows.at(action.index);
3217
+ rawPosition = row ? row.position.end : void 0;
3218
+ } else if (action.type === "delete") {
3219
+ const next = previousRows.at(action.index + 1) ?? (action.index > 0 ? previousRows.at(action.index - 1) : void 0);
3220
+ rawPosition = next ? Math.min(next.position.start, nextValue.length) : 0;
3221
+ } else {
3222
+ const moved = previousRows.at(action.source);
3223
+ rawPosition = moved ? Math.min(moved.position.start, nextValue.length) : void 0;
3224
+ }
3225
+ return rawPosition !== void 0 ? {
3226
+ start: rawPosition,
3227
+ end: rawPosition
3228
+ } : void 0;
2927
3229
  }
2928
3230
  };
2929
3231
  //#endregion
@@ -2944,174 +3246,71 @@ function getAlwaysShowHandle(draggable) {
2944
3246
  return typeof draggable === "object" && !!draggable.alwaysShowHandle;
2945
3247
  }
2946
3248
  //#endregion
2947
- //#region ../../core/src/features/navigation/navigation.ts
2948
- function shiftFocusPrev(store, event) {
2949
- const { focus } = store.nodes;
2950
- if (focus.isMark && !focus.isEditable || focus.isCaretAtBeginning) {
2951
- let prev = focus.prev;
2952
- while (prev.target && prev.isMark && !prev.isEditable) prev = prev.prev;
2953
- if (!prev.target) return false;
2954
- event.preventDefault();
2955
- prev.target.focus();
2956
- Caret.setCaretToEnd(prev.target);
2957
- return true;
3249
+ //#region ../../core/src/features/edit/EditController.ts
3250
+ /**
3251
+ * Single write path for text edits — delegates gating to {@link ValueModel.replace}
3252
+ * and only moves the caret when the edit is accepted. Wrapped in {@link batch}
3253
+ * so subscribers observe a consistent value/selection pair on one tick.
3254
+ */
3255
+ var EditController = class {
3256
+ constructor(value, caret) {
3257
+ this.value = value;
3258
+ this.caret = caret;
2958
3259
  }
2959
- return false;
2960
- }
2961
- function shiftFocusNext(store, event) {
2962
- const { focus } = store.nodes;
2963
- if (focus.isMark && !focus.isEditable || focus.isCaretAtEnd) {
2964
- let next = focus.next;
2965
- while (next.target && next.isMark && !next.isEditable) next = next.next;
2966
- if (!next.target) return false;
2967
- event.preventDefault();
2968
- next.target.focus();
2969
- Caret.trySetIndex(next.target, 0);
2970
- return true;
3260
+ replace(range, replacement) {
3261
+ batch(() => {
3262
+ if (!this.value.replace(range, replacement)) return;
3263
+ this.caret.position(range.start + replacement.length);
3264
+ });
2971
3265
  }
2972
- return false;
2973
- }
3266
+ };
2974
3267
  //#endregion
2975
3268
  //#region ../../core/src/features/keyboard/arrowNav.ts
2976
3269
  function enableArrowNav(store) {
2977
- const container = store.slots.container();
2978
- if (!container) return () => {};
2979
- const scope = effectScope(() => {
2980
- listen(container, "keydown", (e) => {
3270
+ const container = store.dom.container();
3271
+ if (!container) return;
3272
+ listen(container, "keydown", (e) => {
3273
+ if (store.slots.isBlock()) return;
3274
+ if (e.key === KEYBOARD.LEFT) shiftFocus(store, e, "prev");
3275
+ else if (e.key === KEYBOARD.RIGHT) shiftFocus(store, e, "next");
3276
+ if ((e.ctrlKey || e.metaKey) && e.code === "KeyA") {
2981
3277
  if (store.slots.isBlock()) return;
2982
- if (!store.nodes.focus.target) return;
2983
- if (e.key === KEYBOARD.LEFT) shiftFocusPrev(store, e);
2984
- else if (e.key === KEYBOARD.RIGHT) shiftFocusNext(store, e);
2985
- selectAllText(store, e);
2986
- });
2987
- });
2988
- return () => scope();
2989
- }
2990
- //#endregion
2991
- //#region ../../core/src/features/keyboard/rawPosition.ts
2992
- function getCaretRawPosInBlock(blockDiv, token) {
2993
- const selection = window.getSelection();
2994
- if (!selection?.rangeCount) return token.position.end;
2995
- const { focusNode, focusOffset } = selection;
2996
- if (!focusNode) return token.position.end;
2997
- return getDomRawPos(focusNode, focusOffset, blockDiv, token);
2998
- }
2999
- function setCaretAtRawPos(blockDiv, token, rawAbsolutePos) {
3000
- const sel = window.getSelection();
3001
- if (!sel) return;
3002
- if (token.type === "mark") {
3003
- if (setCaretInMarkAtRawPos(blockDiv, token, rawAbsolutePos)) return;
3004
- Caret.setCaretToEnd(blockDiv);
3005
- return;
3006
- }
3007
- const offsetWithinToken = rawAbsolutePos - token.position.start;
3008
- const textNode = nextText(document.createTreeWalker(blockDiv, 4));
3009
- if (textNode) {
3010
- const charOffset = Math.min(offsetWithinToken, textNode.length);
3011
- const range = document.createRange();
3012
- range.setStart(textNode, charOffset);
3013
- range.collapse(true);
3014
- sel.removeAllRanges();
3015
- sel.addRange(range);
3016
- return;
3017
- }
3018
- Caret.setCaretToEnd(blockDiv);
3019
- }
3020
- function getDomRawPos(node, offset, blockDiv, token) {
3021
- if (node === blockDiv) {
3022
- const sel = window.getSelection();
3023
- if (sel?.focusNode && sel.focusNode !== blockDiv) return getDomRawPos(sel.focusNode, sel.focusOffset, blockDiv, token);
3024
- return token.position.end;
3025
- }
3026
- if (node.nodeType === Node.TEXT_NODE && node.parentElement === blockDiv) {
3027
- if (token.type === "mark") return getDomRawPosInMark(node, offset, blockDiv, token);
3028
- return token.position.start + Math.min(offset, token.content.length);
3029
- }
3030
- let child = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
3031
- while (child && child.parentElement !== blockDiv) child = child.parentElement;
3032
- if (!child) return token.position.end;
3033
- if (token.type === "mark") return getDomRawPosInMark(node, offset, blockDiv, token);
3034
- return token.position.start + Math.min(offset, token.content.length);
3035
- }
3036
- function getDomRawPosInMark(node, offset, markElement, markToken) {
3037
- if (markToken.children.length === 0) {
3038
- if (offset === 0) return markToken.position.start;
3039
- const nestedLen = markToken.slot?.content.length ?? markToken.value.length;
3040
- if (nestedLen > 0 && offset >= nestedLen) {
3041
- if (markToken.content.endsWith("\n\n") && markToken.slot) return markToken.slot.end;
3042
- return markToken.position.end;
3043
- }
3044
- return (markToken.slot?.start ?? markToken.position.start) + Math.min(offset, nestedLen);
3045
- }
3046
- let tokenIdx = 0;
3047
- for (const childNode of Array.from(markElement.childNodes)) {
3048
- if (tokenIdx >= markToken.children.length) break;
3049
- const tokenChild = markToken.children[tokenIdx];
3050
- if (isHtmlElement(childNode) && tokenChild.type === "text") {
3051
- if (!isTextTokenSpan(childNode)) continue;
3052
- if (node === childNode) {
3053
- const charOffset = offset === 0 ? 0 : tokenChild.content.length;
3054
- return tokenChild.position.start + Math.min(charOffset, tokenChild.content.length);
3055
- }
3056
- if (childNode.contains(node)) return tokenChild.position.start + Math.min(offset, tokenChild.content.length);
3057
- tokenIdx++;
3058
- } else if (isTextNode(childNode) && tokenChild.type === "text") {
3059
- if (node === childNode) return tokenChild.position.start + Math.min(offset, tokenChild.content.length);
3060
- tokenIdx++;
3061
- } else if (isHtmlElement(childNode) && tokenChild.type === "mark") {
3062
- if (childNode === node || childNode.contains(node)) return getDomRawPosInMark(node, offset, childNode, tokenChild);
3063
- tokenIdx++;
3064
- }
3065
- }
3066
- return markToken.slot?.end ?? markToken.position.end;
3067
- }
3068
- function setCaretInMarkAtRawPos(markElement, markToken, rawAbsolutePos) {
3069
- const sel = window.getSelection();
3070
- if (!sel) return false;
3071
- let tokenIdx = 0;
3072
- for (const childNode of Array.from(markElement.childNodes)) {
3073
- if (tokenIdx >= markToken.children.length) break;
3074
- const tokenChild = markToken.children[tokenIdx];
3075
- if (isHtmlElement(childNode) && tokenChild.type === "text") {
3076
- if (!isTextTokenSpan(childNode)) continue;
3077
- if (rawAbsolutePos >= tokenChild.position.start && rawAbsolutePos <= tokenChild.position.end) {
3078
- const rawTextNode = childNode.firstChild;
3079
- const textNode = isTextNode(rawTextNode) ? rawTextNode : null;
3080
- const offset = rawAbsolutePos - tokenChild.position.start;
3081
- if (textNode) {
3082
- const range = document.createRange();
3083
- range.setStart(textNode, Math.min(offset, textNode.length));
3084
- range.collapse(true);
3085
- sel.removeAllRanges();
3086
- sel.addRange(range);
3087
- } else {
3088
- const range = document.createRange();
3089
- range.setStart(childNode, 0);
3090
- range.collapse(true);
3091
- sel.removeAllRanges();
3092
- sel.addRange(range);
3093
- }
3094
- return true;
3095
- }
3096
- tokenIdx++;
3097
- } else if (isTextNode(childNode) && tokenChild.type === "text") {
3098
- if (rawAbsolutePos >= tokenChild.position.start && rawAbsolutePos <= tokenChild.position.end) {
3099
- const offset = Math.min(rawAbsolutePos - tokenChild.position.start, childNode.length);
3100
- const range = document.createRange();
3101
- range.setStart(childNode, offset);
3102
- range.collapse(true);
3103
- sel.removeAllRanges();
3104
- sel.addRange(range);
3105
- return true;
3106
- }
3107
- tokenIdx++;
3108
- } else if (isHtmlElement(childNode) && tokenChild.type === "mark") {
3109
- const nextChild = tokenIdx + 1 < markToken.children.length ? markToken.children[tokenIdx + 1] : null;
3110
- if (!(rawAbsolutePos === tokenChild.position.end && nextChild?.position.start === rawAbsolutePos) && rawAbsolutePos >= tokenChild.position.start && rawAbsolutePos <= tokenChild.position.end) return setCaretInMarkAtRawPos(childNode, tokenChild, rawAbsolutePos);
3111
- tokenIdx++;
3278
+ e.preventDefault();
3279
+ store.caret.selectAll();
3112
3280
  }
3281
+ });
3282
+ }
3283
+ function shiftFocus(store, event, direction) {
3284
+ const active = document.activeElement instanceof HTMLElement ? document.activeElement : void 0;
3285
+ const located = active ? store.dom.locateNode(active) : void 0;
3286
+ if (!located?.ok) return false;
3287
+ const isFocusedOnMarkElement = active === located.value.tokenElement && !located.value.textElement;
3288
+ const address = located.value.address;
3289
+ const token = store.parsing.index().resolveAddress(address);
3290
+ if (!token.ok) return false;
3291
+ if (!isFocusedOnMarkElement) {
3292
+ const selection = store.dom.readRawSelection();
3293
+ if (!selection.ok || selection.value.range.start !== selection.value.range.end) return false;
3294
+ const atStart = selection.value.range.start <= token.value.position.start;
3295
+ const atEnd = selection.value.range.end >= token.value.position.end;
3296
+ if (direction === "prev" && !atStart) return false;
3297
+ if (direction === "next" && !atEnd) return false;
3298
+ }
3299
+ const path = address.path;
3300
+ const siblingIndex = direction === "prev" ? path[path.length - 1] - 1 : path[path.length - 1] + 1;
3301
+ const siblingPath = [...path.slice(0, -1), siblingIndex];
3302
+ const siblingAddress = store.parsing.index().addressFor(siblingPath);
3303
+ if (!siblingAddress) return false;
3304
+ event.preventDefault();
3305
+ if (!store.dom.focusAddress(siblingAddress, direction === "prev" ? "end" : "start").ok) return false;
3306
+ const sibling = store.parsing.index().resolve(siblingPath);
3307
+ if (sibling?.type === "mark") return true;
3308
+ if (direction === "prev") {
3309
+ store.dom.placeAt(sibling?.position.end ?? 0, "before");
3310
+ return true;
3113
3311
  }
3114
- return false;
3312
+ store.dom.placeAt(sibling?.position.start ?? 0, "after");
3313
+ return true;
3115
3314
  }
3116
3315
  //#endregion
3117
3316
  //#region ../../core/src/features/keyboard/blockEdit.ts
@@ -3120,26 +3319,23 @@ function isTextLikeRow(token) {
3120
3319
  return token.descriptor.hasSlot && token.descriptor.segments.length === 1;
3121
3320
  }
3122
3321
  function enableBlockEdit(store) {
3123
- const container = store.slots.container();
3124
- if (!container) return () => {};
3125
- const scope = effectScope(() => {
3126
- listen(container, "keydown", (e) => {
3127
- if (!store.slots.isBlock()) return;
3128
- if (e.key === KEYBOARD.LEFT || e.key === KEYBOARD.RIGHT) handleBlockArrowLeftRight(store, e, e.key === KEYBOARD.LEFT ? "left" : "right");
3129
- else if (e.key === KEYBOARD.UP || e.key === KEYBOARD.DOWN) handleArrowUpDown(store, e);
3130
- handleDelete$1(store, e);
3131
- handleEnter(store, e);
3132
- });
3133
- listen(container, "beforeinput", (e) => {
3134
- if (!store.slots.isBlock()) return;
3135
- if (e.defaultPrevented) return;
3136
- handleBlockBeforeInput(store, e);
3137
- }, true);
3322
+ const container = store.dom.container();
3323
+ if (!container) return;
3324
+ listen(container, "keydown", (e) => {
3325
+ if (!store.slots.isBlock()) return;
3326
+ if (e.key === KEYBOARD.LEFT || e.key === KEYBOARD.RIGHT) handleBlockArrowLeftRight(store, e, e.key === KEYBOARD.LEFT ? "left" : "right");
3327
+ else if (e.key === KEYBOARD.UP || e.key === KEYBOARD.DOWN) handleArrowUpDown(store, e);
3328
+ handleDelete(store, e);
3329
+ handleEnter(store, e);
3138
3330
  });
3139
- return () => scope();
3331
+ listen(container, "beforeinput", (e) => {
3332
+ if (!store.slots.isBlock()) return;
3333
+ if (e.defaultPrevented) return;
3334
+ handleBlockBeforeInput(store, e);
3335
+ }, true);
3140
3336
  }
3141
- function handleDelete$1(store, event) {
3142
- const container = store.slots.container();
3337
+ function handleDelete(store, event) {
3338
+ const container = store.dom.container();
3143
3339
  if (!container) return;
3144
3340
  const blockDivs = htmlChildren(container);
3145
3341
  const blockIndex = blockDivs.findIndex((div) => div === document.activeElement || div.contains(document.activeElement));
@@ -3148,24 +3344,19 @@ function handleDelete$1(store, event) {
3148
3344
  if (blockIndex >= rows.length) return;
3149
3345
  const token = rows[blockIndex];
3150
3346
  const value = store.value.current();
3151
- if (!store.props.onChange()) return;
3152
3347
  if (event.key === KEYBOARD.BACKSPACE) {
3153
3348
  const blockDiv = blockDivs[blockIndex];
3154
- const caretAtStart = Caret.getCaretIndex(blockDiv) === 0;
3349
+ const caretAtStart = getCaretIndex(blockDiv) === 0;
3155
3350
  if (("content" in token ? token.content : "") === "") {
3156
3351
  event.preventDefault();
3157
3352
  const newValue = rows.length <= 1 ? "" : (() => {
3158
3353
  if (blockIndex >= rows.length - 1) return value.slice(0, rows[blockIndex - 1].position.end);
3159
3354
  return value.slice(0, rows[blockIndex].position.start) + value.slice(rows[blockIndex + 1].position.start);
3160
3355
  })();
3161
- store.value.next(newValue);
3162
- queueMicrotask(() => {
3163
- const target = childAt(container, Math.max(0, blockIndex - 1));
3164
- if (target) {
3165
- target.focus();
3166
- Caret.setCaretToEnd(target);
3167
- }
3168
- });
3356
+ const previous = rows.at(Math.max(0, blockIndex - 1));
3357
+ const pos = previous ? previous.position.end : 0;
3358
+ store.caret.position(pos);
3359
+ store.value.current(newValue);
3169
3360
  return;
3170
3361
  }
3171
3362
  if (caretAtStart && blockIndex > 0) {
@@ -3175,29 +3366,18 @@ function handleDelete$1(store, event) {
3175
3366
  event.preventDefault();
3176
3367
  const joinPos = getMergeDragRowJoinPos(rows, blockIndex);
3177
3368
  const newValue = mergeDragRows(value, rows, blockIndex);
3178
- store.value.next(newValue);
3179
- queueMicrotask(() => {
3180
- const target = childAt(container, blockIndex - 1);
3181
- if (target) {
3182
- target.focus();
3183
- const updatedToken = store.parsing.tokens()[blockIndex - 1];
3184
- setCaretAtRawPos(target, updatedToken, joinPos);
3185
- }
3186
- });
3369
+ store.caret.position(joinPos);
3370
+ store.value.current(newValue);
3187
3371
  return;
3188
3372
  }
3189
3373
  event.preventDefault();
3190
- queueMicrotask(() => {
3191
- const target = blockDivs[blockIndex - 1];
3192
- target.focus();
3193
- if (prevToken.type !== "mark") Caret.setCaretToEnd(target);
3194
- });
3374
+ focusRow(store, prevToken, blockDivs[blockIndex - 1], "end");
3195
3375
  return;
3196
3376
  }
3197
3377
  }
3198
3378
  if (event.key === KEYBOARD.DELETE) {
3199
3379
  const blockDiv = blockDivs[blockIndex];
3200
- const caretIndex = Caret.getCaretIndex(blockDiv);
3380
+ const caretIndex = getCaretIndex(blockDiv);
3201
3381
  const caretAtEnd = caretIndex === blockDiv.textContent.length;
3202
3382
  if (caretIndex === 0 && blockIndex > 0) {
3203
3383
  const prevToken = rows[blockIndex - 1];
@@ -3206,23 +3386,12 @@ function handleDelete$1(store, event) {
3206
3386
  event.preventDefault();
3207
3387
  const joinPos = getMergeDragRowJoinPos(rows, blockIndex);
3208
3388
  const newValue = mergeDragRows(value, rows, blockIndex);
3209
- store.value.next(newValue);
3210
- queueMicrotask(() => {
3211
- const target = childAt(container, blockIndex - 1);
3212
- if (target) {
3213
- target.focus();
3214
- const updatedToken = store.parsing.tokens()[blockIndex - 1];
3215
- setCaretAtRawPos(target, updatedToken, joinPos);
3216
- }
3217
- });
3389
+ store.caret.position(joinPos);
3390
+ store.value.current(newValue);
3218
3391
  return;
3219
3392
  }
3220
3393
  event.preventDefault();
3221
- queueMicrotask(() => {
3222
- const target = blockDivs[blockIndex - 1];
3223
- target.focus();
3224
- if (prevToken.type !== "mark") Caret.setCaretToEnd(target);
3225
- });
3394
+ focusRow(store, prevToken, blockDivs[blockIndex - 1], "end");
3226
3395
  return;
3227
3396
  }
3228
3397
  if (caretAtEnd && blockIndex < rows.length - 1) {
@@ -3232,23 +3401,12 @@ function handleDelete$1(store, event) {
3232
3401
  event.preventDefault();
3233
3402
  const joinPos = getMergeDragRowJoinPos(rows, blockIndex + 1);
3234
3403
  const newValue = mergeDragRows(value, rows, blockIndex + 1);
3235
- store.value.next(newValue);
3236
- queueMicrotask(() => {
3237
- const target = childAt(container, blockIndex);
3238
- if (target) {
3239
- target.focus();
3240
- const updatedToken = store.parsing.tokens()[blockIndex];
3241
- setCaretAtRawPos(target, updatedToken, joinPos);
3242
- }
3243
- });
3404
+ store.caret.position(joinPos);
3405
+ store.value.current(newValue);
3244
3406
  return;
3245
3407
  }
3246
3408
  event.preventDefault();
3247
- queueMicrotask(() => {
3248
- const target = blockDivs[blockIndex + 1];
3249
- target.focus();
3250
- Caret.trySetIndex(target, 0);
3251
- });
3409
+ focusRow(store, nextToken, blockDivs[blockIndex + 1], "start");
3252
3410
  return;
3253
3411
  }
3254
3412
  }
@@ -3256,7 +3414,7 @@ function handleDelete$1(store, event) {
3256
3414
  function handleEnter(store, event) {
3257
3415
  if (event.key !== KEYBOARD.ENTER) return;
3258
3416
  if (event.shiftKey) return;
3259
- const container = store.slots.container();
3417
+ const container = store.dom.container();
3260
3418
  if (!container) return;
3261
3419
  const activeElement = document.activeElement;
3262
3420
  if (!isHtmlElement(activeElement) || !container.contains(activeElement)) return;
@@ -3270,41 +3428,37 @@ function handleEnter(store, event) {
3270
3428
  if (blockIndex === -1) return;
3271
3429
  const rows = store.parsing.tokens();
3272
3430
  const token = rows[blockIndex];
3273
- const blockDiv = blockDivs[blockIndex];
3274
3431
  const value = store.value.current();
3275
- if (!store.props.onChange()) return;
3276
3432
  const newRowContent = createRowContent(store.props.options());
3277
3433
  if (!isTextLikeRow(token)) {
3278
3434
  const newValue = addDragRow(value, rows, blockIndex, newRowContent);
3279
- store.value.next(newValue);
3280
- queueMicrotask(() => {
3281
- const newBlockIndex = blockIndex + 1;
3282
- if (newBlockIndex < container.children.length) {
3283
- const newBlockEl = childAt(container, newBlockIndex);
3284
- if (newBlockEl) {
3285
- newBlockEl.focus();
3286
- Caret.trySetIndex(newBlockEl, 0);
3287
- }
3288
- }
3289
- });
3435
+ const pos = token.position.end + newRowContent.length;
3436
+ store.caret.position(pos);
3437
+ store.value.current(newValue);
3290
3438
  return;
3291
3439
  }
3292
- const absolutePos = getCaretRawPosInBlock(blockDiv, token);
3293
- const newValue = value.slice(0, absolutePos) + newRowContent + value.slice(absolutePos);
3294
- store.value.next(newValue);
3295
- queueMicrotask(() => {
3296
- const newBlockIndex = blockIndex + 1;
3297
- if (newBlockIndex < container.children.length) {
3298
- const newBlockEl = childAt(container, newBlockIndex);
3299
- if (newBlockEl) {
3300
- newBlockEl.focus();
3301
- Caret.trySetIndex(newBlockEl, 0);
3302
- }
3303
- }
3304
- });
3440
+ const raw = store.dom.readRawSelection();
3441
+ const absolutePos = raw.ok ? raw.value.range.start : token.position.end;
3442
+ store.edit.replace({
3443
+ start: absolutePos,
3444
+ end: absolutePos
3445
+ }, newRowContent);
3446
+ }
3447
+ function focusRow(store, token, row, caret) {
3448
+ if (token.type === "mark") {
3449
+ const path = store.parsing.index().pathFor(token);
3450
+ const address = path ? store.parsing.index().addressFor(path) : void 0;
3451
+ if (address && store.dom.focusAddress(address).ok) return;
3452
+ }
3453
+ row.focus();
3454
+ if (caret === "start") {
3455
+ setAtElement(row, 0);
3456
+ return;
3457
+ }
3458
+ setAtElement(row, Infinity);
3305
3459
  }
3306
3460
  function handleBlockArrowLeftRight(store, event, direction) {
3307
- const container = store.slots.container();
3461
+ const container = store.dom.container();
3308
3462
  if (!container) return false;
3309
3463
  const activeElement = document.activeElement;
3310
3464
  if (!isHtmlElement(activeElement) || !container.contains(activeElement)) return false;
@@ -3313,24 +3467,24 @@ function handleBlockArrowLeftRight(store, event, direction) {
3313
3467
  if (blockIndex === -1) return false;
3314
3468
  const blockDiv = blockDivs[blockIndex];
3315
3469
  if (direction === "left") {
3316
- if (Caret.getCaretIndex(blockDiv) !== 0) return false;
3470
+ if (getCaretIndex(blockDiv) !== 0) return false;
3317
3471
  if (blockIndex === 0) return true;
3318
3472
  event.preventDefault();
3319
3473
  const prevBlock = blockDivs[blockIndex - 1];
3320
3474
  prevBlock.focus();
3321
- Caret.setCaretToEnd(prevBlock);
3475
+ setAtElement(prevBlock, Infinity);
3322
3476
  return true;
3323
3477
  }
3324
- if (Caret.getCaretIndex(blockDiv) !== blockDiv.textContent.length) return false;
3478
+ if (getCaretIndex(blockDiv) !== blockDiv.textContent.length) return false;
3325
3479
  if (blockIndex >= blockDivs.length - 1) return true;
3326
3480
  event.preventDefault();
3327
3481
  const nextBlock = blockDivs[blockIndex + 1];
3328
3482
  nextBlock.focus();
3329
- Caret.trySetIndex(nextBlock, 0);
3483
+ setAtElement(nextBlock, 0);
3330
3484
  return true;
3331
3485
  }
3332
3486
  function handleArrowUpDown(store, event) {
3333
- const container = store.slots.container();
3487
+ const container = store.dom.container();
3334
3488
  if (!container) return;
3335
3489
  const activeElement = document.activeElement;
3336
3490
  if (!isHtmlElement(activeElement) || !container.contains(activeElement)) return;
@@ -3339,78 +3493,37 @@ function handleArrowUpDown(store, event) {
3339
3493
  if (blockIndex === -1) return;
3340
3494
  const blockDiv = blockDivs[blockIndex];
3341
3495
  if (event.key === KEYBOARD.UP) {
3342
- if (!Caret.isCaretOnFirstLine(blockDiv)) return;
3496
+ if (!isOnFirstLine(blockDiv)) return;
3343
3497
  if (blockIndex === 0) return;
3344
3498
  event.preventDefault();
3345
- const caretX = Caret.getCaretRect()?.left ?? blockDiv.getBoundingClientRect().left;
3499
+ const caretX = getRect()?.left ?? blockDiv.getBoundingClientRect().left;
3346
3500
  const prevBlockDiv = blockDivs[blockIndex - 1];
3347
3501
  prevBlockDiv.focus();
3348
- const prevRect = prevBlockDiv.getBoundingClientRect();
3349
- Caret.setAtX(prevBlockDiv, caretX, prevRect.bottom - 4);
3502
+ setAtX(prevBlockDiv, caretX, prevBlockDiv.getBoundingClientRect().bottom - 4);
3350
3503
  } else if (event.key === KEYBOARD.DOWN) {
3351
- if (!Caret.isCaretOnLastLine(blockDiv)) return;
3504
+ if (!isOnLastLine(blockDiv)) return;
3352
3505
  if (blockIndex >= blockDivs.length - 1) return;
3353
3506
  event.preventDefault();
3354
- const caretX = Caret.getCaretRect()?.left ?? blockDiv.getBoundingClientRect().left;
3507
+ const caretX = getRect()?.left ?? blockDiv.getBoundingClientRect().left;
3355
3508
  const nextBlockDiv = blockDivs[blockIndex + 1];
3356
3509
  nextBlockDiv.focus();
3357
- const nextRect = nextBlockDiv.getBoundingClientRect();
3358
- Caret.setAtX(nextBlockDiv, caretX, nextRect.top + 4);
3510
+ setAtX(nextBlockDiv, caretX, nextBlockDiv.getBoundingClientRect().top + 4);
3359
3511
  }
3360
3512
  }
3361
3513
  function handleBlockBeforeInput(store, event) {
3362
- const container = store.slots.container();
3514
+ const container = store.dom.container();
3363
3515
  if (!container) return;
3364
3516
  const activeElement = document.activeElement;
3365
3517
  if (!isHtmlElement(activeElement) || !container.contains(activeElement)) return;
3366
- const blockDivs = htmlChildren(container);
3367
- const blockIndex = blockDivs.findIndex((div) => div === activeElement || div.contains(activeElement));
3368
- if (blockIndex === -1) return;
3369
- const blockDiv = blockDivs[blockIndex];
3370
- const rows = store.parsing.tokens();
3371
- if (blockIndex >= rows.length) return;
3372
- const token = rows[blockIndex];
3373
- const value = store.value.current();
3374
- const focusAndSetCaret = (newRawPos) => {
3375
- queueMicrotask(() => {
3376
- const target = childAt(container, blockIndex);
3377
- if (!target) return;
3378
- target.focus();
3379
- const updatedToken = store.parsing.tokens()[blockIndex];
3380
- setCaretAtRawPos(target, updatedToken, newRawPos);
3381
- });
3382
- };
3518
+ if (htmlChildren(container).findIndex((div) => div === activeElement || div.contains(activeElement)) === -1) return;
3383
3519
  switch (event.inputType) {
3384
- case "insertText": {
3385
- event.preventDefault();
3386
- const data = event.data ?? "";
3387
- const ranges = event.getTargetRanges();
3388
- let rawFrom;
3389
- let rawTo;
3390
- if (ranges.length > 0) {
3391
- const rawStart = getDomRawPos(ranges[0].startContainer, ranges[0].startOffset, blockDiv, token);
3392
- const rawEnd = getDomRawPos(ranges[0].endContainer, ranges[0].endOffset, blockDiv, token);
3393
- [rawFrom, rawTo] = rawStart <= rawEnd ? [rawStart, rawEnd] : [rawEnd, rawStart];
3394
- } else rawFrom = rawTo = getCaretRawPosInBlock(blockDiv, token);
3395
- store.value.next(value.slice(0, rawFrom) + data + value.slice(rawTo));
3396
- focusAndSetCaret(rawFrom + data.length);
3520
+ case "insertText":
3521
+ replaceBlockRange(store, event, event.data ?? "");
3397
3522
  break;
3398
- }
3399
3523
  case "insertFromPaste":
3400
3524
  case "insertReplacementText": {
3401
- event.preventDefault();
3402
- const c = store.slots.container();
3403
- const pasteData = (c ? consumeMarkupPaste(c) : void 0) ?? event.dataTransfer?.getData("text/plain") ?? "";
3404
- const ranges = event.getTargetRanges();
3405
- let rawFrom;
3406
- let rawTo;
3407
- if (ranges.length > 0) {
3408
- const rawStart = getDomRawPos(ranges[0].startContainer, ranges[0].startOffset, blockDiv, token);
3409
- const rawEnd = getDomRawPos(ranges[0].endContainer, ranges[0].endOffset, blockDiv, token);
3410
- [rawFrom, rawTo] = rawStart <= rawEnd ? [rawStart, rawEnd] : [rawEnd, rawStart];
3411
- } else rawFrom = rawTo = getCaretRawPosInBlock(blockDiv, token);
3412
- store.value.next(value.slice(0, rawFrom) + pasteData + value.slice(rawTo));
3413
- focusAndSetCaret(rawFrom + pasteData.length);
3525
+ const c = store.dom.container();
3526
+ replaceBlockRange(store, event, (c ? consumeMarkupPaste(c) : void 0) ?? event.dataTransfer?.getData("text/plain") ?? "");
3414
3527
  break;
3415
3528
  }
3416
3529
  case "deleteContentBackward":
@@ -3418,88 +3531,112 @@ function handleBlockBeforeInput(store, event) {
3418
3531
  case "deleteWordBackward":
3419
3532
  case "deleteWordForward":
3420
3533
  case "deleteSoftLineBackward":
3421
- case "deleteSoftLineForward": {
3422
- const ranges = event.getTargetRanges();
3423
- if (!ranges.length) return;
3424
- const rawStart = getDomRawPos(ranges[0].startContainer, ranges[0].startOffset, blockDiv, token);
3425
- const rawEnd = getDomRawPos(ranges[0].endContainer, ranges[0].endOffset, blockDiv, token);
3426
- const [rawFrom, rawTo] = rawStart <= rawEnd ? [rawStart, rawEnd] : [rawEnd, rawStart];
3427
- if (rawFrom === rawTo) return;
3428
- event.preventDefault();
3429
- store.value.next(value.slice(0, rawFrom) + value.slice(rawTo));
3430
- focusAndSetCaret(rawFrom);
3534
+ case "deleteSoftLineForward":
3535
+ replaceBlockRange(store, event, "");
3431
3536
  break;
3432
- }
3433
3537
  }
3434
3538
  }
3539
+ function replaceBlockRange(store, event, replacement) {
3540
+ const raw = rawRangeFromInputEvent$1(store, event);
3541
+ if (!raw.ok) return;
3542
+ const range = rangeForBlockInput(store, event, raw.value.range);
3543
+ if (!range) return;
3544
+ event.preventDefault();
3545
+ store.edit.replace(range, replacement);
3546
+ }
3547
+ function rawRangeFromInputEvent$1(store, event) {
3548
+ const ranges = event.getTargetRanges();
3549
+ if (ranges.length === 0) return store.dom.readRawSelection();
3550
+ return rawRangeFromTargetRange$1(store, ranges[0]);
3551
+ }
3552
+ function rawRangeFromTargetRange$1(store, range) {
3553
+ const start = store.dom.rawPositionFromBoundary(range.startContainer, range.startOffset, "after");
3554
+ const end = store.dom.rawPositionFromBoundary(range.endContainer, range.endOffset, "before");
3555
+ if (!start.ok) return {
3556
+ ok: false,
3557
+ reason: rawSelectionReason$1(start)
3558
+ };
3559
+ if (!end.ok) return {
3560
+ ok: false,
3561
+ reason: rawSelectionReason$1(end)
3562
+ };
3563
+ return {
3564
+ ok: true,
3565
+ value: { range: start.value <= end.value ? {
3566
+ start: start.value,
3567
+ end: end.value
3568
+ } : {
3569
+ start: end.value,
3570
+ end: start.value
3571
+ } }
3572
+ };
3573
+ }
3574
+ function rawSelectionReason$1(result) {
3575
+ if (result.ok) return "invalidBoundary";
3576
+ if (result.reason === "composing") return "invalidBoundary";
3577
+ return result.reason;
3578
+ }
3579
+ function rangeForBlockInput(store, event, range) {
3580
+ if (!event.inputType.startsWith("delete")) return range;
3581
+ if (range.start !== range.end) return range;
3582
+ if (event.inputType.endsWith("Backward") && range.start > 0) return {
3583
+ start: range.start - 1,
3584
+ end: range.start
3585
+ };
3586
+ if (event.inputType.endsWith("Forward") && range.end < store.value.current().length) return {
3587
+ start: range.start,
3588
+ end: range.end + 1
3589
+ };
3590
+ }
3435
3591
  //#endregion
3436
3592
  //#region ../../core/src/features/keyboard/input.ts
3437
3593
  function enableInput(store) {
3438
- const container = store.slots.container();
3439
- if (!container) return () => {};
3440
- const scope = effectScope(() => {
3441
- listen(container, "keydown", (e) => {
3442
- if (!store.slots.isBlock()) handleDelete(store, e);
3443
- });
3444
- listen(container, "paste", (e) => {
3445
- const c = store.slots.container();
3446
- if (c) captureMarkupPaste(e, c);
3447
- handlePaste(store, e);
3448
- });
3449
- listen(container, "beforeinput", (e) => {
3450
- handleBeforeInput(store, e);
3451
- }, true);
3594
+ const container = store.dom.container();
3595
+ if (!container) return;
3596
+ let compositionRange;
3597
+ listen(container, "paste", (e) => {
3598
+ const c = store.dom.container();
3599
+ if (c) captureMarkupPaste(e, c);
3600
+ handlePaste(store, e);
3601
+ });
3602
+ listen(container, "compositionstart", () => {
3603
+ const selection = store.dom.readRawSelection();
3604
+ compositionRange = selection.ok ? selection.value.range : void 0;
3605
+ store.dom.compositionStarted();
3606
+ });
3607
+ listen(container, "compositionend", (e) => {
3608
+ const range = compositionRange;
3609
+ compositionRange = void 0;
3610
+ store.dom.compositionEnded();
3611
+ if (store.slots.isBlock()) return;
3612
+ if (!range) return;
3613
+ const data = e.data;
3614
+ store.edit.replace(range, data);
3615
+ });
3616
+ listen(container, "beforeinput", (e) => {
3617
+ handleBeforeInput(store, e);
3618
+ }, true);
3619
+ listen(container, "keydown", (e) => {
3620
+ handleDeleteKey(store, e);
3452
3621
  });
3453
- return () => scope();
3454
3622
  }
3455
- function handleDelete(store, event) {
3456
- const { focus } = store.nodes;
3457
- if (event.key !== KEYBOARD.DELETE && event.key !== KEYBOARD.BACKSPACE) return;
3458
- if (focus.isMark) {
3459
- if (focus.isEditable) {
3460
- if (event.key === KEYBOARD.BACKSPACE && !focus.isCaretAtBeginning) return;
3461
- if (event.key === KEYBOARD.DELETE && !focus.isCaretAtEnd) return;
3462
- }
3623
+ function handleDeleteKey(store, event) {
3624
+ if (store.slots.isBlock()) return;
3625
+ if (event.key !== KEYBOARD.BACKSPACE && event.key !== KEYBOARD.DELETE) return;
3626
+ if (store.caret.isAllSelected()) {
3463
3627
  event.preventDefault();
3464
- deleteMark("self", store);
3628
+ replaceAllContentWith(store, "");
3465
3629
  return;
3466
3630
  }
3467
- if (event.key === KEYBOARD.BACKSPACE) {
3468
- if (focus.isSpan && focus.isCaretAtBeginning && focus.prev.target) {
3469
- event.preventDefault();
3470
- deleteMark("prev", store);
3471
- return;
3472
- }
3473
- }
3474
- if (event.key === KEYBOARD.DELETE) {
3475
- if (focus.isSpan && focus.isCaretAtEnd && focus.next.target) {
3476
- event.preventDefault();
3477
- deleteMark("next", store);
3478
- return;
3479
- }
3480
- }
3481
- if (focus.isSpan && focus.isEditable && window.getSelection()?.isCollapsed) {
3482
- const content = focus.content;
3483
- const caret = focus.caret;
3484
- if (event.key === KEYBOARD.BACKSPACE && caret > 0) {
3485
- event.preventDefault();
3486
- focus.content = content.slice(0, caret - 1) + content.slice(caret);
3487
- focus.caret = caret - 1;
3488
- store.value.change();
3489
- return;
3490
- }
3491
- if (event.key === KEYBOARD.DELETE && caret >= 0 && caret < content.length) {
3492
- event.preventDefault();
3493
- focus.content = content.slice(0, caret) + content.slice(caret + 1);
3494
- focus.caret = caret;
3495
- store.value.change();
3496
- return;
3497
- }
3498
- }
3631
+ const raw = store.dom.readRawSelection();
3632
+ if (!raw.ok) return;
3633
+ const range = rangeForDelete(store, event.key === KEYBOARD.BACKSPACE ? "deleteContentBackward" : "deleteContentForward", raw.value.range);
3634
+ if (!range) return;
3635
+ event.preventDefault();
3636
+ store.edit.replace(range, "");
3499
3637
  }
3500
3638
  function handleBeforeInput(store, event) {
3501
- const selecting = store.caret.selecting();
3502
- if (selecting === "all" && isFullSelection(store)) {
3639
+ if (store.caret.isAllSelected()) {
3503
3640
  if (event.inputType === "insertFromPaste") {
3504
3641
  event.preventDefault();
3505
3642
  return;
@@ -3508,236 +3645,205 @@ function handleBeforeInput(store, event) {
3508
3645
  replaceAllContentWith(store, event.inputType.startsWith("delete") ? "" : event.data ?? "");
3509
3646
  return;
3510
3647
  }
3511
- if (selecting === "all") store.caret.selecting(void 0);
3512
3648
  if (store.slots.isBlock()) return;
3513
- const { focus } = store.nodes;
3514
- if (!focus.target || !focus.isEditable) return;
3515
- if ((event.inputType === "insertFromPaste" || event.inputType === "insertReplacementText") && handleMarkputSpanPaste(store, focus, event)) return;
3516
- if (applySpanInput(focus, event)) store.value.change();
3517
- }
3518
- function handleMarkputSpanPaste(store, focus, event) {
3519
- const container = store.slots.container();
3520
- if (!container) return false;
3521
- const markup = consumeMarkupPaste(container);
3522
- if (!markup) return false;
3649
+ const raw = rawRangeFromInputEvent(store, event);
3650
+ if (!raw.ok) return;
3651
+ const replacement = replacementForInput(store, event);
3652
+ if (replacement === void 0) return;
3653
+ const range = rangeForInput(store, event, raw.value.range);
3654
+ if (!range) return;
3523
3655
  event.preventDefault();
3524
- const token = store.parsing.tokens()[focus.index];
3525
- const offset = focus.caret;
3526
- const currentValue = store.value.current();
3527
- const ranges = event.getTargetRanges();
3528
- const childElement = container.children[focus.index];
3529
- let rawInsertPos;
3530
- let rawEndPos;
3531
- if (ranges.length > 0) {
3532
- const cumStart = getBoundaryOffset(ranges[0], childElement, true);
3533
- const cumEnd = getBoundaryOffset(ranges[0], childElement, false);
3534
- rawInsertPos = token.position.start + cumStart;
3535
- rawEndPos = token.position.start + cumEnd;
3536
- } else {
3537
- rawInsertPos = token.position.start + offset;
3538
- rawEndPos = token.position.start + offset;
3539
- }
3540
- const caretPos = rawInsertPos + markup.length;
3541
- const newValue = currentValue.slice(0, rawInsertPos) + markup + currentValue.slice(rawEndPos);
3542
- store.value.next(newValue);
3543
- const newTokens = store.parsing.tokens();
3544
- let targetIdx = newTokens.findIndex((t) => t.type === "text" && caretPos >= t.position.start && caretPos <= t.position.end);
3545
- if (targetIdx === -1) targetIdx = newTokens.length - 1;
3546
- const caretWithinToken = caretPos - newTokens[targetIdx].position.start;
3547
- store.caret.recovery({
3548
- anchor: store.nodes.focus,
3549
- caret: caretWithinToken,
3550
- isNext: true,
3551
- childIndex: targetIdx - 2
3552
- });
3553
- return true;
3656
+ store.edit.replace(range, replacement);
3554
3657
  }
3555
- function applySpanInput(focus, event) {
3556
- const offset = focus.caret;
3557
- const content = focus.content;
3558
- let newContent;
3559
- let newCaret;
3560
- switch (event.inputType) {
3561
- case "insertText": {
3562
- event.preventDefault();
3563
- const data = event.data ?? "";
3564
- newContent = content.slice(0, offset) + data + content.slice(offset);
3565
- newCaret = offset + data.length;
3566
- break;
3567
- }
3568
- case "deleteContentBackward":
3569
- case "deleteContentForward":
3570
- case "deleteWordBackward":
3571
- case "deleteWordForward":
3572
- case "deleteSoftLineBackward":
3573
- case "deleteSoftLineForward": {
3574
- const ranges = event.getTargetRanges();
3575
- let startOffset;
3576
- let endOffset;
3577
- if (ranges.length > 0 && ranges[0].startOffset !== ranges[0].endOffset) {
3578
- startOffset = ranges[0].startOffset;
3579
- endOffset = ranges[0].endOffset;
3580
- } else if (event.inputType === "deleteContentBackward" && offset > 0) {
3581
- startOffset = offset - 1;
3582
- endOffset = offset;
3583
- } else if (event.inputType === "deleteContentForward" && offset < content.length) {
3584
- startOffset = offset;
3585
- endOffset = offset + 1;
3586
- } else return false;
3587
- event.preventDefault();
3588
- newContent = content.slice(0, startOffset) + content.slice(endOffset);
3589
- newCaret = startOffset;
3590
- break;
3591
- }
3592
- case "insertFromPaste":
3593
- case "insertReplacementText": {
3594
- const text = event.dataTransfer?.getData("text/plain") ?? "";
3595
- const ranges = event.getTargetRanges();
3596
- const start = ranges[0]?.startOffset ?? offset;
3597
- const end = ranges[0]?.endOffset ?? offset;
3598
- event.preventDefault();
3599
- newContent = content.slice(0, start) + text + content.slice(end);
3600
- newCaret = start + text.length;
3601
- break;
3602
- }
3603
- default: return false;
3658
+ function rawRangeFromInputEvent(store, event) {
3659
+ const ranges = getTargetRanges(event);
3660
+ if (ranges.length === 0) return store.dom.readRawSelection();
3661
+ return rawRangeFromTargetRange(store, ranges[0]);
3662
+ }
3663
+ function rawRangeFromTargetRange(store, range) {
3664
+ const start = store.dom.rawPositionFromBoundary(range.startContainer, range.startOffset, "after");
3665
+ const end = store.dom.rawPositionFromBoundary(range.endContainer, range.endOffset, "before");
3666
+ if (!start.ok) return {
3667
+ ok: false,
3668
+ reason: rawSelectionReason(start)
3669
+ };
3670
+ if (!end.ok) return {
3671
+ ok: false,
3672
+ reason: rawSelectionReason(end)
3673
+ };
3674
+ return {
3675
+ ok: true,
3676
+ value: { range: start.value <= end.value ? {
3677
+ start: start.value,
3678
+ end: end.value
3679
+ } : {
3680
+ start: end.value,
3681
+ end: start.value
3682
+ } }
3683
+ };
3684
+ }
3685
+ function rawSelectionReason(result) {
3686
+ if (result.ok) return "invalidBoundary";
3687
+ if (result.reason === "composing") return "invalidBoundary";
3688
+ return result.reason;
3689
+ }
3690
+ function getTargetRanges(event) {
3691
+ return event.getTargetRanges();
3692
+ }
3693
+ function replacementForInput(store, event) {
3694
+ if (event.inputType.startsWith("delete")) return "";
3695
+ if (event.inputType === "insertFromPaste" || event.inputType === "insertReplacementText") {
3696
+ const container = store.dom.container();
3697
+ return (container ? consumeMarkupPaste(container) : void 0) ?? event.dataTransfer?.getData("text/plain") ?? event.data ?? "";
3604
3698
  }
3605
- focus.content = newContent;
3606
- focus.caret = newCaret;
3607
- return true;
3699
+ if (event.inputType === "insertText") return event.data ?? "";
3608
3700
  }
3609
- function handlePaste(store, event) {
3610
- const selecting = store.caret.selecting();
3611
- if (selecting !== "all" || !isFullSelection(store)) {
3612
- if (selecting === "all") store.caret.selecting(void 0);
3613
- return;
3701
+ function rangeForInput(store, event, range) {
3702
+ if (!event.inputType.startsWith("delete")) return range;
3703
+ return rangeForDelete(store, event.inputType, range);
3704
+ }
3705
+ function rangeForDelete(store, inputType, range) {
3706
+ if (range.start !== range.end) return range;
3707
+ const adjacentMark = adjacentMarkRange(store.parsing.tokens(), range.start, inputType.endsWith("Backward"));
3708
+ if (adjacentMark) return adjacentMark;
3709
+ if (inputType.endsWith("Backward") && range.start > 0) return {
3710
+ start: range.start - 1,
3711
+ end: range.start
3712
+ };
3713
+ if (inputType.endsWith("Forward") && range.end < store.value.current().length) return {
3714
+ start: range.start,
3715
+ end: range.end + 1
3716
+ };
3717
+ }
3718
+ function adjacentMarkRange(tokens, position, backward) {
3719
+ for (const token of tokens) {
3720
+ const nested = token.type === "mark" ? adjacentMarkRange(token.children, position, backward) : void 0;
3721
+ if (nested) return nested;
3722
+ if (token.type === "mark" && (backward ? token.position.end === position : token.position.start === position)) return token.position;
3614
3723
  }
3724
+ }
3725
+ function handlePaste(store, event) {
3726
+ if (!store.caret.isAllSelected()) return;
3615
3727
  event.preventDefault();
3616
- const c = store.slots.container();
3728
+ const c = store.dom.container();
3617
3729
  replaceAllContentWith(store, (c ? consumeMarkupPaste(c) : void 0) ?? event.clipboardData?.getData("text/plain") ?? "");
3618
3730
  }
3619
3731
  function replaceAllContentWith(store, newContent) {
3620
- store.nodes.focus.target = null;
3621
- store.caret.selecting(void 0);
3622
- store.value.last(newContent);
3623
- store.props.onChange()?.(newContent);
3624
- if (store.props.value() === void 0) store.parsing.tokens(store.parsing.parser()?.parse(newContent) ?? [{
3625
- type: "text",
3626
- content: newContent,
3627
- position: {
3628
- start: 0,
3629
- end: newContent.length
3630
- }
3631
- }]);
3632
- queueMicrotask(() => {
3633
- const rawFirstChild = store.slots.container()?.firstChild;
3634
- const firstChild = isHtmlElement(rawFirstChild) ? rawFirstChild : null;
3635
- if (firstChild) {
3636
- store.caret.recovery({
3637
- anchor: store.nodes.focus,
3638
- caret: newContent.length
3639
- });
3640
- firstChild.focus();
3641
- }
3642
- });
3732
+ store.caret.position(newContent.length);
3733
+ store.value.current(newContent);
3643
3734
  }
3644
3735
  //#endregion
3645
- //#region ../../core/src/features/keyboard/KeyboardFeature.ts
3646
- var KeyboardFeature = class {
3647
- #disposers = [];
3648
- constructor(_store) {
3649
- this._store = _store;
3650
- }
3651
- enable() {
3652
- if (this.#disposers.length) return;
3653
- this.#disposers = [
3654
- enableInput(this._store),
3655
- enableBlockEdit(this._store),
3656
- enableArrowNav(this._store)
3657
- ];
3658
- }
3659
- disable() {
3660
- this.#disposers.forEach((d) => d());
3661
- this.#disposers = [];
3736
+ //#region ../../core/src/features/keyboard/KeyboardController.ts
3737
+ var KeyboardController = class {
3738
+ constructor(lifecycle, dom, value, caret, edit, slots, parsing, props) {
3739
+ const ctx = {
3740
+ dom,
3741
+ value,
3742
+ caret,
3743
+ edit,
3744
+ slots,
3745
+ parsing,
3746
+ props
3747
+ };
3748
+ lifecycle.onMounted(() => {
3749
+ enableInput(ctx);
3750
+ enableBlockEdit(ctx);
3751
+ enableArrowNav(ctx);
3752
+ });
3662
3753
  }
3663
3754
  };
3664
3755
  //#endregion
3665
- //#region ../../core/src/features/lifecycle/LifecycleFeature.ts
3666
- var LifecycleFeature = class {
3667
- constructor(_store) {
3668
- this._store = _store;
3756
+ //#region ../../core/src/features/lifecycle/Lifecycle.ts
3757
+ var Lifecycle = class {
3758
+ constructor() {
3669
3759
  this.mounted = event();
3670
3760
  this.unmounted = event();
3671
3761
  this.rendered = event();
3672
3762
  }
3673
- enable() {}
3674
- disable() {}
3763
+ /**
3764
+ * Run `setup` when the editor is mounted. Any reactive subscription
3765
+ * created inside `setup` (`watch`, `listen`, `effect`, nested
3766
+ * `effectScope`) is automatically disposed on `unmounted` and re-created
3767
+ * on the next `mounted`.
3768
+ */
3769
+ onMounted(setup) {
3770
+ let scope;
3771
+ watch(this.mounted, () => {
3772
+ if (scope) return;
3773
+ scope = effectScope(setup);
3774
+ });
3775
+ watch(this.unmounted, () => {
3776
+ scope?.();
3777
+ scope = void 0;
3778
+ });
3779
+ }
3675
3780
  };
3676
3781
  //#endregion
3677
- //#region ../../core/src/features/mark/MarkHandler.ts
3678
- var MarkHandler = class {
3679
- #store;
3680
- #token;
3681
- #readOnly;
3682
- constructor(param) {
3683
- this.change = (props) => {
3684
- this.#token.content = props.content;
3685
- this.#token.value = props.value ?? "";
3686
- if (props.meta !== void 0) this.#token.meta = props.meta;
3687
- this.#emitChange();
3688
- };
3689
- this.remove = () => this.#store.mark.remove({ token: this.#token });
3690
- this.ref = param.ref;
3691
- this.#store = param.store;
3692
- this.#token = param.token;
3693
- }
3694
- get readOnly() {
3695
- return this.#readOnly;
3696
- }
3697
- set readOnly(value) {
3698
- this.#readOnly = value;
3699
- }
3700
- get content() {
3701
- return this.#token.content;
3702
- }
3703
- set content(value) {
3704
- this.#token.content = value;
3705
- this.#emitChange();
3782
+ //#region ../../core/src/features/mark/MarkController.ts
3783
+ var MarkController = class MarkController {
3784
+ #shape;
3785
+ constructor(store, address, snapshot, shape) {
3786
+ this.store = store;
3787
+ this.address = address;
3788
+ this.snapshot = snapshot;
3789
+ this.#shape = shape;
3790
+ }
3791
+ static fromToken(store, token) {
3792
+ const index = store.parsing.index();
3793
+ const path = index.pathFor(token);
3794
+ if (!path) throw new Error("Cannot create MarkController for unindexed token");
3795
+ const address = index.addressFor(path);
3796
+ if (!address) throw new Error("Cannot create MarkController for unresolved token path");
3797
+ return new MarkController(store, address, {
3798
+ value: token.value,
3799
+ meta: token.meta,
3800
+ slot: token.slot?.content,
3801
+ readOnly: store.props.readOnly()
3802
+ }, snapshotTokenShape(token));
3706
3803
  }
3707
3804
  get value() {
3708
- return this.#token.value;
3709
- }
3710
- set value(v) {
3711
- this.#token.value = v ?? "";
3712
- this.#emitChange();
3805
+ return this.snapshot.value;
3713
3806
  }
3714
3807
  get meta() {
3715
- return this.#token.meta;
3716
- }
3717
- set meta(v) {
3718
- this.#token.meta = v;
3719
- this.#emitChange();
3808
+ return this.snapshot.meta;
3720
3809
  }
3721
3810
  get slot() {
3722
- return this.#token.slot?.content;
3723
- }
3724
- get #tokenInfo() {
3725
- return findToken(this.#store.parsing.tokens(), this.#token);
3726
- }
3727
- get depth() {
3728
- return this.#tokenInfo?.depth ?? 0;
3729
- }
3730
- get hasChildren() {
3731
- return this.#token.children.some((child) => child.type === "mark");
3811
+ return this.snapshot.slot;
3732
3812
  }
3733
- get parent() {
3734
- return this.#tokenInfo?.parent;
3813
+ get readOnly() {
3814
+ return this.snapshot.readOnly;
3815
+ }
3816
+ remove() {
3817
+ const resolved = this.#resolve();
3818
+ if (!resolved) return;
3819
+ this.store.value.replace(resolved.position, "");
3820
+ }
3821
+ update(patch) {
3822
+ const resolved = this.#resolve();
3823
+ if (!resolved) return;
3824
+ const token = resolved;
3825
+ const value = patch.value ?? token.value;
3826
+ const meta = patch.meta?.kind === "clear" ? void 0 : patch.meta?.kind === "set" ? patch.meta.value : token.meta;
3827
+ const slot = patch.slot?.kind === "clear" ? void 0 : patch.slot?.kind === "set" ? patch.slot.value : token.slot?.content;
3828
+ const serialized = this.#serialize(token, {
3829
+ value,
3830
+ meta,
3831
+ slot
3832
+ });
3833
+ this.store.value.replace(token.position, serialized);
3735
3834
  }
3736
- get tokens() {
3737
- return this.#token.children;
3835
+ #serialize(token, fields) {
3836
+ return annotate(token.descriptor.markup, {
3837
+ value: fields.value,
3838
+ meta: token.descriptor.gapTypes.includes("meta") ? fields.meta ?? "" : void 0,
3839
+ slot: token.descriptor.hasSlot ? fields.slot ?? "" : void 0
3840
+ });
3738
3841
  }
3739
- #emitChange() {
3740
- this.#store.value.change();
3842
+ #resolve() {
3843
+ if (this.store.props.readOnly()) return void 0;
3844
+ const resolved = this.store.parsing.index().resolveAddress(this.address, this.#shape);
3845
+ if (!resolved.ok || resolved.value.type !== "mark") return void 0;
3846
+ return resolved.value;
3741
3847
  }
3742
3848
  };
3743
3849
  //#endregion
@@ -3820,55 +3926,33 @@ function buildContainerProps(isDraggableBlock, readOnly, className, style, slotP
3820
3926
  };
3821
3927
  }
3822
3928
  var SlotsFeature = class {
3823
- constructor(_store) {
3824
- this._store = _store;
3825
- this.container = signal(null);
3826
- this.isBlock = computed(() => this._store.props.layout() === "block");
3827
- this.isDraggable = computed(() => !!this._store.props.draggable());
3828
- this.containerComponent = computed(() => resolveSlot("container", this._store.props.slots()));
3829
- this.containerProps = computed(() => buildContainerProps(this.isDraggable() && this.isBlock(), this._store.props.readOnly(), this._store.props.className(), this._store.props.style(), this._store.props.slotProps()), { equals: shallow });
3830
- this.blockComponent = computed(() => resolveSlot("block", this._store.props.slots()));
3831
- this.blockProps = computed(() => resolveSlotProps("block", this._store.props.slotProps()));
3832
- this.spanComponent = computed(() => resolveSlot("span", this._store.props.slots()));
3833
- this.spanProps = computed(() => resolveSlotProps("span", this._store.props.slotProps()));
3834
- }
3835
- enable() {}
3836
- disable() {}
3929
+ constructor(props) {
3930
+ this.props = props;
3931
+ this.isBlock = computed(() => this.props.layout() === "block");
3932
+ this.isDraggable = computed(() => !!this.props.draggable());
3933
+ this.containerComponent = computed(() => resolveSlot("container", this.props.slots()));
3934
+ this.containerProps = computed(() => buildContainerProps(this.isDraggable() && this.isBlock(), this.props.readOnly(), this.props.className(), this.props.style(), this.props.slotProps()), { equals: shallow });
3935
+ this.blockComponent = computed(() => resolveSlot("block", this.props.slots()));
3936
+ this.blockProps = computed(() => resolveSlotProps("block", this.props.slotProps()));
3937
+ this.spanComponent = computed(() => resolveSlot("span", this.props.slots()));
3938
+ this.spanProps = computed(() => resolveSlotProps("span", this.props.slotProps()));
3939
+ }
3837
3940
  };
3838
3941
  //#endregion
3839
3942
  //#region ../../core/src/features/mark/MarkFeature.ts
3840
3943
  var MarkFeature = class {
3841
- #scope;
3842
- constructor(_store) {
3843
- this._store = _store;
3944
+ constructor(props) {
3945
+ this.props = props;
3844
3946
  this.enabled = computed(() => {
3845
- if (this._store.props.Mark()) return true;
3846
- return this._store.props.options().some((opt) => "Mark" in opt && opt.Mark != null);
3947
+ if (this.props.Mark()) return true;
3948
+ return this.props.options().some((opt) => "Mark" in opt && opt.Mark != null);
3847
3949
  });
3848
3950
  this.slot = computed(() => {
3849
- const options = this._store.props.options();
3850
- const Mark = this._store.props.Mark();
3851
- const Span = this._store.props.Span();
3951
+ const options = this.props.options();
3952
+ const Mark = this.props.Mark();
3953
+ const Span = this.props.Span();
3852
3954
  return (token) => resolveMarkSlot(token, options, Mark, Span);
3853
3955
  });
3854
- this.remove = event();
3855
- }
3856
- enable() {
3857
- if (this.#scope) return;
3858
- this.#scope = effectScope(() => {
3859
- watch(this.remove, (payload) => {
3860
- const { token } = payload;
3861
- const tokens = this._store.parsing.tokens();
3862
- if (!findToken(tokens, token)) return;
3863
- const value = toString(tokens);
3864
- const nextValue = value.slice(0, token.position.start) + value.slice(token.position.end);
3865
- this._store.value.next(nextValue);
3866
- });
3867
- });
3868
- }
3869
- disable() {
3870
- this.#scope?.();
3871
- this.#scope = void 0;
3872
3956
  }
3873
3957
  };
3874
3958
  //#endregion
@@ -3888,8 +3972,8 @@ function createMarkFromOverlay(match, value, meta) {
3888
3972
  meta,
3889
3973
  content: "",
3890
3974
  position: {
3891
- start: match.index,
3892
- end: match.index + match.span.length
3975
+ start: match.range.start,
3976
+ end: match.range.end
3893
3977
  },
3894
3978
  descriptor: {
3895
3979
  markup,
@@ -3905,92 +3989,124 @@ function createMarkFromOverlay(match, value, meta) {
3905
3989
  };
3906
3990
  }
3907
3991
  //#endregion
3908
- //#region ../../core/src/features/overlay/OverlayFeature.ts
3909
- var OverlayFeature = class {
3992
+ //#region ../../core/src/features/overlay/OverlayController.ts
3993
+ var OverlayController = class {
3910
3994
  #scope;
3911
- constructor(_store) {
3912
- this._store = _store;
3995
+ constructor(lifecycle, props, value, dom, caret, edit, parsing) {
3996
+ this.lifecycle = lifecycle;
3997
+ this.props = props;
3998
+ this.value = value;
3999
+ this.dom = dom;
4000
+ this.caret = caret;
4001
+ this.edit = edit;
4002
+ this.parsing = parsing;
3913
4003
  this.match = signal(void 0);
3914
4004
  this.element = signal(null);
3915
4005
  this.slot = computed(() => {
3916
- const Overlay = this._store.props.Overlay();
4006
+ const Overlay = this.props.Overlay();
3917
4007
  return (option, defaultComponent) => resolveOverlaySlot(Overlay, option, defaultComponent);
3918
4008
  });
3919
4009
  this.select = event();
3920
4010
  this.close = event();
3921
- }
3922
- #probeTrigger() {
3923
- const match = TriggerFinder.find(this._store.props.options(), (option) => option.overlay?.trigger);
3924
- this.match(match);
3925
- }
3926
- enable() {
3927
- if (this.#scope) return;
3928
- this.#scope = effectScope(() => {
3929
- watch(this.close, () => {
3930
- this.match(void 0);
3931
- });
3932
- watch(this._store.value.change, () => {
3933
- const showOverlayOn = this._store.props.showOverlayOn();
3934
- const type = "change";
3935
- if (showOverlayOn === type || Array.isArray(showOverlayOn) && showOverlayOn.includes(type)) this.#probeTrigger();
3936
- });
3937
- alienEffect(() => {
3938
- if (this.match()) {
3939
- this._store.nodes.input.target = this._store.nodes.focus.target;
3940
- listen(window, "keydown", (e) => {
3941
- if (e.key === KEYBOARD.ESC) this.close();
3942
- });
3943
- listen(document, "click", (e) => {
3944
- const target = e.target instanceof HTMLElement ? e.target : null;
3945
- if (this.element()?.contains(target)) return;
3946
- if (this._store.slots.container()?.contains(target)) return;
3947
- this.close();
3948
- }, true);
3949
- }
3950
- });
3951
- const selectionChangeHandler = () => {
3952
- if (!this._store.slots.container()?.contains(document.activeElement)) return;
3953
- const showOverlayOn = this._store.props.showOverlayOn();
3954
- const type = "selectionChange";
3955
- if (showOverlayOn === type || Array.isArray(showOverlayOn) && showOverlayOn.includes(type)) this.#probeTrigger();
4011
+ this.position = computed(() => {
4012
+ if (!this.match()) return {
4013
+ left: 0,
4014
+ top: 0
4015
+ };
4016
+ const rect = getRect();
4017
+ if (!rect) return {
4018
+ left: 0,
4019
+ top: 0
3956
4020
  };
3957
- listen(document, "selectionchange", selectionChangeHandler);
3958
- watch(this.select, (overlayEvent) => {
3959
- const Mark = this._store.props.Mark();
3960
- const onChange = this._store.props.onChange();
3961
- const { mark, match: { option, span, index, source } } = overlayEvent;
3962
- const markup = option.markup;
3963
- if (!markup) return;
3964
- const annotation = mark.type === "mark" ? annotate(markup, {
3965
- value: mark.value,
3966
- meta: mark.meta
3967
- }) : annotate(markup, { value: mark.content });
3968
- const newSpan = createNewSpan(span, annotation, index, source);
3969
- this._store.caret.recovery(Mark ? {
3970
- caret: 0,
3971
- anchor: this._store.nodes.input.next,
3972
- isNext: true,
3973
- childIndex: this._store.nodes.input.index
3974
- } : {
3975
- caret: index + annotation.length,
3976
- anchor: this._store.nodes.input
4021
+ return {
4022
+ left: rect.left,
4023
+ top: rect.top + rect.height + 1
4024
+ };
4025
+ });
4026
+ const hasOverlayTrigger = computed(() => this.props.options().some((opt) => opt.overlay?.trigger != null));
4027
+ const toggle = (enabled) => {
4028
+ if (enabled && !this.#scope) this.#scope = effectScope(() => {
4029
+ watch(this.close, () => {
4030
+ this.match(void 0);
4031
+ });
4032
+ watch(this.value.current, () => {
4033
+ const showOverlayOn = this.props.showOverlayOn();
4034
+ const type = "change";
4035
+ if (showOverlayOn === type || Array.isArray(showOverlayOn) && showOverlayOn.includes(type)) this.#probeTrigger();
4036
+ });
4037
+ effect(() => {
4038
+ if (this.match()) {
4039
+ listen(window, "keydown", (e) => {
4040
+ if (e.key === KEYBOARD.ESC) this.close();
4041
+ });
4042
+ listen(document, "click", (e) => {
4043
+ const target = e.target instanceof HTMLElement ? e.target : null;
4044
+ if (this.element()?.contains(target)) return;
4045
+ if (this.dom.container()?.contains(target)) return;
4046
+ this.close();
4047
+ }, true);
4048
+ }
4049
+ });
4050
+ const selectionChangeHandler = () => {
4051
+ if (!this.dom.container()?.contains(document.activeElement)) return;
4052
+ const showOverlayOn = this.props.showOverlayOn();
4053
+ const type = "selectionChange";
4054
+ if (showOverlayOn === type || Array.isArray(showOverlayOn) && showOverlayOn.includes(type)) this.#probeTrigger();
4055
+ };
4056
+ listen(document, "selectionchange", selectionChangeHandler);
4057
+ watch(this.select, (overlayEvent) => {
4058
+ const { mark, match: { option, range } } = overlayEvent;
4059
+ const markup = option.markup;
4060
+ if (!markup) return;
4061
+ const annotation = mark.type === "mark" ? annotate(markup, {
4062
+ value: mark.value,
4063
+ meta: mark.meta
4064
+ }) : annotate(markup, { value: mark.content });
4065
+ this.edit.replace(range, annotation);
4066
+ this.match(void 0);
3977
4067
  });
3978
- if (this._store.nodes.input.target) {
3979
- this._store.nodes.input.content = newSpan;
3980
- const tokens = this._store.parsing.tokens();
3981
- const inputToken = tokens[this._store.nodes.input.index];
3982
- if (inputToken.type === "text") inputToken.content = newSpan;
3983
- this._store.nodes.focus.target = this._store.nodes.input.target;
3984
- this._store.nodes.input.clear();
3985
- onChange?.(toString(tokens));
3986
- this._store.parsing.reparse();
3987
- }
3988
4068
  });
4069
+ if (!enabled && this.#scope) {
4070
+ this.#scope();
4071
+ this.#scope = void 0;
4072
+ }
4073
+ };
4074
+ this.lifecycle.onMounted(() => {
4075
+ watch(hasOverlayTrigger, toggle);
4076
+ toggle(hasOverlayTrigger());
3989
4077
  });
3990
4078
  }
3991
- disable() {
3992
- this.#scope?.();
3993
- this.#scope = void 0;
4079
+ #probeTrigger() {
4080
+ const match = TriggerFinder.find(this.props.options(), (option) => option.overlay?.trigger, this.dom) ?? this.#probeTriggerFromCaretRange();
4081
+ this.match(match);
4082
+ }
4083
+ #probeTriggerFromCaretRange() {
4084
+ const sel = this.caret.selection();
4085
+ if (!sel || sel.start !== sel.end) return;
4086
+ const cursor = sel.start;
4087
+ const value = this.value.current();
4088
+ const left = value.slice(0, cursor);
4089
+ const rightWord = value.slice(cursor).match(/^\w*/)?.[0] ?? "";
4090
+ for (const option of this.props.options()) {
4091
+ const trigger = option.overlay?.trigger;
4092
+ if (!trigger) continue;
4093
+ const match = left.match(new RegExp(`${escape(trigger)}(\\w*)$`));
4094
+ if (!match) continue;
4095
+ const [sourceLeft, wordLeft] = match;
4096
+ const source = sourceLeft + rightWord;
4097
+ const start = cursor - sourceLeft.length;
4098
+ return {
4099
+ value: wordLeft + rightWord,
4100
+ source,
4101
+ range: {
4102
+ start,
4103
+ end: start + source.length
4104
+ },
4105
+ span: value,
4106
+ node: window.getSelection()?.anchorNode ?? this.dom.container() ?? document.body,
4107
+ option
4108
+ };
4109
+ }
3994
4110
  }
3995
4111
  };
3996
4112
  //#endregion
@@ -4024,10 +4140,9 @@ function navigateSuggestions(key, activeIndex, length) {
4024
4140
  }
4025
4141
  }
4026
4142
  //#endregion
4027
- //#region ../../core/src/features/props/PropsFeature.ts
4028
- var PropsFeature = class {
4029
- constructor(_store) {
4030
- this._store = _store;
4143
+ //#region ../../core/src/features/props/PropsModel.ts
4144
+ var PropsModel = class {
4145
+ constructor() {
4031
4146
  this.value = signal(void 0, { readonly: true });
4032
4147
  this.defaultValue = signal(void 0, { readonly: true });
4033
4148
  this.onChange = signal(void 0, { readonly: true });
@@ -4057,51 +4172,34 @@ var PropsFeature = class {
4057
4172
  }
4058
4173
  };
4059
4174
  //#endregion
4060
- //#region ../../core/src/features/value/ValueFeature.ts
4061
- var ValueFeature = class {
4062
- #scope;
4063
- constructor(_store) {
4064
- this._store = _store;
4065
- this.last = signal(void 0);
4066
- this.next = signal(void 0);
4067
- this.current = computed(() => this.last() ?? this._store.props.value() ?? "");
4068
- this.change = event();
4069
- }
4070
- enable() {
4071
- if (this.#scope) return;
4072
- this.#scope = effectScope(() => {
4073
- watch(this.change, () => {
4074
- const onChange = this._store.props.onChange();
4075
- const { focus } = this._store.nodes;
4076
- if (!focus.target || !focus.target.isContentEditable) {
4077
- const serialized = toString(this._store.parsing.tokens());
4078
- onChange?.(serialized);
4079
- this.last(serialized);
4080
- trigger(this._store.parsing.tokens);
4081
- return;
4082
- }
4083
- const tokens = this._store.parsing.tokens();
4084
- if (focus.index >= tokens.length) return;
4085
- const token = tokens[focus.index];
4086
- if (token.type === "text") token.content = focus.content;
4087
- else token.value = focus.content;
4088
- onChange?.(toString(tokens));
4089
- this._store.parsing.reparse();
4090
- });
4091
- watch(this.next, (newValue) => {
4092
- if (newValue === void 0) return;
4093
- const newTokens = parseWithParser(this._store, newValue);
4094
- batch(() => {
4095
- this._store.parsing.tokens(newTokens);
4096
- this.last(newValue);
4097
- });
4098
- this._store.props.onChange()?.(newValue);
4099
- });
4175
+ //#region ../../core/src/features/value/ValueModel.ts
4176
+ var ValueModel = class {
4177
+ constructor(props) {
4178
+ this.props = props;
4179
+ this.isControlledMode = computed(() => this.props.value() !== void 0);
4180
+ this.current = model({
4181
+ default: () => this.props.defaultValue() ?? "",
4182
+ get: (value) => this.isControlledMode() ? this.props.value() ?? "" : value,
4183
+ set: (next, previous) => {
4184
+ if (next === void 0) return previous;
4185
+ if (this.props.readOnly()) return previous;
4186
+ this.props.onChange()?.(next);
4187
+ return this.isControlledMode() ? previous : next;
4188
+ }
4100
4189
  });
4101
4190
  }
4102
- disable() {
4103
- this.#scope?.();
4104
- this.#scope = void 0;
4191
+ /**
4192
+ * Attempts to replace `range` with `replacement`. Returns `true` when the
4193
+ * edit was accepted (range valid and not read-only), `false` otherwise.
4194
+ * Callers use the return value to gate downstream side effects such as
4195
+ * caret placement.
4196
+ */
4197
+ replace(range, replacement) {
4198
+ if (this.props.readOnly()) return false;
4199
+ const next = replaceInString(this.current(), range, replacement);
4200
+ if (next === void 0) return false;
4201
+ this.current(next);
4202
+ return true;
4105
4203
  }
4106
4204
  };
4107
4205
  //#endregion
@@ -4281,38 +4379,20 @@ var Store = class {
4281
4379
  constructor() {
4282
4380
  this.key = new KeyGenerator();
4283
4381
  this.blocks = new BlockRegistry();
4284
- this.nodes = {
4285
- focus: new NodeProxy(void 0, this),
4286
- input: new NodeProxy(void 0, this)
4287
- };
4288
- this.props = new PropsFeature(this);
4289
- this.handler = new MarkputHandler(this);
4290
- this.lifecycle = new LifecycleFeature(this);
4291
- this.value = new ValueFeature(this);
4292
- this.mark = new MarkFeature(this);
4293
- this.overlay = new OverlayFeature(this);
4294
- this.slots = new SlotsFeature(this);
4295
- this.caret = new CaretFeature(this);
4296
- this.keyboard = new KeyboardFeature(this);
4297
- this.dom = new DomFeature(this);
4298
- this.drag = new DragFeature(this);
4299
- this.clipboard = new ClipboardFeature(this);
4300
- this.parsing = new ParsingFeature(this);
4301
- const features = [
4302
- this.lifecycle,
4303
- this.value,
4304
- this.mark,
4305
- this.overlay,
4306
- this.slots,
4307
- this.caret,
4308
- this.keyboard,
4309
- this.dom,
4310
- this.drag,
4311
- this.clipboard,
4312
- this.parsing
4313
- ];
4314
- watch(this.lifecycle.mounted, () => features.forEach((f) => f.enable()));
4315
- watch(this.lifecycle.unmounted, () => features.forEach((f) => f.disable()));
4382
+ this.lifecycle = new Lifecycle();
4383
+ this.props = new PropsModel();
4384
+ this.value = new ValueModel(this.props);
4385
+ this.mark = new MarkFeature(this.props);
4386
+ this.slots = new SlotsFeature(this.props);
4387
+ this.parsing = new ParseController(this.lifecycle, this.value, this.mark, this.props, this.slots);
4388
+ this.dom = new DomController(this.lifecycle, this.props, this.parsing, this.value);
4389
+ this.caret = new CaretModel(this.lifecycle, this.dom, this.value);
4390
+ this.edit = new EditController(this.value, this.caret);
4391
+ this.overlay = new OverlayController(this.lifecycle, this.props, this.value, this.dom, this.caret, this.edit, this.parsing);
4392
+ this.keyboard = new KeyboardController(this.lifecycle, this.dom, this.value, this.caret, this.edit, this.slots, this.parsing, this.props);
4393
+ this.drag = new DragController(this.props, this.value, this.parsing, this.caret);
4394
+ this.clipboard = new ClipboardController(this.lifecycle, this.edit, this.dom, this.parsing);
4395
+ this.handler = new MarkputHandler(this.dom, this.overlay, this.parsing);
4316
4396
  }
4317
4397
  };
4318
4398
  //#endregion
@@ -4379,14 +4459,24 @@ const Popup = ({ ref, style, children }) => {
4379
4459
  //#endregion
4380
4460
  //#region src/components/BlockMenu.tsx
4381
4461
  const BlockMenu = memo(({ token }) => {
4382
- const { blockStore, menuOpen, menuPosition } = useMarkput((s) => ({
4383
- blockStore: s.blocks.get(token),
4384
- menuOpen: s.blocks.get(token).state.menuOpen,
4385
- menuPosition: s.blocks.get(token).state.menuPosition
4386
- }));
4462
+ const { blockStore, menuOpen, menuPosition, dom, index } = useMarkput((s) => {
4463
+ const blockStore = s.blocks.get(token);
4464
+ return {
4465
+ blockStore,
4466
+ menuOpen: blockStore.state.menuOpen,
4467
+ menuPosition: blockStore.state.menuPosition,
4468
+ dom: s.dom,
4469
+ index: s.parsing.index
4470
+ };
4471
+ });
4472
+ const path = index.pathFor(token);
4473
+ const controlRef = useMemo(() => path ? dom.controlFor(path) : void 0, [dom, path]);
4387
4474
  if (!menuOpen) return null;
4388
4475
  return /* @__PURE__ */ jsx(Popup, {
4389
- ref: (el) => blockStore.attachMenu(el),
4476
+ ref: (el) => {
4477
+ blockStore.attachMenu(el);
4478
+ controlRef?.(el);
4479
+ },
4390
4480
  style: {
4391
4481
  top: menuPosition.top,
4392
4482
  left: menuPosition.left
@@ -4412,20 +4502,30 @@ BlockMenu.displayName = "BlockMenu";
4412
4502
  //#region src/components/DragHandle.tsx
4413
4503
  const iconGrip = `${styles$1.Icon} ${styles$1.IconGrip}`;
4414
4504
  const DragHandle = memo(({ token, blockIndex }) => {
4415
- const { blockStore, action, readOnly, draggable, isDragging, isHovered } = useMarkput((s) => ({
4416
- blockStore: s.blocks.get(token),
4417
- action: s.drag.action,
4418
- readOnly: s.props.readOnly,
4419
- draggable: s.props.draggable,
4420
- isDragging: s.blocks.get(token).state.isDragging,
4421
- isHovered: s.blocks.get(token).state.isHovered
4422
- }));
4505
+ const { blockStore, action, readOnly, draggable, isDragging, isHovered, dom, index } = useMarkput((s) => {
4506
+ const blockStore = s.blocks.get(token);
4507
+ return {
4508
+ blockStore,
4509
+ action: s.drag.action,
4510
+ readOnly: s.props.readOnly,
4511
+ draggable: s.props.draggable,
4512
+ isDragging: blockStore.state.isDragging,
4513
+ isHovered: blockStore.state.isHovered,
4514
+ dom: s.dom,
4515
+ index: s.parsing.index
4516
+ };
4517
+ });
4423
4518
  const alwaysShowHandle = useMemo(() => getAlwaysShowHandle(draggable), [draggable]);
4519
+ const path = index.pathFor(token);
4520
+ const controlRef = useMemo(() => path ? dom.controlFor(path) : void 0, [dom, path]);
4424
4521
  if (readOnly) return null;
4425
4522
  return /* @__PURE__ */ jsx("div", {
4523
+ ref: controlRef,
4426
4524
  className: cx(styles$1.SidePanel, alwaysShowHandle ? styles$1.SidePanelAlways : isHovered && !isDragging && styles$1.SidePanelVisible),
4427
4525
  children: /* @__PURE__ */ jsx("button", {
4428
- ref: (el) => blockStore.attachGrip(el, blockIndex, { action }),
4526
+ ref: (el) => {
4527
+ blockStore.attachGrip(el, blockIndex, { action });
4528
+ },
4429
4529
  type: "button",
4430
4530
  draggable: true,
4431
4531
  className: cx(styles$1.GripButton, isDragging && styles$1.GripButtonDragging),
@@ -4438,8 +4538,16 @@ DragHandle.displayName = "DragHandle";
4438
4538
  //#endregion
4439
4539
  //#region src/components/DropIndicator.tsx
4440
4540
  const DropIndicator = memo(({ token, position }) => {
4441
- if (useMarkput((s) => s.blocks.get(token).state.dropPosition) !== position) return null;
4541
+ const dropPosition = useMarkput((s) => s.blocks.get(token).state.dropPosition);
4542
+ const { dom, index } = useMarkput((s) => ({
4543
+ dom: s.dom,
4544
+ index: s.parsing.index
4545
+ }));
4546
+ const path = index.pathFor(token);
4547
+ const controlRef = useMemo(() => path ? dom.controlFor(path) : void 0, [dom, path]);
4548
+ if (dropPosition !== position) return null;
4442
4549
  return /* @__PURE__ */ jsx("div", {
4550
+ ref: controlRef,
4443
4551
  className: styles$1.DropIndicator,
4444
4552
  style: position === "before" ? { top: -1 } : { bottom: -1 }
4445
4553
  });
@@ -4449,25 +4557,50 @@ DropIndicator.displayName = "DropIndicator";
4449
4557
  //#region src/lib/providers/TokenContext.ts
4450
4558
  const TokenContext = createContext(void 0);
4451
4559
  TokenContext.displayName = "TokenProvider";
4452
- function useToken() {
4560
+ function useTokenContext() {
4453
4561
  const value = useContext(TokenContext);
4454
4562
  if (value === void 0) throw new Error("Token not found. Make sure to wrap component in TokenContext.Provider.");
4455
4563
  return value;
4456
4564
  }
4457
4565
  //#endregion
4566
+ //#region src/components/TokenChildren.tsx
4567
+ const sequenceHostStyle = { display: "contents" };
4568
+ const TokenChildren = memo(({ ownerPath, children }) => {
4569
+ const { dom } = useMarkput((s) => ({ dom: s.dom }));
4570
+ return /* @__PURE__ */ jsx("span", {
4571
+ ref: useMemo(() => dom.childrenFor(ownerPath), [dom, ownerPath]),
4572
+ style: sequenceHostStyle,
4573
+ children
4574
+ });
4575
+ });
4576
+ TokenChildren.displayName = "TokenChildren";
4577
+ //#endregion
4458
4578
  //#region src/components/Token.tsx
4459
- const Token = memo(({ mark }) => {
4460
- const { resolveMarkSlot, key } = useMarkput((s) => ({
4579
+ const Token = memo(({ token }) => {
4580
+ const { resolveMarkSlot, key, index, store } = useMarkput((s) => ({
4461
4581
  resolveMarkSlot: s.mark.slot,
4462
- key: s.key
4582
+ key: s.key,
4583
+ index: s.parsing.index,
4584
+ store: s
4463
4585
  }));
4464
- const [Component, props] = resolveMarkSlot(mark);
4586
+ const path = index.pathFor(token);
4587
+ const address = path ? index.addressFor(path) : void 0;
4588
+ if (!path || !address) return null;
4589
+ const [Component, props] = resolveMarkSlot(token);
4590
+ const children = token.type === "mark" && token.children.length > 0 ? /* @__PURE__ */ jsx(TokenChildren, {
4591
+ ownerPath: path,
4592
+ children: token.children.map((child) => /* @__PURE__ */ jsx(Token, { token: child }, key.get(child)))
4593
+ }) : void 0;
4465
4594
  return /* @__PURE__ */ jsx(TokenContext, {
4466
- value: mark,
4467
- children: /* @__PURE__ */ jsx(Component, {
4468
- children: mark.type === "mark" && mark.children.length > 0 ? mark.children.map((child) => /* @__PURE__ */ jsx(Token, { mark: child }, key.get(child))) : void 0,
4469
- ...props
4470
- })
4595
+ value: {
4596
+ store,
4597
+ token,
4598
+ address
4599
+ },
4600
+ children: children ? /* @__PURE__ */ jsx(Component, {
4601
+ ...props,
4602
+ children
4603
+ }) : /* @__PURE__ */ jsx(Component, { ...props })
4471
4604
  });
4472
4605
  });
4473
4606
  Token.displayName = "Token";
@@ -4483,8 +4616,11 @@ const Block = memo(({ token }) => {
4483
4616
  tokens: s.parsing.tokens
4484
4617
  }));
4485
4618
  const blockIndex = tokens.indexOf(token);
4619
+ const setBlockRef = (el) => {
4620
+ blockStore.attachContainer(el, blockIndex, { action });
4621
+ };
4486
4622
  return /* @__PURE__ */ jsxs(Component, {
4487
- ref: (el) => blockStore.attachContainer(el, blockIndex, { action }),
4623
+ ref: setBlockRef,
4488
4624
  "data-testid": "block",
4489
4625
  ...slotProps,
4490
4626
  className: cx(styles$1.Block, slotProps?.className),
@@ -4501,7 +4637,7 @@ const Block = memo(({ token }) => {
4501
4637
  token,
4502
4638
  blockIndex
4503
4639
  }),
4504
- /* @__PURE__ */ jsx(Token, { mark: token }),
4640
+ /* @__PURE__ */ jsx(Token, { token }),
4505
4641
  /* @__PURE__ */ jsx(DropIndicator, {
4506
4642
  token,
4507
4643
  position: "after"
@@ -4514,75 +4650,61 @@ Block.displayName = "Block";
4514
4650
  //#endregion
4515
4651
  //#region src/components/Container.tsx
4516
4652
  const Container = memo(() => {
4517
- const storeCtx = useContext(StoreContext);
4518
- if (!storeCtx) throw new Error("Store not found");
4519
- const store = storeCtx;
4520
- const { isBlock, tokens, key, lifecycleEmit, Component, props } = useMarkput((s) => ({
4653
+ const { dom, lifecycle, isBlock, tokens, key, Component, props } = useMarkput((s) => ({
4654
+ dom: s.dom,
4655
+ lifecycle: s.lifecycle,
4521
4656
  isBlock: s.slots.isBlock,
4522
4657
  tokens: s.parsing.tokens,
4523
4658
  key: s.key,
4524
- lifecycleEmit: s.lifecycle,
4525
4659
  Component: s.slots.containerComponent,
4526
4660
  props: s.slots.containerProps
4527
4661
  }));
4528
4662
  useLayoutEffect(() => {
4529
- lifecycleEmit.rendered();
4530
- }, [tokens, lifecycleEmit]);
4663
+ lifecycle.rendered();
4664
+ });
4531
4665
  return /* @__PURE__ */ jsx(Component, {
4532
- ref: store.slots.container,
4666
+ ref: dom.container,
4533
4667
  ...props,
4534
- children: isBlock ? tokens.map((t) => /* @__PURE__ */ jsx(Block, { token: t }, key.get(t))) : tokens.map((t) => /* @__PURE__ */ jsx(Token, { mark: t }, key.get(t)))
4668
+ children: isBlock ? tokens.map((t) => /* @__PURE__ */ jsx(Block, { token: t }, key.get(t))) : tokens.map((t) => /* @__PURE__ */ jsx(Token, { token: t }, key.get(t)))
4535
4669
  });
4536
4670
  });
4537
4671
  Container.displayName = "Container";
4538
4672
  //#endregion
4539
4673
  //#region src/lib/hooks/useOverlay.tsx
4540
4674
  function useOverlay() {
4541
- const match = useMarkput((s) => s.overlay.match);
4542
- const storeRef = useRef(null);
4543
- if (storeRef.current === null) {
4544
- const ctx = useContext(StoreContext);
4545
- if (!ctx) throw new Error("Store not found");
4546
- storeRef.current = ctx;
4547
- }
4548
- const store = storeRef.current;
4549
- const style = useMemo(() => {
4550
- if (!match) return {
4551
- left: 0,
4552
- top: 0
4553
- };
4554
- return Caret.getAbsolutePosition();
4555
- }, [match]);
4556
- const close = useCallback(() => store.overlay.close(), [store]);
4675
+ const { match, overlay } = useMarkput((s) => ({
4676
+ match: s.overlay.match,
4677
+ overlay: s.overlay
4678
+ }));
4679
+ const style = useMarkput((s) => s.overlay.position());
4680
+ const close = useCallback(() => overlay.close(), [overlay]);
4557
4681
  return {
4558
4682
  match,
4559
4683
  style,
4560
4684
  select: useCallback((value) => {
4561
4685
  if (!match) return;
4562
4686
  const mark = createMarkFromOverlay(match, value.value, value.meta);
4563
- store.overlay.select({
4687
+ overlay.select({
4564
4688
  mark,
4565
4689
  match
4566
4690
  });
4567
- store.overlay.close();
4568
- }, [match, store]),
4691
+ overlay.close();
4692
+ }, [match, overlay]),
4569
4693
  close,
4570
4694
  ref: useMemo(() => ({
4571
4695
  get current() {
4572
- return store.overlay.element();
4696
+ return overlay.element();
4573
4697
  },
4574
4698
  set current(v) {
4575
- store.overlay.element(v);
4699
+ overlay.element(v);
4576
4700
  }
4577
- }), [store])
4701
+ }), [overlay])
4578
4702
  };
4579
4703
  }
4580
4704
  //#endregion
4581
4705
  //#region src/components/Suggestions/Suggestions.tsx
4582
4706
  const Suggestions = () => {
4583
- const storeCtx = useContext(StoreContext);
4584
- if (!storeCtx) throw new Error("Store not found");
4585
- const store = storeCtx;
4707
+ const container = useMarkput((s) => s.dom.container);
4586
4708
  const { match, select, style, ref } = useOverlay();
4587
4709
  const [active, setActive] = useState(NaN);
4588
4710
  const data = match?.option.overlay?.data ?? [];
@@ -4593,7 +4715,6 @@ const Suggestions = () => {
4593
4715
  const filteredRef = useRef(filtered);
4594
4716
  filteredRef.current = filtered;
4595
4717
  useEffect(() => {
4596
- const container = store.slots.container();
4597
4718
  if (!container) return;
4598
4719
  const handler = (event) => {
4599
4720
  const result = navigateSuggestions(event.key, activeRef.current, length);
@@ -4617,7 +4738,11 @@ const Suggestions = () => {
4617
4738
  };
4618
4739
  container.addEventListener("keydown", handler);
4619
4740
  return () => container.removeEventListener("keydown", handler);
4620
- }, [length, select]);
4741
+ }, [
4742
+ container,
4743
+ length,
4744
+ select
4745
+ ]);
4621
4746
  if (!filtered.length) return null;
4622
4747
  return /* @__PURE__ */ jsx(Popup, {
4623
4748
  ref,
@@ -4649,8 +4774,14 @@ OverlayRenderer.displayName = "OverlayRenderer";
4649
4774
  //#endregion
4650
4775
  //#region src/components/MarkedInput.tsx
4651
4776
  function MarkedInput(props) {
4652
- const [store] = useState(() => new Store());
4653
- store.props.set(props);
4777
+ const [store] = useState(() => {
4778
+ const nextStore = new Store();
4779
+ nextStore.props.set(props);
4780
+ return nextStore;
4781
+ });
4782
+ useLayoutEffect(() => {
4783
+ store.props.set(props);
4784
+ });
4654
4785
  useLayoutEffect(() => {
4655
4786
  store.lifecycle.mounted();
4656
4787
  return () => store.lifecycle.unmounted();
@@ -4663,31 +4794,34 @@ function MarkedInput(props) {
4663
4794
  }
4664
4795
  //#endregion
4665
4796
  //#region src/lib/hooks/useMark.tsx
4666
- const useMark = (options = {}) => {
4667
- const { store, readOnly } = useMarkput((s) => ({
4668
- store: s,
4669
- readOnly: s.props.readOnly
4670
- }));
4671
- const token = useToken();
4797
+ const useMark = () => {
4798
+ const { store, token } = useTokenContext();
4799
+ const readOnly = useMarkput((s) => s.props.readOnly);
4672
4800
  if (token.type !== "mark") throw new Error("useMark must be called within a mark token context");
4673
- const ref = useRef(null);
4674
- const mark = useMemo(() => new MarkHandler({
4675
- ref,
4801
+ return useMemo(() => MarkController.fromToken(store, token), [
4676
4802
  store,
4677
- token
4678
- }), [store, token]);
4679
- useUncontrolledInit(ref, options, token);
4680
- useEffect(() => {
4681
- mark.readOnly = readOnly;
4682
- }, [mark, readOnly]);
4683
- return mark;
4803
+ token,
4804
+ readOnly
4805
+ ]);
4806
+ };
4807
+ //#endregion
4808
+ //#region src/lib/hooks/useMarkInfo.tsx
4809
+ const useMarkInfo = () => {
4810
+ const { store, token } = useTokenContext();
4811
+ if (token.type !== "mark") throw new Error("useMarkInfo must be called within a mark token context");
4812
+ const index = store.parsing.index();
4813
+ const path = index.pathFor(token);
4814
+ if (!path) throw new Error("Mark token is not indexed");
4815
+ const address = index.addressFor(path);
4816
+ if (!address) throw new Error("Mark token path is stale");
4817
+ return {
4818
+ address,
4819
+ depth: findToken(store.parsing.tokens(), token)?.depth ?? 0,
4820
+ hasNestedMarks: token.children.some((child) => child.type === "mark"),
4821
+ key: index.key(path)
4822
+ };
4684
4823
  };
4685
- function useUncontrolledInit(ref, options, token) {
4686
- useEffect(() => {
4687
- if (!options.controlled && ref.current) ref.current.textContent = token.content;
4688
- }, []);
4689
- }
4690
4824
  //#endregion
4691
- export { MarkHandler, MarkedInput, MarkputHandler, annotate, denote, useMark, useMarkput, useOverlay };
4825
+ export { MarkController, MarkedInput, MarkputHandler, annotate, denote, useMark, useMarkInfo, useMarkput, useOverlay };
4692
4826
 
4693
4827
  //# sourceMappingURL=index.js.map