@markput/react 0.12.1 → 0.14.0

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
@@ -58,13 +58,15 @@ var MarkputHandler = class {
58
58
  this.store = store;
59
59
  }
60
60
  get container() {
61
- return this.store.slots.container();
61
+ return this.store.dom.container();
62
62
  }
63
63
  get overlay() {
64
64
  return this.store.overlay.element();
65
65
  }
66
66
  focus() {
67
- this.store.nodes.focus.head?.focus();
67
+ const firstAddress = this.store.parsing.index().addressFor([0]);
68
+ if (firstAddress && this.store.dom.focusAddress(firstAddress).ok) return;
69
+ this.container?.focus();
68
70
  }
69
71
  };
70
72
  //#endregion
@@ -79,192 +81,6 @@ var KeyGenerator = class {
79
81
  }
80
82
  };
81
83
  //#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
84
  //#region ../../core/src/shared/signals/alien-signals/system.ts
269
85
  let ReactiveFlags = /* @__PURE__ */ function(ReactiveFlags) {
270
86
  ReactiveFlags[ReactiveFlags["None"] = 0] = "None";
@@ -764,30 +580,6 @@ function batch(fn, opts) {
764
580
  mutableScope = prevMutable;
765
581
  }
766
582
  }
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
583
  function untracked(fn) {
792
584
  const prev = setActiveSub(void 0);
793
585
  try {
@@ -803,358 +595,33 @@ function listen(target, event, handler, options) {
803
595
  });
804
596
  }
805
597
  //#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
- }
865
- //#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;
910
- };
911
- }
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
- }
929
- };
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;
941
- }
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
- };
598
+ //#region ../../core/src/features/parsing/parser/constants.ts
599
+ /**
600
+ * Constants for ParserV2 - modern markup parser with nesting support
601
+ *
602
+ * This module contains the placeholder constants used by ParserV2.
603
+ * Unlike the legacy Parser, ParserV2 supports:
604
+ * - `__value__` - main content (replaces `__label__`)
605
+ * - `__meta__` - metadata (replaces `__value__`)
606
+ * - `__slot__` - nested content (new feature)
607
+ *
608
+ * For legacy Parser compatibility, see ../Parser/constants.ts
609
+ * For Markup types, see ./types.ts
610
+ */
611
+ const PLACEHOLDER = {
612
+ Value: "__value__",
613
+ Meta: "__meta__",
614
+ Slot: "__slot__"
615
+ };
616
+ /**
617
+ * Gap types used in markup descriptors
618
+ * Represents the content type in gaps between segments
619
+ */
620
+ const GAP_TYPE = {
621
+ Value: "value",
622
+ Meta: "meta",
623
+ Slot: "slot"
624
+ };
1158
625
  //#endregion
1159
626
  //#region ../../core/src/features/parsing/parser/core/MarkupDescriptor.ts
1160
627
  /**
@@ -1640,6 +1107,11 @@ var PatternMatcher = class {
1640
1107
  }
1641
1108
  };
1642
1109
  //#endregion
1110
+ //#region ../../core/src/shared/escape.ts
1111
+ const escape = (str) => {
1112
+ return str.replace(/[.*+?^${}()|[\]\\\\]/g, "\\$&");
1113
+ };
1114
+ //#endregion
1643
1115
  //#region ../../core/src/features/parsing/parser/core/SegmentMatcher.ts
1644
1116
  /**
1645
1117
  * Computes regex pattern for dynamic segment using pre-computed exclusions
@@ -1986,11 +1458,7 @@ function processTokensWithCallback(tokens, callback) {
1986
1458
  * ```
1987
1459
  */
1988
1460
  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;
1461
+ return markup.replaceAll(PLACEHOLDER.Value, params.value ?? "").replaceAll(PLACEHOLDER.Meta, params.meta ?? "").replaceAll(PLACEHOLDER.Slot, params.slot ?? "");
1994
1462
  }
1995
1463
  //#endregion
1996
1464
  //#region ../../core/src/features/parsing/parser/utils/toString.ts
@@ -2259,100 +1727,7 @@ function findToken(tokens, target, depth = 0, parent) {
2259
1727
  }
2260
1728
  }
2261
1729
  //#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
- };
2279
- }
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);
2291
- }
2292
- //#endregion
2293
1730
  //#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 ?? "");
2338
- }
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);
2347
- }
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
- });
2355
- }
2356
1731
  function parseWithParser(store, value) {
2357
1732
  const parser = store.parsing.parser();
2358
1733
  if (!parser) return [{
@@ -2366,12 +1741,86 @@ function parseWithParser(store, value) {
2366
1741
  return parser.parse(value);
2367
1742
  }
2368
1743
  //#endregion
1744
+ //#region ../../core/src/features/parsing/tokenIndex.ts
1745
+ function pathEquals(a, b) {
1746
+ return a.length === b.length && a.every((part, index) => part === b[index]);
1747
+ }
1748
+ function pathKey(path) {
1749
+ return path.join(".");
1750
+ }
1751
+ function resolvePath(tokens, path) {
1752
+ if (path.length === 0) return void 0;
1753
+ let current = tokens;
1754
+ let token;
1755
+ for (const index of path) {
1756
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) return void 0;
1757
+ token = current[index];
1758
+ current = token.type === "mark" ? token.children : [];
1759
+ }
1760
+ return token;
1761
+ }
1762
+ function snapshotTokenShape(token) {
1763
+ if (token.type === "text") return { kind: "text" };
1764
+ return {
1765
+ kind: "mark",
1766
+ descriptor: token.descriptor,
1767
+ descriptorIndex: token.descriptor.index
1768
+ };
1769
+ }
1770
+ function shapeMatches(token, expected) {
1771
+ if (expected.kind === "text") return token.type === "text";
1772
+ return token.type === "mark" && token.descriptor === expected.descriptor && token.descriptor.index === expected.descriptorIndex;
1773
+ }
1774
+ function createTokenIndex(tokens, generation) {
1775
+ const paths = /* @__PURE__ */ new WeakMap();
1776
+ const visit = (items, parent) => {
1777
+ items.forEach((token, index) => {
1778
+ const path = [...parent, index];
1779
+ paths.set(token, path);
1780
+ if (token.type === "mark") visit(token.children, path);
1781
+ });
1782
+ };
1783
+ visit(tokens, []);
1784
+ return {
1785
+ generation,
1786
+ pathFor: (token) => paths.get(token),
1787
+ addressFor: (path) => resolvePath(tokens, path) ? {
1788
+ path: [...path],
1789
+ parseGeneration: generation
1790
+ } : void 0,
1791
+ resolve: (path) => resolvePath(tokens, path),
1792
+ resolveAddress(address, expected) {
1793
+ if (address.parseGeneration !== generation) return {
1794
+ ok: false,
1795
+ reason: "stale"
1796
+ };
1797
+ const token = resolvePath(tokens, address.path);
1798
+ if (!token) return {
1799
+ ok: false,
1800
+ reason: "stale"
1801
+ };
1802
+ if (expected && !shapeMatches(token, expected)) return {
1803
+ ok: false,
1804
+ reason: "stale"
1805
+ };
1806
+ return {
1807
+ ok: true,
1808
+ value: token
1809
+ };
1810
+ },
1811
+ key: pathKey,
1812
+ equals: pathEquals
1813
+ };
1814
+ }
1815
+ //#endregion
2369
1816
  //#region ../../core/src/features/parsing/ParseFeature.ts
2370
1817
  var ParsingFeature = class {
1818
+ #generation = signal(0);
2371
1819
  #scope;
2372
1820
  constructor(_store) {
2373
1821
  this._store = _store;
2374
1822
  this.tokens = signal([]);
1823
+ this.index = computed(() => createTokenIndex(this.tokens(), this.#generation()));
2375
1824
  this.parser = computed(() => {
2376
1825
  if (!this._store.mark.enabled()) return;
2377
1826
  const markups = this._store.props.options().map((opt) => opt.markup);
@@ -2380,6 +1829,15 @@ var ParsingFeature = class {
2380
1829
  });
2381
1830
  this.reparse = event();
2382
1831
  }
1832
+ parseValue(value) {
1833
+ return parseWithParser(this._store, value);
1834
+ }
1835
+ acceptTokens(tokens) {
1836
+ batch(() => {
1837
+ this.tokens(tokens);
1838
+ this.#generation(this.#generation() + 1);
1839
+ }, { mutable: true });
1840
+ }
2383
1841
  enable() {
2384
1842
  if (this.#scope) return;
2385
1843
  this.sync();
@@ -2392,29 +1850,450 @@ var ParsingFeature = class {
2392
1850
  this.#scope?.();
2393
1851
  this.#scope = void 0;
2394
1852
  }
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);
1853
+ sync(value = this._store.value.current()) {
1854
+ this.acceptTokens(this.parseValue(value));
2399
1855
  }
2400
1856
  #subscribeParse() {
2401
1857
  watch(this.reparse, () => {
2402
1858
  if (this._store.caret.recovery()) {
2403
1859
  const text = toString(this.tokens());
2404
- this.tokens(parseWithParser(this._store, text));
2405
- this._store.value.last(text);
1860
+ this.acceptTokens(this.parseValue(text));
2406
1861
  return;
2407
1862
  }
2408
- this.tokens(this._store.nodes.focus.target ? getTokensByUI(this._store) : computeTokensFromValue(this._store));
1863
+ this.sync();
2409
1864
  });
2410
1865
  }
2411
1866
  #subscribeReactiveParse() {
2412
- watch(computed(() => [this._store.props.value(), this.parser()]), () => {
2413
- if (!this._store.caret.recovery()) this.reparse();
1867
+ watch(computed(() => this.parser()), () => {
1868
+ if (!this._store.caret.recovery()) this.sync(this._store.value.current());
2414
1869
  });
2415
1870
  }
2416
1871
  };
2417
1872
  //#endregion
1873
+ //#region ../../core/src/shared/checkers/domGuards.ts
1874
+ /** Type guard: checks if a value is an HTMLElement. */
1875
+ function isHtmlElement(el) {
1876
+ return typeof HTMLElement !== "undefined" && el instanceof HTMLElement;
1877
+ }
1878
+ /** Get all children of an element as HTMLElement[], filtering out non-HTML elements. */
1879
+ function htmlChildren(parent) {
1880
+ if (!parent) return [];
1881
+ return Array.from(parent.children).filter((child) => child instanceof HTMLElement);
1882
+ }
1883
+ /** Get the first element child as HTMLElement, or null. */
1884
+ function firstHtmlChild(parent) {
1885
+ const child = parent?.firstElementChild;
1886
+ return child instanceof HTMLElement ? child : null;
1887
+ }
1888
+ /** Safely narrow an event's target to Node. */
1889
+ function nodeTarget(event) {
1890
+ const { target } = event;
1891
+ return target instanceof Node ? target : null;
1892
+ }
1893
+ /** Get the next node from a TreeWalker as Text, or null. */
1894
+ function nextText(walker) {
1895
+ const node = walker.nextNode();
1896
+ return node?.nodeType === 3 ? node : null;
1897
+ }
1898
+ //#endregion
1899
+ //#region ../../core/src/features/caret/Caret.ts
1900
+ var Caret = class {
1901
+ static get isSelectedPosition() {
1902
+ const selection = window.getSelection();
1903
+ if (!selection) return;
1904
+ return selection.isCollapsed;
1905
+ }
1906
+ static getCurrentPosition() {
1907
+ return window.getSelection()?.anchorOffset ?? 0;
1908
+ }
1909
+ static getFocusedSpan() {
1910
+ return window.getSelection()?.anchorNode?.textContent ?? "";
1911
+ }
1912
+ static getSelectedNode() {
1913
+ const node = window.getSelection()?.anchorNode;
1914
+ if (node && document.contains(node)) return node;
1915
+ throw new Error("Anchor node of selection is not exists!");
1916
+ }
1917
+ static getAbsolutePosition() {
1918
+ const rect = window.getSelection()?.getRangeAt(0).getBoundingClientRect();
1919
+ if (rect) return {
1920
+ left: rect.left,
1921
+ top: rect.top + rect.height + 1
1922
+ };
1923
+ return {
1924
+ left: 0,
1925
+ top: 0
1926
+ };
1927
+ }
1928
+ /** Returns the raw DOMRect of the current caret position, or null if unavailable. */
1929
+ static getCaretRect() {
1930
+ try {
1931
+ return (window.getSelection()?.getRangeAt(0))?.getBoundingClientRect() ?? null;
1932
+ } catch {
1933
+ return null;
1934
+ }
1935
+ }
1936
+ /**
1937
+ * Returns true if the caret is on the first visual line of the element.
1938
+ */
1939
+ static isCaretOnFirstLine(element) {
1940
+ const caretRect = this.getCaretRect();
1941
+ if (!caretRect || caretRect.height === 0) return true;
1942
+ const elRect = element.getBoundingClientRect();
1943
+ return caretRect.top < elRect.top + caretRect.height + 2;
1944
+ }
1945
+ /**
1946
+ * Returns true if the caret is on the last visual line of the element.
1947
+ */
1948
+ static isCaretOnLastLine(element) {
1949
+ const caretRect = this.getCaretRect();
1950
+ if (!caretRect || caretRect.height === 0) return true;
1951
+ const elRect = element.getBoundingClientRect();
1952
+ return caretRect.bottom > elRect.bottom - caretRect.height - 2;
1953
+ }
1954
+ /**
1955
+ * Positions the caret in `element` at the character closest to the given x coordinate.
1956
+ * `y` defaults to the vertical center of the element.
1957
+ */
1958
+ static setAtX(element, x, y) {
1959
+ const elRect = element.getBoundingClientRect();
1960
+ const targetY = y ?? elRect.top + elRect.height / 2;
1961
+ const caretDoc = document;
1962
+ const caretPos = caretDoc.caretRangeFromPoint?.(x, targetY) ?? caretDoc.caretPositionFromPoint?.(x, targetY);
1963
+ if (!caretPos) return;
1964
+ const sel = window.getSelection();
1965
+ if (!sel) return;
1966
+ let domRange;
1967
+ if (caretPos instanceof Range) domRange = caretPos;
1968
+ else if ("offsetNode" in caretPos) {
1969
+ domRange = document.createRange();
1970
+ domRange.setStart(caretPos.offsetNode, caretPos.offset);
1971
+ domRange.collapse(true);
1972
+ } else return;
1973
+ if (!element.contains(domRange.startContainer)) {
1974
+ this.setIndex(element, Infinity);
1975
+ return;
1976
+ }
1977
+ sel.removeAllRanges();
1978
+ sel.addRange(domRange);
1979
+ }
1980
+ static trySetIndex(element, offset) {
1981
+ try {
1982
+ this.setIndex(element, offset);
1983
+ } catch (e) {
1984
+ console.error(e);
1985
+ }
1986
+ }
1987
+ /**
1988
+ * Sets the caret at character `offset` within `element` by walking text nodes.
1989
+ * Use Infinity to position at the very end of all text.
1990
+ */
1991
+ static setIndex(element, offset) {
1992
+ const selection = window.getSelection();
1993
+ if (!selection) return;
1994
+ const walker = document.createTreeWalker(element, 4);
1995
+ let node = nextText(walker);
1996
+ if (!node) return;
1997
+ let remaining = isFinite(offset) ? Math.max(0, offset) : Infinity;
1998
+ for (;;) {
1999
+ const next = nextText(walker);
2000
+ if (!next || remaining <= node.length) {
2001
+ const charOffset = isFinite(remaining) ? Math.min(remaining, node.length) : node.length;
2002
+ const range = document.createRange();
2003
+ range.setStart(node, charOffset);
2004
+ range.collapse(true);
2005
+ selection.removeAllRanges();
2006
+ selection.addRange(range);
2007
+ return;
2008
+ }
2009
+ remaining -= node.length;
2010
+ node = next;
2011
+ }
2012
+ }
2013
+ static getCaretIndex(element) {
2014
+ let position = 0;
2015
+ const selection = window.getSelection();
2016
+ if (!selection?.rangeCount) return position;
2017
+ const range = selection.getRangeAt(0);
2018
+ const preCaretRange = range.cloneRange();
2019
+ preCaretRange.selectNodeContents(element);
2020
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
2021
+ position = preCaretRange.toString().length;
2022
+ return position;
2023
+ }
2024
+ static setCaretToEnd(element) {
2025
+ if (!element) return;
2026
+ this.setIndex(element, Infinity);
2027
+ }
2028
+ static getIndex() {
2029
+ return window.getSelection()?.anchorOffset ?? NaN;
2030
+ }
2031
+ static setIndex1(offset) {
2032
+ const selection = window.getSelection();
2033
+ if (!selection?.anchorNode || !selection.rangeCount) return;
2034
+ const range = selection.getRangeAt(0);
2035
+ range.setStart(range.startContainer.firstChild ?? range.startContainer, offset);
2036
+ range.setEnd(range.startContainer.firstChild ?? range.startContainer, offset);
2037
+ }
2038
+ setCaretRightTo(element, offset) {
2039
+ const range = window.getSelection()?.getRangeAt(0);
2040
+ range?.setStart(range.endContainer, offset);
2041
+ range?.setEnd(range.endContainer, offset);
2042
+ }
2043
+ };
2044
+ //#endregion
2045
+ //#region ../../core/src/features/caret/focus.ts
2046
+ function enableFocus(store) {
2047
+ const container = store.dom.container();
2048
+ if (!container) return () => {};
2049
+ const scope = effectScope(() => {
2050
+ listen(container, "focusin", (e) => {
2051
+ const target = isHtmlElement(e.target) ? e.target : void 0;
2052
+ if (!target) {
2053
+ store.caret.location(void 0);
2054
+ return;
2055
+ }
2056
+ const result = store.dom.locateNode(target);
2057
+ if (!result.ok) {
2058
+ if (result.reason === "control") return;
2059
+ store.caret.location(void 0);
2060
+ return;
2061
+ }
2062
+ const role = result.value.textElement?.contains(target) ? "text" : "markDescendant";
2063
+ store.caret.location({
2064
+ address: result.value.address,
2065
+ role
2066
+ });
2067
+ });
2068
+ listen(container, "focusout", () => {
2069
+ store.caret.location(void 0);
2070
+ });
2071
+ listen(container, "click", () => {
2072
+ const tokens = store.parsing.tokens();
2073
+ if (tokens.length === 1 && tokens[0].type === "text" && tokens[0].content === "") {
2074
+ const container = store.dom.container();
2075
+ (container ? firstHtmlChild(container) : null)?.focus();
2076
+ }
2077
+ });
2078
+ });
2079
+ return () => scope();
2080
+ }
2081
+ //#endregion
2082
+ //#region ../../core/src/features/caret/selection.ts
2083
+ function enableSelection(store) {
2084
+ let pressedNode = null;
2085
+ let isPressed = false;
2086
+ const scope = effectScope(() => {
2087
+ listen(document, "mousedown", (e) => {
2088
+ pressedNode = nodeTarget(e);
2089
+ isPressed = true;
2090
+ });
2091
+ listen(document, "mousemove", (e) => {
2092
+ const container = store.dom.container();
2093
+ if (!container) return;
2094
+ const currentIsPressed = isPressed;
2095
+ const isNotInnerSome = !container.contains(pressedNode) || pressedNode !== e.target;
2096
+ const isInside = window.getSelection()?.containsNode(container, true);
2097
+ if (currentIsPressed && isNotInnerSome && isInside) {
2098
+ if (store.caret.selecting() !== "drag") store.caret.selecting("drag");
2099
+ }
2100
+ });
2101
+ listen(document, "mouseup", () => {
2102
+ isPressed = false;
2103
+ pressedNode = null;
2104
+ if (store.caret.selecting() === "drag") {
2105
+ const sel = window.getSelection();
2106
+ if (!sel || sel.isCollapsed) store.caret.selecting(void 0);
2107
+ }
2108
+ });
2109
+ listen(document, "selectionchange", () => {
2110
+ const sel = window.getSelection();
2111
+ if (store.caret.selecting() === "drag" && (!sel || sel.isCollapsed)) store.caret.selecting(void 0);
2112
+ if (!sel?.focusNode) return;
2113
+ const result = store.dom.locateNode(sel.focusNode);
2114
+ if (!result.ok) {
2115
+ if (result.reason === "control") return;
2116
+ store.caret.location(void 0);
2117
+ return;
2118
+ }
2119
+ const role = result.value.textElement?.contains(sel.focusNode) ? "text" : "markDescendant";
2120
+ store.caret.location({
2121
+ address: result.value.address,
2122
+ role
2123
+ });
2124
+ });
2125
+ alienEffect(() => {
2126
+ if (store.caret.selecting() === "drag") store.dom.reconcile();
2127
+ });
2128
+ });
2129
+ return () => {
2130
+ if (store.caret.selecting() === "drag") store.caret.selecting(void 0);
2131
+ scope();
2132
+ pressedNode = null;
2133
+ isPressed = false;
2134
+ };
2135
+ }
2136
+ //#endregion
2137
+ //#region ../../core/src/features/caret/CaretFeature.ts
2138
+ var CaretFeature = class {
2139
+ #disposers = [];
2140
+ constructor(_store) {
2141
+ this._store = _store;
2142
+ this.recovery = signal(void 0);
2143
+ this.location = signal(void 0);
2144
+ this.selecting = signal(void 0);
2145
+ }
2146
+ enable() {
2147
+ if (this.#disposers.length) return;
2148
+ this.#disposers = [enableFocus(this._store), enableSelection(this._store)];
2149
+ }
2150
+ disable() {
2151
+ this.#disposers.forEach((d) => d());
2152
+ this.#disposers = [];
2153
+ }
2154
+ placeAt(rawPosition, affinity = "after") {
2155
+ return this._store.dom.placeCaretAtRawPosition(rawPosition, affinity);
2156
+ }
2157
+ focus(address, boundary = "start") {
2158
+ return this._store.dom.focusAddress(address, boundary);
2159
+ }
2160
+ };
2161
+ //#endregion
2162
+ //#region ../../core/src/features/caret/selectionHelpers.ts
2163
+ function isFullSelection(store) {
2164
+ const sel = window.getSelection();
2165
+ const container = store.dom.container();
2166
+ if (!sel?.rangeCount || !container?.firstChild || !container.lastChild) return false;
2167
+ try {
2168
+ const range = sel.getRangeAt(0);
2169
+ return container.contains(range.startContainer) && container.contains(range.endContainer) && range.toString().length > 0;
2170
+ } catch {
2171
+ return false;
2172
+ }
2173
+ }
2174
+ function selectAllText(store, event) {
2175
+ if ((event.ctrlKey || event.metaKey) && event.code === "KeyA") {
2176
+ if (store.slots.isBlock()) return;
2177
+ event.preventDefault();
2178
+ const selection = window.getSelection();
2179
+ const anchorNode = store.dom.container()?.firstChild;
2180
+ const focusNode = store.dom.container()?.lastChild;
2181
+ if (!selection || !anchorNode || !focusNode) return;
2182
+ selection.setBaseAndExtent(anchorNode, 0, focusNode, 1);
2183
+ store.caret.selecting("all");
2184
+ }
2185
+ }
2186
+ //#endregion
2187
+ //#region ../../core/src/features/caret/TriggerFinder.ts
2188
+ /** Regex to match word characters from the start of a string */
2189
+ const wordRegex = /* @__PURE__ */ new RegExp(/^\w*/);
2190
+ var TriggerFinder = class TriggerFinder {
2191
+ constructor(store) {
2192
+ this.store = store;
2193
+ const caretPosition = Caret.getCurrentPosition();
2194
+ this.node = Caret.getSelectedNode();
2195
+ this.span = Caret.getFocusedSpan();
2196
+ this.dividedText = this.getDividedTextBy(caretPosition);
2197
+ }
2198
+ /**
2199
+ * Find overlay match in text using provided options and trigger extractor.
2200
+ * @template T - Type of option objects
2201
+ * @param options - Array of options to search through
2202
+ * @param getTrigger - Function that extracts trigger from each option
2203
+ * @returns OverlayMatch with correct option type or undefined
2204
+ *
2205
+ * @example
2206
+ * // React usage
2207
+ * TriggerFinder.find(options, (opt) => opt.slotProps?.overlay?.trigger ?? '@')
2208
+ *
2209
+ * @example
2210
+ * // Other framework usage
2211
+ * TriggerFinder.find(vueOptions, (opt) => opt.overlay?.trigger ?? '@')
2212
+ */
2213
+ static find(options, getTrigger, store) {
2214
+ if (!options) return;
2215
+ if (!Caret.isSelectedPosition) return;
2216
+ try {
2217
+ return new TriggerFinder(store).find(options, getTrigger);
2218
+ } catch {
2219
+ return;
2220
+ }
2221
+ }
2222
+ getDividedTextBy(position) {
2223
+ return {
2224
+ left: this.span.slice(0, position),
2225
+ right: this.span.slice(position)
2226
+ };
2227
+ }
2228
+ /**
2229
+ * Find overlay match in provided options.
2230
+ * @template T - Type of option objects
2231
+ * @param options - Array of options
2232
+ * @param getTrigger - Function to extract trigger from each option
2233
+ */
2234
+ find(options, getTrigger) {
2235
+ for (let i = 0; i < options.length; i++) {
2236
+ const option = options[i];
2237
+ const trigger = getTrigger(option, i);
2238
+ if (!trigger) continue;
2239
+ const match = this.matchInTextVia(trigger);
2240
+ if (match) {
2241
+ const range = this.#rawRangeForMatch(match.annotation, match.index);
2242
+ if (!range) return void 0;
2243
+ return {
2244
+ value: match.word,
2245
+ source: match.annotation,
2246
+ range,
2247
+ span: this.span,
2248
+ node: this.node,
2249
+ option
2250
+ };
2251
+ }
2252
+ }
2253
+ }
2254
+ #rawRangeForMatch(source, index) {
2255
+ if (!this.store) return {
2256
+ start: index,
2257
+ end: index + source.length
2258
+ };
2259
+ const boundary = this.store.dom.rawPositionFromBoundary(this.node, index + source.length, "after");
2260
+ if (!boundary.ok) return void 0;
2261
+ return {
2262
+ start: boundary.value - source.length,
2263
+ end: boundary.value
2264
+ };
2265
+ }
2266
+ matchInTextVia(trigger = "@") {
2267
+ const rightMatch = this.matchRightPart();
2268
+ const leftMatch = this.matchLeftPart(trigger);
2269
+ if (leftMatch) return {
2270
+ word: leftMatch.word + rightMatch.word,
2271
+ annotation: leftMatch.annotation + rightMatch.word,
2272
+ index: leftMatch.index
2273
+ };
2274
+ }
2275
+ matchRightPart() {
2276
+ const { right } = this.dividedText;
2277
+ return { word: right.match(wordRegex)?.[0] };
2278
+ }
2279
+ matchLeftPart(trigger) {
2280
+ const regex = this.makeTriggerRegex(trigger);
2281
+ const { left } = this.dividedText;
2282
+ const match = left.match(regex);
2283
+ if (!match) return;
2284
+ const [annotation, word] = match;
2285
+ return {
2286
+ word,
2287
+ annotation,
2288
+ index: match.index ?? 0
2289
+ };
2290
+ }
2291
+ makeTriggerRegex(trigger) {
2292
+ const patten = escape(trigger) + "(\\w*)$";
2293
+ return new RegExp(patten);
2294
+ }
2295
+ };
2296
+ //#endregion
2418
2297
  //#region ../../core/src/features/clipboard/pasteMarkup.ts
2419
2298
  /** Custom MIME type for markput markup syntax. */
2420
2299
  const MARKPUT_MIME = "application/x-markput";
@@ -2442,98 +2321,25 @@ function consumeMarkupPaste(container) {
2442
2321
  return markup;
2443
2322
  }
2444
2323
  //#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);
2460
- }
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
- };
2506
- }
2507
- //#endregion
2508
2324
  //#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;
2325
+ function htmlFromRange(range) {
2326
+ const fragment = range.cloneContents();
2327
+ const div = document.createElement("div");
2328
+ div.appendChild(fragment);
2329
+ return div.innerHTML;
2330
+ }
2331
+ function serializeRawRange(tokens, range) {
2332
+ return toString(trimTokensForRawRange(tokens, range));
2333
+ }
2334
+ function trimTokensForRawRange(tokens, range) {
2335
+ return tokens.filter((token) => token.position.end > range.start && token.position.start < range.end).map((token) => {
2336
+ if (token.type === "text") {
2337
+ const start = Math.max(0, range.start - token.position.start);
2338
+ const end = Math.min(token.content.length, range.end - token.position.start);
2339
+ return Object.assign({}, token, { content: token.content.slice(start, end) });
2340
+ }
2341
+ if (token.children.length === 0) return token;
2342
+ return Object.assign({}, token, { children: trimTokensForRawRange(token.children, range) });
2537
2343
  });
2538
2344
  }
2539
2345
  var ClipboardFeature = class {
@@ -2543,7 +2349,7 @@ var ClipboardFeature = class {
2543
2349
  }
2544
2350
  enable() {
2545
2351
  if (this.#scope) return;
2546
- const container = this.store.slots.container();
2352
+ const container = this.store.dom.container();
2547
2353
  if (!container) return;
2548
2354
  this.#scope = effectScope(() => {
2549
2355
  listen(container, "copy", (e) => {
@@ -2551,25 +2357,14 @@ var ClipboardFeature = class {
2551
2357
  });
2552
2358
  listen(container, "cut", (e) => {
2553
2359
  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
2360
+ const raw = this.store.dom.readRawSelection();
2361
+ if (!raw.ok || raw.value.range.start === raw.value.range.end) return;
2362
+ this.store.value.replaceRange(raw.value.range, "", {
2363
+ source: "cut",
2364
+ recover: {
2365
+ kind: "caret",
2366
+ rawPosition: raw.value.range.start
2367
+ }
2573
2368
  });
2574
2369
  });
2575
2370
  });
@@ -2579,20 +2374,15 @@ var ClipboardFeature = class {
2579
2374
  this.#scope = void 0;
2580
2375
  }
2581
2376
  #handleCopy(e) {
2582
- const container = this.store.slots.container();
2583
- if (!container) return false;
2377
+ if (!this.store.dom.container()) return false;
2378
+ const raw = this.store.dom.readRawSelection();
2379
+ if (!raw.ok || raw.value.range.start === raw.value.range.end) return false;
2584
2380
  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;
2381
+ const range = sel?.rangeCount ? sel.getRangeAt(0) : void 0;
2382
+ if (!range) return false;
2590
2383
  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));
2384
+ const html = htmlFromRange(range);
2385
+ const markup = serializeRawRange(this.store.parsing.tokens(), raw.value.range);
2596
2386
  e.preventDefault();
2597
2387
  e.clipboardData?.setData("text/plain", plainText);
2598
2388
  e.clipboardData?.setData("text/html", html);
@@ -2601,120 +2391,705 @@ var ClipboardFeature = class {
2601
2391
  }
2602
2392
  };
2603
2393
  //#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"));
2607
- }
2608
- //#endregion
2609
2394
  //#region ../../core/src/features/dom/DomFeature.ts
2395
+ function nextTextNode(walker) {
2396
+ const node = walker.nextNode();
2397
+ return node instanceof Text ? node : null;
2398
+ }
2399
+ function splitsSurrogatePair(text, offset) {
2400
+ if (offset <= 0 || offset >= text.length) return false;
2401
+ const prev = text.charCodeAt(offset - 1);
2402
+ const next = text.charCodeAt(offset);
2403
+ return prev >= 55296 && prev <= 56319 && next >= 56320 && next <= 57343;
2404
+ }
2405
+ function textOffsetWithin(surface, node, offset) {
2406
+ if (node.nodeType === Node.TEXT_NODE) {
2407
+ if (splitsSurrogatePair(node.textContent ?? "", offset)) return void 0;
2408
+ return node instanceof Text ? textOffsetFromTreeWalker(surface, node, offset) : void 0;
2409
+ }
2410
+ if (node === surface) return elementBoundaryOffset(surface, offset);
2411
+ }
2412
+ function textOffsetFromTreeWalker(surface, target, targetOffset) {
2413
+ let total = 0;
2414
+ const walker = document.createTreeWalker(surface, NodeFilter.SHOW_TEXT);
2415
+ let current = nextTextNode(walker);
2416
+ while (current) {
2417
+ if (current === target) return total + targetOffset;
2418
+ total += current.length;
2419
+ current = nextTextNode(walker);
2420
+ }
2421
+ }
2422
+ function textLength(surface) {
2423
+ let total = 0;
2424
+ const walker = document.createTreeWalker(surface, NodeFilter.SHOW_TEXT);
2425
+ let current = nextTextNode(walker);
2426
+ while (current) {
2427
+ total += current.length;
2428
+ current = nextTextNode(walker);
2429
+ }
2430
+ return total;
2431
+ }
2432
+ function elementBoundaryOffset(surface, offset) {
2433
+ if (offset <= 0) return 0;
2434
+ if (offset >= surface.childNodes.length) return textLength(surface);
2435
+ let total = 0;
2436
+ for (let i = 0; i < offset; i++) {
2437
+ const child = surface.childNodes.item(i);
2438
+ if (child.nodeType === Node.TEXT_NODE && child instanceof Text) {
2439
+ total += child.length;
2440
+ continue;
2441
+ }
2442
+ if (child instanceof HTMLElement) total += textLength(child);
2443
+ }
2444
+ return total;
2445
+ }
2446
+ function hasEditableAncestorBefore(node, boundary) {
2447
+ let current = node instanceof HTMLElement ? node : node.parentElement;
2448
+ while (current && current !== boundary) {
2449
+ if (current.isContentEditable || current.contentEditable === "true" || current.contentEditable === "plaintext-only") return true;
2450
+ current = current.parentElement;
2451
+ }
2452
+ return false;
2453
+ }
2610
2454
  var DomFeature = class {
2455
+ #domIndex = signal(void 0, { readonly: true });
2456
+ #pendingControls = /* @__PURE__ */ new Map();
2457
+ #pendingChildSequences = /* @__PURE__ */ new Map();
2458
+ #nextControlId = 0;
2459
+ #nextChildSequenceId = 0;
2460
+ #elementRoles = /* @__PURE__ */ new WeakMap();
2461
+ #pathElements = /* @__PURE__ */ new Map();
2462
+ #generation = 0;
2463
+ #rendering = false;
2464
+ #isComposing = false;
2465
+ #queuedRender = false;
2611
2466
  #scope;
2612
2467
  constructor(_store) {
2613
2468
  this._store = _store;
2469
+ this.index = computed(() => this.#domIndex());
2470
+ this.container = signal(null);
2471
+ this.diagnostics = event();
2614
2472
  }
2615
2473
  enable() {
2616
2474
  if (this.#scope) return;
2617
2475
  this.#scope = effectScope(() => {
2618
- alienEffect(() => {
2619
- this._store.props.readOnly();
2620
- this.reconcile();
2621
- });
2622
- alienEffect(() => {
2623
- if (this._store.caret.selecting() === void 0) this.reconcile();
2476
+ watch(this._store.lifecycle.rendered, () => {
2477
+ this.#handleRendered();
2624
2478
  });
2479
+ watch(computed(() => ({
2480
+ readOnly: this._store.props.readOnly(),
2481
+ selecting: this._store.caret.selecting()
2482
+ })), () => this.reconcile());
2625
2483
  });
2626
2484
  }
2627
2485
  disable() {
2628
2486
  this.#scope?.();
2629
2487
  this.#scope = void 0;
2630
2488
  }
2489
+ compositionStarted() {
2490
+ this.#isComposing = true;
2491
+ }
2492
+ compositionEnded() {
2493
+ if (!this.#isComposing) return;
2494
+ this.#isComposing = false;
2495
+ }
2496
+ controlFor(ownerPath) {
2497
+ const key = `control:${ownerPath ? pathKey(ownerPath) : "global"}:${++this.#nextControlId}`;
2498
+ const callback = (element) => {
2499
+ if (element) this.#pendingControls.set(key, {
2500
+ ownerPath: ownerPath ? [...ownerPath] : void 0,
2501
+ element
2502
+ });
2503
+ else this.#pendingControls.delete(key);
2504
+ };
2505
+ return callback;
2506
+ }
2507
+ childrenFor(ownerPath) {
2508
+ const key = `children:${pathKey(ownerPath)}:${++this.#nextChildSequenceId}`;
2509
+ const callback = (element) => {
2510
+ if (element) this.#pendingChildSequences.set(key, {
2511
+ ownerPath: [...ownerPath],
2512
+ element
2513
+ });
2514
+ else this.#pendingChildSequences.delete(key);
2515
+ };
2516
+ return callback;
2517
+ }
2631
2518
  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;
2519
+ this.#reconcileStructuralTextSurfaces();
2520
+ }
2521
+ locateNode(node) {
2522
+ if (!this.index()) return {
2523
+ ok: false,
2524
+ reason: "notIndexed"
2525
+ };
2526
+ const container = this.container();
2527
+ if (!container || !container.contains(node)) return {
2528
+ ok: false,
2529
+ reason: "outsideEditor"
2530
+ };
2531
+ let current = node;
2532
+ while (current) {
2533
+ if (current instanceof HTMLElement) {
2534
+ const role = this.#elementRoles.get(current);
2535
+ if (role?.role === "control") return {
2536
+ ok: false,
2537
+ reason: "control"
2538
+ };
2539
+ if (role) {
2540
+ const elements = this.#pathElements.get(pathKey(role.path));
2541
+ if (!elements?.tokenElement) return {
2542
+ ok: false,
2543
+ reason: "notIndexed"
2544
+ };
2545
+ return {
2546
+ ok: true,
2547
+ value: {
2548
+ address: role.address,
2549
+ tokenElement: elements.tokenElement,
2550
+ textElement: elements.textElement,
2551
+ rowElement: elements.rowElement
2552
+ }
2553
+ };
2554
+ }
2555
+ }
2556
+ if (current === container) break;
2557
+ current = current.parentNode;
2558
+ }
2559
+ return {
2560
+ ok: false,
2561
+ reason: "outsideEditor"
2562
+ };
2563
+ }
2564
+ placeCaretAtRawPosition(rawPosition, affinity = "after") {
2565
+ if (!this.index()) return {
2566
+ ok: false,
2567
+ reason: "notIndexed"
2568
+ };
2569
+ const target = this.#findTextTargetForRawPosition(rawPosition, affinity);
2570
+ if (!target) return this.#focusMarkBoundaryForRawPosition(rawPosition);
2571
+ target.element.focus();
2572
+ this.#placeCaretInTextSurface(target.element, rawPosition - target.start);
2573
+ return {
2574
+ ok: true,
2575
+ value: void 0
2576
+ };
2577
+ }
2578
+ focusAddress(address, boundary = "start") {
2579
+ if (!this.index()) return {
2580
+ ok: false,
2581
+ reason: "notIndexed"
2582
+ };
2583
+ if (!this._store.parsing.index().resolveAddress(address).ok) return {
2584
+ ok: false,
2585
+ reason: "stale"
2586
+ };
2587
+ const elements = this.#pathElements.get(pathKey(address.path));
2588
+ const target = elements?.textElement ?? elements?.tokenElement ?? elements?.rowElement;
2589
+ if (!target) return {
2590
+ ok: false,
2591
+ reason: "notIndexed"
2592
+ };
2593
+ target.focus();
2594
+ const role = target === elements?.textElement ? "text" : target === elements?.rowElement ? "row" : "markDescendant";
2595
+ if (role === "markDescendant") this.#placeCollapsedBoundary(target, boundary === "end" ? target.childNodes.length : 0);
2596
+ this._store.caret.location({
2597
+ address,
2598
+ role
2599
+ });
2600
+ return {
2601
+ ok: true,
2602
+ value: void 0
2603
+ };
2604
+ }
2605
+ rawPositionFromBoundary(node, offset, affinity = "after") {
2606
+ if (!this.index()) return {
2607
+ ok: false,
2608
+ reason: "notIndexed"
2609
+ };
2610
+ if (this.#isComposing) return {
2611
+ ok: false,
2612
+ reason: "composing"
2613
+ };
2614
+ const container = this.container();
2615
+ if (container && node === container) return this.#rawPositionFromContainerBoundary(offset, affinity);
2616
+ const location = this.locateNode(node);
2617
+ if (!location.ok) return location.reason === "control" ? {
2618
+ ok: false,
2619
+ reason: "control"
2620
+ } : location;
2621
+ const token = this._store.parsing.index().resolveAddress(location.value.address);
2622
+ if (!token.ok) return {
2623
+ ok: false,
2624
+ reason: "notIndexed"
2625
+ };
2626
+ if (node instanceof HTMLElement) {
2627
+ if (this.#elementRoles.get(node)?.role === "childSequence") {
2628
+ const childCount = node.childNodes.length;
2629
+ if (offset <= 0) return {
2630
+ ok: true,
2631
+ value: token.value.position.start
2632
+ };
2633
+ if (offset >= childCount) return {
2634
+ ok: true,
2635
+ value: token.value.position.end
2636
+ };
2637
+ return this.#rawPositionFromTokenChildBoundary(node, offset, token.value, affinity);
2646
2638
  }
2647
- } else for (let i = 0; i < children.length; i += 2) {
2648
- const el = childAt(container, i);
2649
- if (el) el.contentEditable = value;
2639
+ }
2640
+ const textElement = location.value.textElement;
2641
+ if (textElement?.contains(node)) {
2642
+ const local = textOffsetWithin(textElement, node, offset);
2643
+ if (local === void 0) return {
2644
+ ok: false,
2645
+ reason: "invalidBoundary"
2646
+ };
2647
+ return {
2648
+ ok: true,
2649
+ value: token.value.position.start + local
2650
+ };
2651
+ }
2652
+ if (node === location.value.tokenElement) {
2653
+ const childCount = location.value.tokenElement.childNodes.length;
2654
+ if (offset <= 0) return {
2655
+ ok: true,
2656
+ value: token.value.position.start
2657
+ };
2658
+ if (offset >= childCount) return {
2659
+ ok: true,
2660
+ value: token.value.position.end
2661
+ };
2662
+ return this.#rawPositionFromTokenChildBoundary(location.value.tokenElement, offset, token.value, affinity);
2663
+ }
2664
+ if (token.value.type === "mark" && location.value.tokenElement.contains(node)) {
2665
+ if (hasEditableAncestorBefore(node, location.value.tokenElement)) return {
2666
+ ok: false,
2667
+ reason: "invalidBoundary"
2668
+ };
2669
+ return {
2670
+ ok: true,
2671
+ value: affinity === "after" ? token.value.position.start : token.value.position.end
2672
+ };
2673
+ }
2674
+ if (location.value.rowElement && node === location.value.rowElement) return {
2675
+ ok: true,
2676
+ value: offset <= 0 ? token.value.position.start : token.value.position.end
2677
+ };
2678
+ return {
2679
+ ok: false,
2680
+ reason: "invalidBoundary"
2681
+ };
2682
+ }
2683
+ readRawSelection() {
2684
+ if (!this.index()) return {
2685
+ ok: false,
2686
+ reason: "notIndexed"
2687
+ };
2688
+ const selection = window.getSelection();
2689
+ if (!selection || selection.rangeCount === 0) return {
2690
+ ok: false,
2691
+ reason: "invalidBoundary"
2692
+ };
2693
+ const range = selection.getRangeAt(0);
2694
+ const start = this.rawPositionFromBoundary(range.startContainer, range.startOffset, "after");
2695
+ const end = this.rawPositionFromBoundary(range.endContainer, range.endOffset, "before");
2696
+ if (!start.ok) {
2697
+ const reason = start.reason === "composing" ? "invalidBoundary" : start.reason;
2698
+ return {
2699
+ ok: false,
2700
+ reason: reason === "control" || reason === "outsideEditor" ? "mixedBoundary" : reason
2701
+ };
2702
+ }
2703
+ if (!end.ok) {
2704
+ const reason = end.reason === "composing" ? "invalidBoundary" : end.reason;
2705
+ return {
2706
+ ok: false,
2707
+ reason: reason === "control" || reason === "outsideEditor" ? "mixedBoundary" : reason
2708
+ };
2709
+ }
2710
+ const rangeValue = start.value <= end.value ? {
2711
+ start: start.value,
2712
+ end: end.value
2713
+ } : {
2714
+ start: end.value,
2715
+ end: start.value
2716
+ };
2717
+ const direction = rangeValue.start === rangeValue.end ? void 0 : selection.anchorNode === range.endContainer && selection.anchorOffset === range.endOffset ? "backward" : "forward";
2718
+ return {
2719
+ ok: true,
2720
+ value: direction ? {
2721
+ range: rangeValue,
2722
+ direction
2723
+ } : { range: rangeValue }
2724
+ };
2725
+ }
2726
+ #handleRendered() {
2727
+ if (this.#rendering) {
2728
+ this.#queuedRender = true;
2729
+ this.diagnostics({
2730
+ kind: "renderReentry",
2731
+ reason: "rendered event queued during DOM indexing"
2732
+ });
2733
+ return;
2734
+ }
2735
+ this.#rendering = true;
2736
+ try {
2737
+ this.#commitRendered();
2738
+ } finally {
2739
+ this.#rendering = false;
2740
+ const queued = this.#queuedRender;
2741
+ this.#queuedRender = false;
2742
+ if (queued) this.#handleRendered();
2743
+ }
2744
+ }
2745
+ #commitRendered() {
2746
+ const container = this.container();
2747
+ if (!container) {
2748
+ this.diagnostics({
2749
+ kind: "missingContainer",
2750
+ reason: "container is not registered"
2751
+ });
2752
+ return;
2753
+ }
2754
+ const tokenIndex = this._store.parsing.index();
2755
+ const pathElements = /* @__PURE__ */ new Map();
2756
+ const elementRoles = /* @__PURE__ */ new WeakMap();
2757
+ const controlElements = /* @__PURE__ */ new Set();
2758
+ for (const { element } of this.#pendingControls.values()) {
2759
+ controlElements.add(element);
2760
+ elementRoles.set(element, { role: "control" });
2650
2761
  }
2651
2762
  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);
2763
+ if (this._store.props.layout() === "block") this.#indexBlockTokens(container, tokens, tokenIndex, controlElements, pathElements, elementRoles);
2764
+ else this.#indexTokenSequence(container, tokens, [], void 0, tokenIndex, controlElements, pathElements, elementRoles);
2765
+ this.#pathElements = pathElements;
2766
+ this.#elementRoles = elementRoles;
2767
+ this.#reconcileStructuralTextSurfaces();
2768
+ batch(() => this.#domIndex({ generation: ++this.#generation }), { mutable: true });
2769
+ this.#clearStaleCaretLocation();
2770
+ this.#applyPendingRecovery();
2771
+ }
2772
+ #elementChildren(element) {
2773
+ return Array.from(element.children).filter((child) => child instanceof HTMLElement);
2774
+ }
2775
+ #isControlRoot(element, controlElements) {
2776
+ if (controlElements.has(element)) return true;
2777
+ for (const control of controlElements) if (element.contains(control)) return true;
2778
+ return false;
2779
+ }
2780
+ #childSequenceHostsFor(ownerPath) {
2781
+ const hosts = [];
2782
+ for (const registration of this.#pendingChildSequences.values()) if (pathEquals(registration.ownerPath, ownerPath)) hosts.push(registration.element);
2783
+ return hosts;
2784
+ }
2785
+ #indexNestedTokenSequence(token, path, address, ownerElement, rowElement, tokenIndex, controlElements, pathElements, elementRoles) {
2786
+ if (token.type !== "mark" || token.children.length === 0) return;
2787
+ const hosts = this.#childSequenceHostsFor(path);
2788
+ if (hosts.length === 0) {
2789
+ this.#indexTokenSequence(ownerElement, token.children, path, rowElement, tokenIndex, controlElements, pathElements, elementRoles);
2790
+ return;
2791
+ }
2792
+ const ownerKey = pathKey(path);
2793
+ if (hosts.length !== 1) {
2794
+ this.diagnostics({
2795
+ kind: "ambiguousStructure",
2796
+ path,
2797
+ reason: `expected exactly 1 child sequence host for owner path ${ownerKey} but found ${hosts.length}`
2798
+ });
2799
+ return;
2800
+ }
2801
+ const host = hosts[0];
2802
+ if (!ownerElement.contains(host)) {
2803
+ this.diagnostics({
2804
+ kind: "ambiguousStructure",
2805
+ path,
2806
+ reason: `child sequence host for owner path ${ownerKey} is not contained by owner token element`
2807
+ });
2808
+ return;
2663
2809
  }
2810
+ elementRoles.set(host, {
2811
+ role: "childSequence",
2812
+ path,
2813
+ address
2814
+ });
2815
+ this.#indexTokenSequence(host, token.children, path, rowElement, tokenIndex, controlElements, pathElements, elementRoles);
2664
2816
  }
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++;
2685
- }
2686
- const el = childAt(parent, childIdx);
2687
- if (el) {
2688
- this.#reconcileMarkChildren(token.children, el, editable);
2689
- childIdx++;
2817
+ #indexBlockTokens(container, tokens, tokenIndex, controlElements, pathElements, elementRoles) {
2818
+ const rows = this.#elementChildren(container);
2819
+ if (rows.length !== tokens.length) this.diagnostics({
2820
+ kind: "ambiguousStructure",
2821
+ reason: `expected ${tokens.length} block rows but found ${rows.length}`
2822
+ });
2823
+ tokens.forEach((token, i) => {
2824
+ const row = rows.at(i);
2825
+ if (!row) return;
2826
+ const candidates = this.#elementChildren(row).filter((child) => !this.#isControlRoot(child, controlElements));
2827
+ if (candidates.length !== 1) {
2828
+ this.diagnostics({
2829
+ kind: "ambiguousStructure",
2830
+ path: [i],
2831
+ reason: `expected 1 block token element but found ${candidates.length}`
2832
+ });
2833
+ return;
2690
2834
  }
2835
+ this.#indexTokenElement(token, [i], candidates[0], row, tokenIndex, controlElements, pathElements, elementRoles);
2836
+ });
2837
+ }
2838
+ #indexTokenSequence(parent, tokens, basePath, rowElement, tokenIndex, controlElements, pathElements, elementRoles) {
2839
+ const elements = this.#elementChildren(parent).filter((child) => !this.#isControlRoot(child, controlElements));
2840
+ if (elements.length !== tokens.length) {
2841
+ this.diagnostics({
2842
+ kind: "ambiguousStructure",
2843
+ path: basePath.length ? basePath : void 0,
2844
+ reason: `expected ${tokens.length} child token elements but found ${elements.length}`
2845
+ });
2846
+ return;
2691
2847
  }
2848
+ tokens.forEach((token, i) => {
2849
+ const element = elements.at(i);
2850
+ if (!element) return;
2851
+ this.#indexTokenElement(token, [...basePath, i], element, rowElement, tokenIndex, controlElements, pathElements, elementRoles);
2852
+ });
2692
2853
  }
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);
2854
+ #indexTokenElement(token, path, element, rowElement, tokenIndex, controlElements, pathElements, elementRoles) {
2855
+ const address = tokenIndex.addressFor(path);
2856
+ if (!address) {
2857
+ this.diagnostics({
2858
+ kind: "stalePath",
2859
+ path,
2860
+ reason: "structural path no longer resolves"
2861
+ });
2862
+ return;
2863
+ }
2864
+ const record = {
2865
+ path: [...path],
2866
+ address,
2867
+ tokenElement: element,
2868
+ textElement: token.type === "text" ? element : void 0,
2869
+ rowElement
2870
+ };
2871
+ pathElements.set(tokenIndex.key(path), record);
2872
+ elementRoles.set(element, {
2873
+ role: token.type === "text" ? "text" : "token",
2874
+ path,
2875
+ address
2876
+ });
2877
+ if (rowElement && path.length === 1) elementRoles.set(rowElement, {
2878
+ role: "row",
2879
+ path,
2880
+ address
2881
+ });
2882
+ this.#indexNestedTokenSequence(token, path, address, element, rowElement, tokenIndex, controlElements, pathElements, elementRoles);
2883
+ }
2884
+ #reconcileStructuralTextSurfaces() {
2885
+ const tokenIndex = this._store.parsing.index();
2886
+ const editable = this._store.props.readOnly() || this._store.caret.selecting() ? "false" : "true";
2887
+ for (const record of this.#pathElements.values()) {
2888
+ const resolved = tokenIndex.resolveAddress(record.address);
2889
+ if (!resolved.ok) {
2890
+ this.diagnostics({
2891
+ kind: "stalePath",
2892
+ path: record.path,
2893
+ reason: "structural path became stale during reconciliation"
2894
+ });
2895
+ continue;
2896
+ }
2897
+ if (record.textElement) {
2898
+ if (resolved.value.type !== "text") {
2899
+ this.diagnostics({
2900
+ kind: "missingRole",
2901
+ path: record.path,
2902
+ reason: "text role registered for non-text token"
2903
+ });
2904
+ continue;
2703
2905
  }
2906
+ if (record.textElement.textContent !== resolved.value.content) record.textElement.textContent = resolved.value.content;
2907
+ record.textElement.contentEditable = editable;
2704
2908
  continue;
2705
2909
  }
2706
- const el = childAt(blockEl, readOnly ? 0 : 1);
2707
- if (!el) continue;
2708
- if (el.textContent !== token.content) el.textContent = token.content;
2910
+ if (resolved.value.type === "mark") if (this._store.props.readOnly()) record.tokenElement.removeAttribute("tabindex");
2911
+ else record.tokenElement.tabIndex = 0;
2912
+ }
2913
+ }
2914
+ #rawPositionFromContainerBoundary(offset, affinity) {
2915
+ const tokens = this._store.parsing.tokens();
2916
+ if (tokens.length === 0) return {
2917
+ ok: true,
2918
+ value: 0
2919
+ };
2920
+ if (offset <= 0) return {
2921
+ ok: true,
2922
+ value: tokens[0].position.start
2923
+ };
2924
+ if (offset >= tokens.length) return {
2925
+ ok: true,
2926
+ value: tokens[tokens.length - 1].position.end
2927
+ };
2928
+ const before = tokens[offset - 1];
2929
+ const after = tokens[offset];
2930
+ return {
2931
+ ok: true,
2932
+ value: affinity === "before" ? before.position.end : after.position.start
2933
+ };
2934
+ }
2935
+ #rawPositionFromTokenChildBoundary(tokenElement, offset, token, affinity) {
2936
+ if (token.type === "text") {
2937
+ const textElement = this.#pathElements.get(pathKey(this._store.parsing.index().pathFor(token) ?? []))?.textElement;
2938
+ if (!textElement || textLength(textElement) === 0) return {
2939
+ ok: true,
2940
+ value: token.position.start
2941
+ };
2942
+ }
2943
+ const before = this.#locateRegisteredDescendant(tokenElement.childNodes.item(offset - 1));
2944
+ const after = this.#locateRegisteredDescendant(tokenElement.childNodes.item(offset));
2945
+ if (before?.ok && after?.ok) {
2946
+ const beforeToken = this._store.parsing.index().resolveAddress(before.value.address);
2947
+ const afterToken = this._store.parsing.index().resolveAddress(after.value.address);
2948
+ if (beforeToken.ok && afterToken.ok) return {
2949
+ ok: true,
2950
+ value: affinity === "before" ? beforeToken.value.position.end : afterToken.value.position.start
2951
+ };
2952
+ }
2953
+ return {
2954
+ ok: true,
2955
+ value: affinity === "before" ? token.position.start : token.position.end
2956
+ };
2957
+ }
2958
+ #locateRegisteredDescendant(node) {
2959
+ if (!node) return void 0;
2960
+ return this.locateNode(node);
2961
+ }
2962
+ #findTextTargetForRawPosition(rawPosition, affinity) {
2963
+ const candidates = [];
2964
+ const tokenIndex = this._store.parsing.index();
2965
+ for (const record of this.#pathElements.values()) {
2966
+ if (!record.textElement) continue;
2967
+ const resolved = tokenIndex.resolveAddress(record.address);
2968
+ if (!resolved.ok || resolved.value.type !== "text") continue;
2969
+ candidates.push({
2970
+ element: record.textElement,
2971
+ start: resolved.value.position.start,
2972
+ end: resolved.value.position.end
2973
+ });
2974
+ }
2975
+ candidates.sort((a, b) => a.start - b.start);
2976
+ const containing = candidates.find((candidate) => rawPosition >= candidate.start && rawPosition <= candidate.end);
2977
+ if (containing) return containing;
2978
+ if (affinity === "before") return [...candidates].toReversed().find((candidate) => candidate.end <= rawPosition);
2979
+ return candidates.find((candidate) => candidate.start >= rawPosition);
2980
+ }
2981
+ #focusMarkBoundaryForRawPosition(rawPosition) {
2982
+ const tokenIndex = this._store.parsing.index();
2983
+ for (const record of this.#pathElements.values()) {
2984
+ const resolved = tokenIndex.resolveAddress(record.address);
2985
+ if (!resolved.ok || resolved.value.type !== "mark") continue;
2986
+ if (rawPosition !== resolved.value.position.start && rawPosition !== resolved.value.position.end) continue;
2987
+ const boundary = rawPosition === resolved.value.position.end ? "end" : "start";
2988
+ record.tokenElement.focus();
2989
+ this.#placeCollapsedBoundary(record.tokenElement, boundary === "end" ? record.tokenElement.childNodes.length : 0);
2990
+ this._store.caret.location({
2991
+ address: record.address,
2992
+ role: "markDescendant"
2993
+ });
2994
+ return {
2995
+ ok: true,
2996
+ value: void 0
2997
+ };
2998
+ }
2999
+ return {
3000
+ ok: false,
3001
+ reason: "invalidBoundary"
3002
+ };
3003
+ }
3004
+ #placeCaretInTextSurface(surface, offset) {
3005
+ const selection = window.getSelection();
3006
+ if (!selection) return;
3007
+ const boundary = this.#boundaryInTextSurface(surface, offset);
3008
+ if (!boundary) return;
3009
+ const range = document.createRange();
3010
+ range.setStart(boundary.node, boundary.offset);
3011
+ range.collapse(true);
3012
+ selection.removeAllRanges();
3013
+ selection.addRange(range);
3014
+ }
3015
+ #placeCollapsedBoundary(element, offset) {
3016
+ const selection = window.getSelection();
3017
+ if (!selection) return;
3018
+ const range = document.createRange();
3019
+ range.setStart(element, Math.min(Math.max(offset, 0), element.childNodes.length));
3020
+ range.collapse(true);
3021
+ selection.removeAllRanges();
3022
+ selection.addRange(range);
3023
+ }
3024
+ #applyPendingRecovery() {
3025
+ const recovery = this._store.caret.recovery();
3026
+ if (!recovery) return;
3027
+ if (recovery.kind === "caret") {
3028
+ const result = this._store.caret.placeAt(recovery.rawPosition, recovery.affinity);
3029
+ this._store.caret.recovery(void 0);
3030
+ if (!result.ok) this.diagnostics({
3031
+ kind: "recoveryFailed",
3032
+ reason: `pending caret recovery could not be applied: ${result.reason}`
3033
+ });
3034
+ return;
3035
+ }
3036
+ const result = this.#placeSelection(recovery.selection);
3037
+ this._store.caret.recovery(void 0);
3038
+ if (!result.ok) this.diagnostics({
3039
+ kind: "recoveryFailed",
3040
+ reason: `pending selection recovery could not be applied: ${result.reason}`
3041
+ });
3042
+ }
3043
+ #placeSelection(selection) {
3044
+ const start = this.#findTextTargetForRawPosition(selection.range.start, "after");
3045
+ const end = this.#findTextTargetForRawPosition(selection.range.end, "before");
3046
+ const browserSelection = window.getSelection();
3047
+ if (!start || !end || !browserSelection) return {
3048
+ ok: false,
3049
+ reason: "invalidBoundary"
3050
+ };
3051
+ const startBoundary = this.#boundaryInTextSurface(start.element, selection.range.start - start.start);
3052
+ const endBoundary = this.#boundaryInTextSurface(end.element, selection.range.end - end.start);
3053
+ if (!startBoundary || !endBoundary) return {
3054
+ ok: false,
3055
+ reason: "invalidBoundary"
3056
+ };
3057
+ const range = document.createRange();
3058
+ range.setStart(startBoundary.node, startBoundary.offset);
3059
+ range.setEnd(endBoundary.node, endBoundary.offset);
3060
+ browserSelection.removeAllRanges();
3061
+ browserSelection.addRange(range);
3062
+ return {
3063
+ ok: true,
3064
+ value: void 0
3065
+ };
3066
+ }
3067
+ #boundaryInTextSurface(surface, offset) {
3068
+ const walker = document.createTreeWalker(surface, NodeFilter.SHOW_TEXT);
3069
+ let remaining = Math.max(0, offset);
3070
+ let node = nextTextNode(walker);
3071
+ while (node) {
3072
+ if (remaining <= node.length) return {
3073
+ node,
3074
+ offset: remaining
3075
+ };
3076
+ remaining -= node.length;
3077
+ node = nextTextNode(walker);
2709
3078
  }
3079
+ const text = surface.firstChild instanceof Text ? surface.firstChild : document.createTextNode("");
3080
+ if (!text.parentNode) surface.append(text);
3081
+ return {
3082
+ node: text,
3083
+ offset: text.length
3084
+ };
3085
+ }
3086
+ #clearStaleCaretLocation() {
3087
+ const location = this._store.caret.location();
3088
+ if (!location) return;
3089
+ if (!this._store.parsing.index().resolveAddress(location.address).ok || !this.#pathElements.has(pathKey(location.address.path))) this._store.caret.location(void 0);
2710
3090
  }
2711
3091
  };
2712
3092
  //#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
3093
  //#region ../../core/src/features/editing/createRowContent.ts
2719
3094
  function createRowContent(options) {
2720
3095
  const firstOption = options[0];
@@ -2726,47 +3101,6 @@ function createRowContent(options) {
2726
3101
  });
2727
3102
  }
2728
3103
  //#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
3104
  //#region ../../core/src/features/drag/operations.ts
2771
3105
  function gapText(value, a, b) {
2772
3106
  return value.substring(a.position.end, b.position.start);
@@ -2876,16 +3210,16 @@ var DragFeature = class {
2876
3210
  this.#unsub = watch(this.action, (action) => {
2877
3211
  switch (action.type) {
2878
3212
  case "reorder":
2879
- this.#reorder(action.source, action.target);
3213
+ this.#reorder(action);
2880
3214
  break;
2881
3215
  case "add":
2882
- this.#add(action.afterIndex);
3216
+ this.#add(action);
2883
3217
  break;
2884
3218
  case "delete":
2885
- this.#delete(action.index);
3219
+ this.#delete(action);
2886
3220
  break;
2887
3221
  case "duplicate":
2888
- this.#duplicate(action.index);
3222
+ this.#duplicate(action);
2889
3223
  break;
2890
3224
  }
2891
3225
  });
@@ -2894,36 +3228,74 @@ var DragFeature = class {
2894
3228
  this.#unsub?.();
2895
3229
  this.#unsub = void 0;
2896
3230
  }
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);
3231
+ #reorder(action) {
3232
+ const value = this.store.value.current();
3233
+ const rows = this.store.parsing.tokens();
3234
+ const newValue = reorderDragRows(value, rows, action.source, action.target);
3235
+ if (newValue !== value) this.store.value.replaceAll(newValue, {
3236
+ source: "drag",
3237
+ recover: this.#recoverAfterDrag(action, rows, newValue)
3238
+ });
2902
3239
  }
2903
- #add(afterIndex) {
2904
- const value = this.store.props.value();
2905
- if (value == null || !this.store.props.onChange()) return;
3240
+ #add(action) {
3241
+ const value = this.store.value.current();
2906
3242
  const rawRows = this.store.parsing.tokens();
2907
3243
  const rows = rawRows.length > 0 ? rawRows : [EMPTY_TEXT_TOKEN];
2908
3244
  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();
3245
+ const newValue = addDragRow(value, rows, action.afterIndex, newRowContent);
3246
+ this.store.value.replaceAll(newValue, {
3247
+ source: "drag",
3248
+ recover: this.#recoverAfterDrag(action, rows, newValue)
2914
3249
  });
2915
3250
  }
2916
- #delete(index) {
2917
- const value = this.store.props.value();
2918
- if (value == null || !this.store.props.onChange()) return;
3251
+ #delete(action) {
3252
+ const value = this.store.value.current();
2919
3253
  const rows = this.store.parsing.tokens();
2920
- this.store.value.next(deleteDragRow(value, rows, index));
3254
+ const newValue = deleteDragRow(value, rows, action.index);
3255
+ this.store.value.replaceAll(newValue, {
3256
+ source: "drag",
3257
+ recover: this.#recoverAfterDrag(action, rows, newValue)
3258
+ });
2921
3259
  }
2922
- #duplicate(index) {
2923
- const value = this.store.props.value();
2924
- if (value == null || !this.store.props.onChange()) return;
3260
+ #duplicate(action) {
3261
+ const value = this.store.value.current();
2925
3262
  const rows = this.store.parsing.tokens();
2926
- this.store.value.next(duplicateDragRow(value, rows, index));
3263
+ const newValue = duplicateDragRow(value, rows, action.index);
3264
+ this.store.value.replaceAll(newValue, {
3265
+ source: "drag",
3266
+ recover: this.#recoverAfterDrag(action, rows, newValue)
3267
+ });
3268
+ }
3269
+ #recoverAfterDrag(action, previousRows, nextValue) {
3270
+ if (action.type === "add") {
3271
+ const after = previousRows.at(action.afterIndex);
3272
+ return {
3273
+ kind: "caret",
3274
+ rawPosition: after ? after.position.end : nextValue.length
3275
+ };
3276
+ }
3277
+ if (action.type === "duplicate") {
3278
+ const row = previousRows.at(action.index);
3279
+ return row ? {
3280
+ kind: "caret",
3281
+ rawPosition: row.position.end
3282
+ } : void 0;
3283
+ }
3284
+ if (action.type === "delete") {
3285
+ const next = previousRows.at(action.index + 1) ?? (action.index > 0 ? previousRows.at(action.index - 1) : void 0);
3286
+ return next ? {
3287
+ kind: "caret",
3288
+ rawPosition: Math.min(next.position.start, nextValue.length)
3289
+ } : {
3290
+ kind: "caret",
3291
+ rawPosition: 0
3292
+ };
3293
+ }
3294
+ const moved = previousRows.at(action.source);
3295
+ return moved ? {
3296
+ kind: "caret",
3297
+ rawPosition: Math.min(moved.position.start, nextValue.length)
3298
+ } : void 0;
2927
3299
  }
2928
3300
  };
2929
3301
  //#endregion
@@ -2940,178 +3312,52 @@ function getDragTargetIndex(blockIndex, position) {
2940
3312
  }
2941
3313
  //#endregion
2942
3314
  //#region ../../core/src/features/drag/config.ts
2943
- function getAlwaysShowHandle(draggable) {
2944
- return typeof draggable === "object" && !!draggable.alwaysShowHandle;
2945
- }
2946
- //#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;
2958
- }
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;
2971
- }
2972
- return false;
2973
- }
2974
- //#endregion
2975
- //#region ../../core/src/features/keyboard/arrowNav.ts
2976
- function enableArrowNav(store) {
2977
- const container = store.slots.container();
2978
- if (!container) return () => {};
2979
- const scope = effectScope(() => {
2980
- listen(container, "keydown", (e) => {
2981
- 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);
3315
+ function getAlwaysShowHandle(draggable) {
3316
+ return typeof draggable === "object" && !!draggable.alwaysShowHandle;
3035
3317
  }
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;
3318
+ //#endregion
3319
+ //#region ../../core/src/features/keyboard/arrowNav.ts
3320
+ function enableArrowNav(store) {
3321
+ const container = store.dom.container();
3322
+ if (!container) return () => {};
3323
+ const scope = effectScope(() => {
3324
+ listen(container, "keydown", (e) => {
3325
+ if (store.slots.isBlock()) return;
3326
+ if (e.key === KEYBOARD.LEFT) shiftFocus(store, e, "prev");
3327
+ else if (e.key === KEYBOARD.RIGHT) shiftFocus(store, e, "next");
3328
+ selectAllText(store, e);
3329
+ });
3330
+ });
3331
+ return () => scope();
3067
3332
  }
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++;
3112
- }
3333
+ function shiftFocus(store, event, direction) {
3334
+ const location = store.caret.location();
3335
+ if (!location) return false;
3336
+ const token = store.parsing.index().resolveAddress(location.address);
3337
+ if (!token.ok) return false;
3338
+ if (!(token.value.type === "mark" && location.role !== "text")) {
3339
+ const selection = store.dom.readRawSelection();
3340
+ if (!selection.ok || selection.value.range.start !== selection.value.range.end) return false;
3341
+ const atStart = selection.value.range.start <= token.value.position.start;
3342
+ const atEnd = selection.value.range.end >= token.value.position.end;
3343
+ if (direction === "prev" && !atStart) return false;
3344
+ if (direction === "next" && !atEnd) return false;
3345
+ }
3346
+ const path = location.address.path;
3347
+ const siblingIndex = direction === "prev" ? path[path.length - 1] - 1 : path[path.length - 1] + 1;
3348
+ const siblingPath = [...path.slice(0, -1), siblingIndex];
3349
+ const siblingAddress = store.parsing.index().addressFor(siblingPath);
3350
+ if (!siblingAddress) return false;
3351
+ event.preventDefault();
3352
+ if (!store.caret.focus(siblingAddress, direction === "prev" ? "end" : "start").ok) return false;
3353
+ const sibling = store.parsing.index().resolve(siblingPath);
3354
+ if (sibling?.type === "mark") return true;
3355
+ if (direction === "prev") {
3356
+ store.caret.placeAt(sibling?.position.end ?? 0, "before");
3357
+ return true;
3113
3358
  }
3114
- return false;
3359
+ store.caret.placeAt(sibling?.position.start ?? 0, "after");
3360
+ return true;
3115
3361
  }
3116
3362
  //#endregion
3117
3363
  //#region ../../core/src/features/keyboard/blockEdit.ts
@@ -3120,14 +3366,14 @@ function isTextLikeRow(token) {
3120
3366
  return token.descriptor.hasSlot && token.descriptor.segments.length === 1;
3121
3367
  }
3122
3368
  function enableBlockEdit(store) {
3123
- const container = store.slots.container();
3369
+ const container = store.dom.container();
3124
3370
  if (!container) return () => {};
3125
3371
  const scope = effectScope(() => {
3126
3372
  listen(container, "keydown", (e) => {
3127
3373
  if (!store.slots.isBlock()) return;
3128
3374
  if (e.key === KEYBOARD.LEFT || e.key === KEYBOARD.RIGHT) handleBlockArrowLeftRight(store, e, e.key === KEYBOARD.LEFT ? "left" : "right");
3129
3375
  else if (e.key === KEYBOARD.UP || e.key === KEYBOARD.DOWN) handleArrowUpDown(store, e);
3130
- handleDelete$1(store, e);
3376
+ handleDelete(store, e);
3131
3377
  handleEnter(store, e);
3132
3378
  });
3133
3379
  listen(container, "beforeinput", (e) => {
@@ -3138,8 +3384,8 @@ function enableBlockEdit(store) {
3138
3384
  });
3139
3385
  return () => scope();
3140
3386
  }
3141
- function handleDelete$1(store, event) {
3142
- const container = store.slots.container();
3387
+ function handleDelete(store, event) {
3388
+ const container = store.dom.container();
3143
3389
  if (!container) return;
3144
3390
  const blockDivs = htmlChildren(container);
3145
3391
  const blockIndex = blockDivs.findIndex((div) => div === document.activeElement || div.contains(document.activeElement));
@@ -3148,7 +3394,6 @@ function handleDelete$1(store, event) {
3148
3394
  if (blockIndex >= rows.length) return;
3149
3395
  const token = rows[blockIndex];
3150
3396
  const value = store.value.current();
3151
- if (!store.props.onChange()) return;
3152
3397
  if (event.key === KEYBOARD.BACKSPACE) {
3153
3398
  const blockDiv = blockDivs[blockIndex];
3154
3399
  const caretAtStart = Caret.getCaretIndex(blockDiv) === 0;
@@ -3158,12 +3403,12 @@ function handleDelete$1(store, event) {
3158
3403
  if (blockIndex >= rows.length - 1) return value.slice(0, rows[blockIndex - 1].position.end);
3159
3404
  return value.slice(0, rows[blockIndex].position.start) + value.slice(rows[blockIndex + 1].position.start);
3160
3405
  })();
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);
3406
+ const previous = rows.at(Math.max(0, blockIndex - 1));
3407
+ store.value.replaceAll(newValue, {
3408
+ source: "block",
3409
+ recover: {
3410
+ kind: "caret",
3411
+ rawPosition: previous ? previous.position.end : 0
3167
3412
  }
3168
3413
  });
3169
3414
  return;
@@ -3175,13 +3420,11 @@ function handleDelete$1(store, event) {
3175
3420
  event.preventDefault();
3176
3421
  const joinPos = getMergeDragRowJoinPos(rows, blockIndex);
3177
3422
  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);
3423
+ store.value.replaceAll(newValue, {
3424
+ source: "block",
3425
+ recover: {
3426
+ kind: "caret",
3427
+ rawPosition: joinPos
3185
3428
  }
3186
3429
  });
3187
3430
  return;
@@ -3189,8 +3432,7 @@ function handleDelete$1(store, event) {
3189
3432
  event.preventDefault();
3190
3433
  queueMicrotask(() => {
3191
3434
  const target = blockDivs[blockIndex - 1];
3192
- target.focus();
3193
- if (prevToken.type !== "mark") Caret.setCaretToEnd(target);
3435
+ focusRow(store, prevToken, target, "end");
3194
3436
  });
3195
3437
  return;
3196
3438
  }
@@ -3206,13 +3448,11 @@ function handleDelete$1(store, event) {
3206
3448
  event.preventDefault();
3207
3449
  const joinPos = getMergeDragRowJoinPos(rows, blockIndex);
3208
3450
  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);
3451
+ store.value.replaceAll(newValue, {
3452
+ source: "block",
3453
+ recover: {
3454
+ kind: "caret",
3455
+ rawPosition: joinPos
3216
3456
  }
3217
3457
  });
3218
3458
  return;
@@ -3220,8 +3460,7 @@ function handleDelete$1(store, event) {
3220
3460
  event.preventDefault();
3221
3461
  queueMicrotask(() => {
3222
3462
  const target = blockDivs[blockIndex - 1];
3223
- target.focus();
3224
- if (prevToken.type !== "mark") Caret.setCaretToEnd(target);
3463
+ focusRow(store, prevToken, target, "end");
3225
3464
  });
3226
3465
  return;
3227
3466
  }
@@ -3232,13 +3471,11 @@ function handleDelete$1(store, event) {
3232
3471
  event.preventDefault();
3233
3472
  const joinPos = getMergeDragRowJoinPos(rows, blockIndex + 1);
3234
3473
  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);
3474
+ store.value.replaceAll(newValue, {
3475
+ source: "block",
3476
+ recover: {
3477
+ kind: "caret",
3478
+ rawPosition: joinPos
3242
3479
  }
3243
3480
  });
3244
3481
  return;
@@ -3246,8 +3483,7 @@ function handleDelete$1(store, event) {
3246
3483
  event.preventDefault();
3247
3484
  queueMicrotask(() => {
3248
3485
  const target = blockDivs[blockIndex + 1];
3249
- target.focus();
3250
- Caret.trySetIndex(target, 0);
3486
+ focusRow(store, nextToken, target, "start");
3251
3487
  });
3252
3488
  return;
3253
3489
  }
@@ -3256,7 +3492,7 @@ function handleDelete$1(store, event) {
3256
3492
  function handleEnter(store, event) {
3257
3493
  if (event.key !== KEYBOARD.ENTER) return;
3258
3494
  if (event.shiftKey) return;
3259
- const container = store.slots.container();
3495
+ const container = store.dom.container();
3260
3496
  if (!container) return;
3261
3497
  const activeElement = document.activeElement;
3262
3498
  if (!isHtmlElement(activeElement) || !container.contains(activeElement)) return;
@@ -3270,41 +3506,47 @@ function handleEnter(store, event) {
3270
3506
  if (blockIndex === -1) return;
3271
3507
  const rows = store.parsing.tokens();
3272
3508
  const token = rows[blockIndex];
3273
- const blockDiv = blockDivs[blockIndex];
3274
3509
  const value = store.value.current();
3275
- if (!store.props.onChange()) return;
3276
3510
  const newRowContent = createRowContent(store.props.options());
3277
3511
  if (!isTextLikeRow(token)) {
3278
3512
  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
- }
3513
+ store.value.replaceAll(newValue, {
3514
+ source: "block",
3515
+ recover: {
3516
+ kind: "caret",
3517
+ rawPosition: token.position.end + newRowContent.length
3288
3518
  }
3289
3519
  });
3290
3520
  return;
3291
3521
  }
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
- }
3522
+ const raw = store.dom.readRawSelection();
3523
+ const absolutePos = raw.ok ? raw.value.range.start : token.position.end;
3524
+ store.value.replaceRange({
3525
+ start: absolutePos,
3526
+ end: absolutePos
3527
+ }, newRowContent, {
3528
+ source: "block",
3529
+ recover: {
3530
+ kind: "caret",
3531
+ rawPosition: absolutePos + newRowContent.length
3303
3532
  }
3304
3533
  });
3305
3534
  }
3535
+ function focusRow(store, token, row, caret) {
3536
+ if (token.type === "mark") {
3537
+ const path = store.parsing.index().pathFor(token);
3538
+ const address = path ? store.parsing.index().addressFor(path) : void 0;
3539
+ if (address && store.caret.focus(address).ok) return;
3540
+ }
3541
+ row.focus();
3542
+ if (caret === "start") {
3543
+ Caret.trySetIndex(row, 0);
3544
+ return;
3545
+ }
3546
+ Caret.setCaretToEnd(row);
3547
+ }
3306
3548
  function handleBlockArrowLeftRight(store, event, direction) {
3307
- const container = store.slots.container();
3549
+ const container = store.dom.container();
3308
3550
  if (!container) return false;
3309
3551
  const activeElement = document.activeElement;
3310
3552
  if (!isHtmlElement(activeElement) || !container.contains(activeElement)) return false;
@@ -3330,7 +3572,7 @@ function handleBlockArrowLeftRight(store, event, direction) {
3330
3572
  return true;
3331
3573
  }
3332
3574
  function handleArrowUpDown(store, event) {
3333
- const container = store.slots.container();
3575
+ const container = store.dom.container();
3334
3576
  if (!container) return;
3335
3577
  const activeElement = document.activeElement;
3336
3578
  if (!isHtmlElement(activeElement) || !container.contains(activeElement)) return;
@@ -3359,58 +3601,19 @@ function handleArrowUpDown(store, event) {
3359
3601
  }
3360
3602
  }
3361
3603
  function handleBlockBeforeInput(store, event) {
3362
- const container = store.slots.container();
3604
+ const container = store.dom.container();
3363
3605
  if (!container) return;
3364
3606
  const activeElement = document.activeElement;
3365
3607
  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
- };
3608
+ if (htmlChildren(container).findIndex((div) => div === activeElement || div.contains(activeElement)) === -1) return;
3383
3609
  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);
3610
+ case "insertText":
3611
+ replaceBlockRange(store, event, event.data ?? "");
3397
3612
  break;
3398
- }
3399
3613
  case "insertFromPaste":
3400
3614
  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);
3615
+ const c = store.dom.container();
3616
+ replaceBlockRange(store, event, (c ? consumeMarkupPaste(c) : void 0) ?? event.dataTransfer?.getData("text/plain") ?? "");
3414
3617
  break;
3415
3618
  }
3416
3619
  case "deleteContentBackward":
@@ -3418,84 +3621,131 @@ function handleBlockBeforeInput(store, event) {
3418
3621
  case "deleteWordBackward":
3419
3622
  case "deleteWordForward":
3420
3623
  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);
3624
+ case "deleteSoftLineForward":
3625
+ replaceBlockRange(store, event, "");
3431
3626
  break;
3432
- }
3433
3627
  }
3434
3628
  }
3629
+ function replaceBlockRange(store, event, replacement) {
3630
+ const raw = rawRangeFromInputEvent$1(store, event);
3631
+ if (!raw.ok) return;
3632
+ const range = rangeForBlockInput(store, event, raw.value.range);
3633
+ if (!range) return;
3634
+ event.preventDefault();
3635
+ store.value.replaceRange(range, replacement, {
3636
+ source: "block",
3637
+ recover: {
3638
+ kind: "caret",
3639
+ rawPosition: range.start + replacement.length
3640
+ }
3641
+ });
3642
+ }
3643
+ function rawRangeFromInputEvent$1(store, event) {
3644
+ const ranges = event.getTargetRanges();
3645
+ if (ranges.length === 0) return store.dom.readRawSelection();
3646
+ return rawRangeFromTargetRange$1(store, ranges[0]);
3647
+ }
3648
+ function rawRangeFromTargetRange$1(store, range) {
3649
+ const start = store.dom.rawPositionFromBoundary(range.startContainer, range.startOffset, "after");
3650
+ const end = store.dom.rawPositionFromBoundary(range.endContainer, range.endOffset, "before");
3651
+ if (!start.ok) return {
3652
+ ok: false,
3653
+ reason: rawSelectionReason$1(start)
3654
+ };
3655
+ if (!end.ok) return {
3656
+ ok: false,
3657
+ reason: rawSelectionReason$1(end)
3658
+ };
3659
+ return {
3660
+ ok: true,
3661
+ value: { range: start.value <= end.value ? {
3662
+ start: start.value,
3663
+ end: end.value
3664
+ } : {
3665
+ start: end.value,
3666
+ end: start.value
3667
+ } }
3668
+ };
3669
+ }
3670
+ function rawSelectionReason$1(result) {
3671
+ if (result.ok) return "invalidBoundary";
3672
+ if (result.reason === "composing") return "invalidBoundary";
3673
+ return result.reason;
3674
+ }
3675
+ function rangeForBlockInput(store, event, range) {
3676
+ if (!event.inputType.startsWith("delete")) return range;
3677
+ if (range.start !== range.end) return range;
3678
+ if (event.inputType.endsWith("Backward") && range.start > 0) return {
3679
+ start: range.start - 1,
3680
+ end: range.start
3681
+ };
3682
+ if (event.inputType.endsWith("Forward") && range.end < store.value.current().length) return {
3683
+ start: range.start,
3684
+ end: range.end + 1
3685
+ };
3686
+ }
3435
3687
  //#endregion
3436
3688
  //#region ../../core/src/features/keyboard/input.ts
3437
3689
  function enableInput(store) {
3438
- const container = store.slots.container();
3690
+ const container = store.dom.container();
3439
3691
  if (!container) return () => {};
3692
+ let compositionRange;
3440
3693
  const scope = effectScope(() => {
3441
- listen(container, "keydown", (e) => {
3442
- if (!store.slots.isBlock()) handleDelete(store, e);
3443
- });
3444
3694
  listen(container, "paste", (e) => {
3445
- const c = store.slots.container();
3695
+ const c = store.dom.container();
3446
3696
  if (c) captureMarkupPaste(e, c);
3447
3697
  handlePaste(store, e);
3448
3698
  });
3699
+ listen(container, "compositionstart", () => {
3700
+ const selection = store.dom.readRawSelection();
3701
+ compositionRange = selection.ok ? selection.value.range : void 0;
3702
+ store.dom.compositionStarted();
3703
+ });
3704
+ listen(container, "compositionend", (e) => {
3705
+ const range = compositionRange;
3706
+ compositionRange = void 0;
3707
+ store.dom.compositionEnded();
3708
+ if (store.slots.isBlock()) return;
3709
+ if (!range) return;
3710
+ const data = e.data;
3711
+ store.value.replaceRange(range, data, {
3712
+ source: "input",
3713
+ recover: {
3714
+ kind: "caret",
3715
+ rawPosition: range.start + data.length
3716
+ }
3717
+ });
3718
+ });
3449
3719
  listen(container, "beforeinput", (e) => {
3450
3720
  handleBeforeInput(store, e);
3451
3721
  }, true);
3722
+ listen(container, "keydown", (e) => {
3723
+ handleDeleteKey(store, e);
3724
+ });
3452
3725
  });
3453
3726
  return () => scope();
3454
3727
  }
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
- }
3728
+ function handleDeleteKey(store, event) {
3729
+ if (store.slots.isBlock()) return;
3730
+ if (event.key !== KEYBOARD.BACKSPACE && event.key !== KEYBOARD.DELETE) return;
3731
+ if (store.caret.selecting() === "all" && isFullSelection(store)) {
3463
3732
  event.preventDefault();
3464
- deleteMark("self", store);
3733
+ replaceAllContentWith(store, "");
3465
3734
  return;
3466
3735
  }
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;
3736
+ if (store.caret.selecting() === "all") store.caret.selecting(void 0);
3737
+ const raw = store.dom.readRawSelection();
3738
+ if (!raw.ok) return;
3739
+ const range = rangeForDelete(store, event.key === KEYBOARD.BACKSPACE ? "deleteContentBackward" : "deleteContentForward", raw.value.range);
3740
+ if (!range) return;
3741
+ event.preventDefault();
3742
+ store.value.replaceRange(range, "", {
3743
+ source: "input",
3744
+ recover: {
3745
+ kind: "caret",
3746
+ rawPosition: range.start
3497
3747
  }
3498
- }
3748
+ });
3499
3749
  }
3500
3750
  function handleBeforeInput(store, event) {
3501
3751
  const selecting = store.caret.selecting();
@@ -3510,101 +3760,87 @@ function handleBeforeInput(store, event) {
3510
3760
  }
3511
3761
  if (selecting === "all") store.caret.selecting(void 0);
3512
3762
  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;
3763
+ const raw = rawRangeFromInputEvent(store, event);
3764
+ if (!raw.ok) return;
3765
+ const replacement = replacementForInput(store, event);
3766
+ if (replacement === void 0) return;
3767
+ const range = rangeForInput(store, event, raw.value.range);
3768
+ if (!range) return;
3523
3769
  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
3770
+ store.value.replaceRange(range, replacement, {
3771
+ source: "input",
3772
+ recover: {
3773
+ kind: "caret",
3774
+ rawPosition: range.start + replacement.length
3775
+ }
3552
3776
  });
3553
- return true;
3554
3777
  }
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;
3778
+ function rawRangeFromInputEvent(store, event) {
3779
+ const ranges = getTargetRanges(event);
3780
+ if (ranges.length === 0) return store.dom.readRawSelection();
3781
+ return rawRangeFromTargetRange(store, ranges[0]);
3782
+ }
3783
+ function rawRangeFromTargetRange(store, range) {
3784
+ const start = store.dom.rawPositionFromBoundary(range.startContainer, range.startOffset, "after");
3785
+ const end = store.dom.rawPositionFromBoundary(range.endContainer, range.endOffset, "before");
3786
+ if (!start.ok) return {
3787
+ ok: false,
3788
+ reason: rawSelectionReason(start)
3789
+ };
3790
+ if (!end.ok) return {
3791
+ ok: false,
3792
+ reason: rawSelectionReason(end)
3793
+ };
3794
+ return {
3795
+ ok: true,
3796
+ value: { range: start.value <= end.value ? {
3797
+ start: start.value,
3798
+ end: end.value
3799
+ } : {
3800
+ start: end.value,
3801
+ end: start.value
3802
+ } }
3803
+ };
3804
+ }
3805
+ function rawSelectionReason(result) {
3806
+ if (result.ok) return "invalidBoundary";
3807
+ if (result.reason === "composing") return "invalidBoundary";
3808
+ return result.reason;
3809
+ }
3810
+ function getTargetRanges(event) {
3811
+ return event.getTargetRanges();
3812
+ }
3813
+ function replacementForInput(store, event) {
3814
+ if (event.inputType.startsWith("delete")) return "";
3815
+ if (event.inputType === "insertFromPaste" || event.inputType === "insertReplacementText") {
3816
+ const container = store.dom.container();
3817
+ return (container ? consumeMarkupPaste(container) : void 0) ?? event.dataTransfer?.getData("text/plain") ?? event.data ?? "";
3818
+ }
3819
+ if (event.inputType === "insertText") return event.data ?? "";
3820
+ }
3821
+ function rangeForInput(store, event, range) {
3822
+ if (!event.inputType.startsWith("delete")) return range;
3823
+ return rangeForDelete(store, event.inputType, range);
3824
+ }
3825
+ function rangeForDelete(store, inputType, range) {
3826
+ if (range.start !== range.end) return range;
3827
+ const adjacentMark = adjacentMarkRange(store.parsing.tokens(), range.start, inputType.endsWith("Backward"));
3828
+ if (adjacentMark) return adjacentMark;
3829
+ if (inputType.endsWith("Backward") && range.start > 0) return {
3830
+ start: range.start - 1,
3831
+ end: range.start
3832
+ };
3833
+ if (inputType.endsWith("Forward") && range.end < store.value.current().length) return {
3834
+ start: range.start,
3835
+ end: range.end + 1
3836
+ };
3837
+ }
3838
+ function adjacentMarkRange(tokens, position, backward) {
3839
+ for (const token of tokens) {
3840
+ const nested = token.type === "mark" ? adjacentMarkRange(token.children, position, backward) : void 0;
3841
+ if (nested) return nested;
3842
+ if (token.type === "mark" && (backward ? token.position.end === position : token.position.start === position)) return token.position;
3604
3843
  }
3605
- focus.content = newContent;
3606
- focus.caret = newCaret;
3607
- return true;
3608
3844
  }
3609
3845
  function handlePaste(store, event) {
3610
3846
  const selecting = store.caret.selecting();
@@ -3613,31 +3849,16 @@ function handlePaste(store, event) {
3613
3849
  return;
3614
3850
  }
3615
3851
  event.preventDefault();
3616
- const c = store.slots.container();
3852
+ const c = store.dom.container();
3617
3853
  replaceAllContentWith(store, (c ? consumeMarkupPaste(c) : void 0) ?? event.clipboardData?.getData("text/plain") ?? "");
3618
3854
  }
3619
3855
  function replaceAllContentWith(store, newContent) {
3620
- store.nodes.focus.target = null;
3621
3856
  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();
3857
+ store.value.replaceAll(newContent, {
3858
+ source: "input",
3859
+ recover: {
3860
+ kind: "caret",
3861
+ rawPosition: newContent.length
3641
3862
  }
3642
3863
  });
3643
3864
  }
@@ -3664,8 +3885,7 @@ var KeyboardFeature = class {
3664
3885
  //#endregion
3665
3886
  //#region ../../core/src/features/lifecycle/LifecycleFeature.ts
3666
3887
  var LifecycleFeature = class {
3667
- constructor(_store) {
3668
- this._store = _store;
3888
+ constructor() {
3669
3889
  this.mounted = event();
3670
3890
  this.unmounted = event();
3671
3891
  this.rendered = event();
@@ -3674,70 +3894,80 @@ var LifecycleFeature = class {
3674
3894
  disable() {}
3675
3895
  };
3676
3896
  //#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();
3897
+ //#region ../../core/src/features/mark/MarkController.ts
3898
+ var MarkController = class MarkController {
3899
+ #shape;
3900
+ constructor(store, address, snapshot, shape) {
3901
+ this.store = store;
3902
+ this.address = address;
3903
+ this.snapshot = snapshot;
3904
+ this.#shape = shape;
3905
+ }
3906
+ static fromToken(store, token) {
3907
+ const index = store.parsing.index();
3908
+ const path = index.pathFor(token);
3909
+ if (!path) throw new Error("Cannot create MarkController for unindexed token");
3910
+ const address = index.addressFor(path);
3911
+ if (!address) throw new Error("Cannot create MarkController for unresolved token path");
3912
+ return new MarkController(store, address, {
3913
+ value: token.value,
3914
+ meta: token.meta,
3915
+ slot: token.slot?.content,
3916
+ readOnly: store.props.readOnly()
3917
+ }, snapshotTokenShape(token));
3706
3918
  }
3707
3919
  get value() {
3708
- return this.#token.value;
3709
- }
3710
- set value(v) {
3711
- this.#token.value = v ?? "";
3712
- this.#emitChange();
3920
+ return this.snapshot.value;
3713
3921
  }
3714
3922
  get meta() {
3715
- return this.#token.meta;
3716
- }
3717
- set meta(v) {
3718
- this.#token.meta = v;
3719
- this.#emitChange();
3923
+ return this.snapshot.meta;
3720
3924
  }
3721
3925
  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;
3926
+ return this.snapshot.slot;
3729
3927
  }
3730
- get hasChildren() {
3731
- return this.#token.children.some((child) => child.type === "mark");
3732
- }
3733
- get parent() {
3734
- return this.#tokenInfo?.parent;
3928
+ get readOnly() {
3929
+ return this.snapshot.readOnly;
3930
+ }
3931
+ remove() {
3932
+ const resolved = this.#resolve();
3933
+ if (!resolved.ok) return resolved;
3934
+ return this.store.value.replaceRange(resolved.value.position, "", { source: "mark" });
3935
+ }
3936
+ update(patch) {
3937
+ const resolved = this.#resolve();
3938
+ if (!resolved.ok) return resolved;
3939
+ const token = resolved.value;
3940
+ const value = patch.value ?? token.value;
3941
+ const meta = patch.meta?.kind === "clear" ? void 0 : patch.meta?.kind === "set" ? patch.meta.value : token.meta;
3942
+ const slot = patch.slot?.kind === "clear" ? void 0 : patch.slot?.kind === "set" ? patch.slot.value : token.slot?.content;
3943
+ const serialized = this.#serialize(token, {
3944
+ value,
3945
+ meta,
3946
+ slot
3947
+ });
3948
+ return this.store.value.replaceRange(token.position, serialized, { source: "mark" });
3735
3949
  }
3736
- get tokens() {
3737
- return this.#token.children;
3950
+ #serialize(token, fields) {
3951
+ return annotate(token.descriptor.markup, {
3952
+ value: fields.value,
3953
+ meta: token.descriptor.gapTypes.includes("meta") ? fields.meta ?? "" : void 0,
3954
+ slot: token.descriptor.hasSlot ? fields.slot ?? "" : void 0
3955
+ });
3738
3956
  }
3739
- #emitChange() {
3740
- this.#store.value.change();
3957
+ #resolve() {
3958
+ if (this.store.props.readOnly()) return {
3959
+ ok: false,
3960
+ reason: "readOnly"
3961
+ };
3962
+ const resolved = this.store.parsing.index().resolveAddress(this.address, this.#shape);
3963
+ if (!resolved.ok || resolved.value.type !== "mark") return {
3964
+ ok: false,
3965
+ reason: "stale"
3966
+ };
3967
+ return {
3968
+ ok: true,
3969
+ value: resolved.value
3970
+ };
3741
3971
  }
3742
3972
  };
3743
3973
  //#endregion
@@ -3822,7 +4052,6 @@ function buildContainerProps(isDraggableBlock, readOnly, className, style, slotP
3822
4052
  var SlotsFeature = class {
3823
4053
  constructor(_store) {
3824
4054
  this._store = _store;
3825
- this.container = signal(null);
3826
4055
  this.isBlock = computed(() => this._store.props.layout() === "block");
3827
4056
  this.isDraggable = computed(() => !!this._store.props.draggable());
3828
4057
  this.containerComponent = computed(() => resolveSlot("container", this._store.props.slots()));
@@ -3838,7 +4067,6 @@ var SlotsFeature = class {
3838
4067
  //#endregion
3839
4068
  //#region ../../core/src/features/mark/MarkFeature.ts
3840
4069
  var MarkFeature = class {
3841
- #scope;
3842
4070
  constructor(_store) {
3843
4071
  this._store = _store;
3844
4072
  this.enabled = computed(() => {
@@ -3851,25 +4079,9 @@ var MarkFeature = class {
3851
4079
  const Span = this._store.props.Span();
3852
4080
  return (token) => resolveMarkSlot(token, options, Mark, Span);
3853
4081
  });
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
4082
  }
4083
+ enable() {}
4084
+ disable() {}
3873
4085
  };
3874
4086
  //#endregion
3875
4087
  //#region ../../core/src/features/overlay/filterSuggestions.ts
@@ -3888,8 +4100,8 @@ function createMarkFromOverlay(match, value, meta) {
3888
4100
  meta,
3889
4101
  content: "",
3890
4102
  position: {
3891
- start: match.index,
3892
- end: match.index + match.span.length
4103
+ start: match.range.start,
4104
+ end: match.range.end
3893
4105
  },
3894
4106
  descriptor: {
3895
4107
  markup,
@@ -3920,9 +4132,37 @@ var OverlayFeature = class {
3920
4132
  this.close = event();
3921
4133
  }
3922
4134
  #probeTrigger() {
3923
- const match = TriggerFinder.find(this._store.props.options(), (option) => option.overlay?.trigger);
4135
+ const match = TriggerFinder.find(this._store.props.options(), (option) => option.overlay?.trigger, this._store) ?? this.#probeTriggerFromRecovery();
3924
4136
  this.match(match);
3925
4137
  }
4138
+ #probeTriggerFromRecovery() {
4139
+ const recovery = this._store.caret.recovery();
4140
+ if (recovery?.kind !== "caret") return;
4141
+ const value = this._store.value.current();
4142
+ const cursor = recovery.rawPosition;
4143
+ const left = value.slice(0, cursor);
4144
+ const rightWord = value.slice(cursor).match(/^\w*/)?.[0] ?? "";
4145
+ for (const option of this._store.props.options()) {
4146
+ const trigger = option.overlay?.trigger;
4147
+ if (!trigger) continue;
4148
+ const match = left.match(new RegExp(`${escape(trigger)}(\\w*)$`));
4149
+ if (!match) continue;
4150
+ const [sourceLeft, wordLeft] = match;
4151
+ const source = sourceLeft + rightWord;
4152
+ const start = cursor - sourceLeft.length;
4153
+ return {
4154
+ value: wordLeft + rightWord,
4155
+ source,
4156
+ range: {
4157
+ start,
4158
+ end: start + source.length
4159
+ },
4160
+ span: value,
4161
+ node: window.getSelection()?.anchorNode ?? this._store.dom.container() ?? document.body,
4162
+ option
4163
+ };
4164
+ }
4165
+ }
3926
4166
  enable() {
3927
4167
  if (this.#scope) return;
3928
4168
  this.#scope = effectScope(() => {
@@ -3936,55 +4176,40 @@ var OverlayFeature = class {
3936
4176
  });
3937
4177
  alienEffect(() => {
3938
4178
  if (this.match()) {
3939
- this._store.nodes.input.target = this._store.nodes.focus.target;
3940
4179
  listen(window, "keydown", (e) => {
3941
4180
  if (e.key === KEYBOARD.ESC) this.close();
3942
4181
  });
3943
4182
  listen(document, "click", (e) => {
3944
4183
  const target = e.target instanceof HTMLElement ? e.target : null;
3945
4184
  if (this.element()?.contains(target)) return;
3946
- if (this._store.slots.container()?.contains(target)) return;
4185
+ if (this._store.dom.container()?.contains(target)) return;
3947
4186
  this.close();
3948
4187
  }, true);
3949
4188
  }
3950
4189
  });
3951
4190
  const selectionChangeHandler = () => {
3952
- if (!this._store.slots.container()?.contains(document.activeElement)) return;
4191
+ if (!this._store.dom.container()?.contains(document.activeElement)) return;
3953
4192
  const showOverlayOn = this._store.props.showOverlayOn();
3954
4193
  const type = "selectionChange";
3955
4194
  if (showOverlayOn === type || Array.isArray(showOverlayOn) && showOverlayOn.includes(type)) this.#probeTrigger();
3956
4195
  };
3957
4196
  listen(document, "selectionchange", selectionChangeHandler);
3958
4197
  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;
4198
+ const { mark, match: { option, range } } = overlayEvent;
3962
4199
  const markup = option.markup;
3963
4200
  if (!markup) return;
3964
4201
  const annotation = mark.type === "mark" ? annotate(markup, {
3965
4202
  value: mark.value,
3966
4203
  meta: mark.meta
3967
4204
  }) : 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
4205
+ this._store.value.replaceRange(range, annotation, {
4206
+ source: "overlay",
4207
+ recover: {
4208
+ kind: "caret",
4209
+ rawPosition: range.start + annotation.length
4210
+ }
3977
4211
  });
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
- }
4212
+ this.match(void 0);
3988
4213
  });
3989
4214
  });
3990
4215
  }
@@ -4057,45 +4282,47 @@ var PropsFeature = class {
4057
4282
  }
4058
4283
  };
4059
4284
  //#endregion
4285
+ //#region ../../core/src/features/value/ControlledEcho.ts
4286
+ var ControlledEcho = class {
4287
+ #pending;
4288
+ propose(candidate, recovery) {
4289
+ this.#pending = {
4290
+ candidate,
4291
+ recovery
4292
+ };
4293
+ }
4294
+ onEcho(value) {
4295
+ const pending = this.#pending;
4296
+ if (!pending) return void 0;
4297
+ this.#pending = void 0;
4298
+ return pending.candidate === value ? pending.recovery : void 0;
4299
+ }
4300
+ supersede() {
4301
+ this.#pending = void 0;
4302
+ }
4303
+ };
4304
+ //#endregion
4060
4305
  //#region ../../core/src/features/value/ValueFeature.ts
4061
4306
  var ValueFeature = class {
4307
+ #controlledEcho = new ControlledEcho();
4062
4308
  #scope;
4063
4309
  constructor(_store) {
4064
4310
  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() ?? "");
4311
+ this.current = signal("");
4312
+ this.isControlledMode = computed(() => this._store.props.value() !== void 0);
4068
4313
  this.change = event();
4069
4314
  }
4070
4315
  enable() {
4071
4316
  if (this.#scope) return;
4317
+ this.#commitAccepted(this._store.props.value() ?? this._store.props.defaultValue() ?? "");
4072
4318
  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);
4319
+ watch(this._store.props.value, (value) => {
4320
+ if (value === void 0) return;
4321
+ if (value === this.current()) return;
4322
+ const recovery = this.#controlledEcho.onEcho(value);
4323
+ this.#commitAccepted(value);
4324
+ if (recovery) this._store.caret.recovery(recovery);
4325
+ this.change();
4099
4326
  });
4100
4327
  });
4101
4328
  }
@@ -4103,6 +4330,52 @@ var ValueFeature = class {
4103
4330
  this.#scope?.();
4104
4331
  this.#scope = void 0;
4105
4332
  }
4333
+ replaceRange(range, replacement, options) {
4334
+ const current = this.current();
4335
+ if (this._store.props.readOnly()) return {
4336
+ ok: false,
4337
+ reason: "readOnly"
4338
+ };
4339
+ if (range.start < 0 || range.end < range.start || range.end > current.length) return {
4340
+ ok: false,
4341
+ reason: "invalidRange"
4342
+ };
4343
+ const candidate = current.slice(0, range.start) + replacement + current.slice(range.end);
4344
+ return this.#commitCandidate(candidate, options?.recover);
4345
+ }
4346
+ replaceAll(next, options) {
4347
+ return this.replaceRange({
4348
+ start: 0,
4349
+ end: this.current().length
4350
+ }, next, options);
4351
+ }
4352
+ #commitCandidate(candidate, recovery) {
4353
+ if (this.isControlledMode()) {
4354
+ this.#controlledEcho.propose(candidate, recovery);
4355
+ this._store.props.onChange()?.(candidate);
4356
+ return {
4357
+ ok: true,
4358
+ accepted: "pendingControlledEcho",
4359
+ value: candidate
4360
+ };
4361
+ }
4362
+ this._store.props.onChange()?.(candidate);
4363
+ this.#commitAccepted(candidate);
4364
+ this._store.caret.recovery(recovery);
4365
+ this.change();
4366
+ return {
4367
+ ok: true,
4368
+ accepted: "immediate",
4369
+ value: candidate
4370
+ };
4371
+ }
4372
+ #commitAccepted(value) {
4373
+ const tokens = this._store.parsing.parseValue(value);
4374
+ batch(() => {
4375
+ this._store.parsing.acceptTokens(tokens);
4376
+ this.current(value);
4377
+ });
4378
+ }
4106
4379
  };
4107
4380
  //#endregion
4108
4381
  //#region ../../core/src/shared/utils/menuUtils.ts
@@ -4281,13 +4554,9 @@ var Store = class {
4281
4554
  constructor() {
4282
4555
  this.key = new KeyGenerator();
4283
4556
  this.blocks = new BlockRegistry();
4284
- this.nodes = {
4285
- focus: new NodeProxy(void 0, this),
4286
- input: new NodeProxy(void 0, this)
4287
- };
4288
4557
  this.props = new PropsFeature(this);
4289
4558
  this.handler = new MarkputHandler(this);
4290
- this.lifecycle = new LifecycleFeature(this);
4559
+ this.lifecycle = new LifecycleFeature();
4291
4560
  this.value = new ValueFeature(this);
4292
4561
  this.mark = new MarkFeature(this);
4293
4562
  this.overlay = new OverlayFeature(this);
@@ -4379,14 +4648,24 @@ const Popup = ({ ref, style, children }) => {
4379
4648
  //#endregion
4380
4649
  //#region src/components/BlockMenu.tsx
4381
4650
  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
- }));
4651
+ const { blockStore, menuOpen, menuPosition, dom, index } = useMarkput((s) => {
4652
+ const blockStore = s.blocks.get(token);
4653
+ return {
4654
+ blockStore,
4655
+ menuOpen: blockStore.state.menuOpen,
4656
+ menuPosition: blockStore.state.menuPosition,
4657
+ dom: s.dom,
4658
+ index: s.parsing.index
4659
+ };
4660
+ });
4661
+ const path = index.pathFor(token);
4662
+ const controlRef = useMemo(() => path ? dom.controlFor(path) : void 0, [dom, path]);
4387
4663
  if (!menuOpen) return null;
4388
4664
  return /* @__PURE__ */ jsx(Popup, {
4389
- ref: (el) => blockStore.attachMenu(el),
4665
+ ref: (el) => {
4666
+ blockStore.attachMenu(el);
4667
+ controlRef?.(el);
4668
+ },
4390
4669
  style: {
4391
4670
  top: menuPosition.top,
4392
4671
  left: menuPosition.left
@@ -4412,20 +4691,30 @@ BlockMenu.displayName = "BlockMenu";
4412
4691
  //#region src/components/DragHandle.tsx
4413
4692
  const iconGrip = `${styles$1.Icon} ${styles$1.IconGrip}`;
4414
4693
  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
- }));
4694
+ const { blockStore, action, readOnly, draggable, isDragging, isHovered, dom, index } = useMarkput((s) => {
4695
+ const blockStore = s.blocks.get(token);
4696
+ return {
4697
+ blockStore,
4698
+ action: s.drag.action,
4699
+ readOnly: s.props.readOnly,
4700
+ draggable: s.props.draggable,
4701
+ isDragging: blockStore.state.isDragging,
4702
+ isHovered: blockStore.state.isHovered,
4703
+ dom: s.dom,
4704
+ index: s.parsing.index
4705
+ };
4706
+ });
4423
4707
  const alwaysShowHandle = useMemo(() => getAlwaysShowHandle(draggable), [draggable]);
4708
+ const path = index.pathFor(token);
4709
+ const controlRef = useMemo(() => path ? dom.controlFor(path) : void 0, [dom, path]);
4424
4710
  if (readOnly) return null;
4425
4711
  return /* @__PURE__ */ jsx("div", {
4712
+ ref: controlRef,
4426
4713
  className: cx(styles$1.SidePanel, alwaysShowHandle ? styles$1.SidePanelAlways : isHovered && !isDragging && styles$1.SidePanelVisible),
4427
4714
  children: /* @__PURE__ */ jsx("button", {
4428
- ref: (el) => blockStore.attachGrip(el, blockIndex, { action }),
4715
+ ref: (el) => {
4716
+ blockStore.attachGrip(el, blockIndex, { action });
4717
+ },
4429
4718
  type: "button",
4430
4719
  draggable: true,
4431
4720
  className: cx(styles$1.GripButton, isDragging && styles$1.GripButtonDragging),
@@ -4438,8 +4727,16 @@ DragHandle.displayName = "DragHandle";
4438
4727
  //#endregion
4439
4728
  //#region src/components/DropIndicator.tsx
4440
4729
  const DropIndicator = memo(({ token, position }) => {
4441
- if (useMarkput((s) => s.blocks.get(token).state.dropPosition) !== position) return null;
4730
+ const dropPosition = useMarkput((s) => s.blocks.get(token).state.dropPosition);
4731
+ const { dom, index } = useMarkput((s) => ({
4732
+ dom: s.dom,
4733
+ index: s.parsing.index
4734
+ }));
4735
+ const path = index.pathFor(token);
4736
+ const controlRef = useMemo(() => path ? dom.controlFor(path) : void 0, [dom, path]);
4737
+ if (dropPosition !== position) return null;
4442
4738
  return /* @__PURE__ */ jsx("div", {
4739
+ ref: controlRef,
4443
4740
  className: styles$1.DropIndicator,
4444
4741
  style: position === "before" ? { top: -1 } : { bottom: -1 }
4445
4742
  });
@@ -4449,25 +4746,50 @@ DropIndicator.displayName = "DropIndicator";
4449
4746
  //#region src/lib/providers/TokenContext.ts
4450
4747
  const TokenContext = createContext(void 0);
4451
4748
  TokenContext.displayName = "TokenProvider";
4452
- function useToken() {
4749
+ function useTokenContext() {
4453
4750
  const value = useContext(TokenContext);
4454
4751
  if (value === void 0) throw new Error("Token not found. Make sure to wrap component in TokenContext.Provider.");
4455
4752
  return value;
4456
4753
  }
4457
4754
  //#endregion
4755
+ //#region src/components/TokenChildren.tsx
4756
+ const sequenceHostStyle = { display: "contents" };
4757
+ const TokenChildren = memo(({ ownerPath, children }) => {
4758
+ const { dom } = useMarkput((s) => ({ dom: s.dom }));
4759
+ return /* @__PURE__ */ jsx("span", {
4760
+ ref: useMemo(() => dom.childrenFor(ownerPath), [dom, ownerPath]),
4761
+ style: sequenceHostStyle,
4762
+ children
4763
+ });
4764
+ });
4765
+ TokenChildren.displayName = "TokenChildren";
4766
+ //#endregion
4458
4767
  //#region src/components/Token.tsx
4459
- const Token = memo(({ mark }) => {
4460
- const { resolveMarkSlot, key } = useMarkput((s) => ({
4768
+ const Token = memo(({ token }) => {
4769
+ const { resolveMarkSlot, key, index, store } = useMarkput((s) => ({
4461
4770
  resolveMarkSlot: s.mark.slot,
4462
- key: s.key
4771
+ key: s.key,
4772
+ index: s.parsing.index,
4773
+ store: s
4463
4774
  }));
4464
- const [Component, props] = resolveMarkSlot(mark);
4775
+ const path = index.pathFor(token);
4776
+ const address = path ? index.addressFor(path) : void 0;
4777
+ if (!path || !address) return null;
4778
+ const [Component, props] = resolveMarkSlot(token);
4779
+ const children = token.type === "mark" && token.children.length > 0 ? /* @__PURE__ */ jsx(TokenChildren, {
4780
+ ownerPath: path,
4781
+ children: token.children.map((child) => /* @__PURE__ */ jsx(Token, { token: child }, key.get(child)))
4782
+ }) : void 0;
4465
4783
  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
- })
4784
+ value: {
4785
+ store,
4786
+ token,
4787
+ address
4788
+ },
4789
+ children: children ? /* @__PURE__ */ jsx(Component, {
4790
+ ...props,
4791
+ children
4792
+ }) : /* @__PURE__ */ jsx(Component, { ...props })
4471
4793
  });
4472
4794
  });
4473
4795
  Token.displayName = "Token";
@@ -4483,8 +4805,11 @@ const Block = memo(({ token }) => {
4483
4805
  tokens: s.parsing.tokens
4484
4806
  }));
4485
4807
  const blockIndex = tokens.indexOf(token);
4808
+ const setBlockRef = (el) => {
4809
+ blockStore.attachContainer(el, blockIndex, { action });
4810
+ };
4486
4811
  return /* @__PURE__ */ jsxs(Component, {
4487
- ref: (el) => blockStore.attachContainer(el, blockIndex, { action }),
4812
+ ref: setBlockRef,
4488
4813
  "data-testid": "block",
4489
4814
  ...slotProps,
4490
4815
  className: cx(styles$1.Block, slotProps?.className),
@@ -4501,7 +4826,7 @@ const Block = memo(({ token }) => {
4501
4826
  token,
4502
4827
  blockIndex
4503
4828
  }),
4504
- /* @__PURE__ */ jsx(Token, { mark: token }),
4829
+ /* @__PURE__ */ jsx(Token, { token }),
4505
4830
  /* @__PURE__ */ jsx(DropIndicator, {
4506
4831
  token,
4507
4832
  position: "after"
@@ -4514,38 +4839,32 @@ Block.displayName = "Block";
4514
4839
  //#endregion
4515
4840
  //#region src/components/Container.tsx
4516
4841
  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) => ({
4842
+ const { dom, lifecycle, isBlock, tokens, key, Component, props } = useMarkput((s) => ({
4843
+ dom: s.dom,
4844
+ lifecycle: s.lifecycle,
4521
4845
  isBlock: s.slots.isBlock,
4522
4846
  tokens: s.parsing.tokens,
4523
4847
  key: s.key,
4524
- lifecycleEmit: s.lifecycle,
4525
4848
  Component: s.slots.containerComponent,
4526
4849
  props: s.slots.containerProps
4527
4850
  }));
4528
4851
  useLayoutEffect(() => {
4529
- lifecycleEmit.rendered();
4530
- }, [tokens, lifecycleEmit]);
4852
+ lifecycle.rendered();
4853
+ });
4531
4854
  return /* @__PURE__ */ jsx(Component, {
4532
- ref: store.slots.container,
4855
+ ref: dom.container,
4533
4856
  ...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)))
4857
+ 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
4858
  });
4536
4859
  });
4537
4860
  Container.displayName = "Container";
4538
4861
  //#endregion
4539
4862
  //#region src/lib/hooks/useOverlay.tsx
4540
4863
  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;
4864
+ const { match, overlay } = useMarkput((s) => ({
4865
+ match: s.overlay.match,
4866
+ overlay: s.overlay
4867
+ }));
4549
4868
  const style = useMemo(() => {
4550
4869
  if (!match) return {
4551
4870
  left: 0,
@@ -4553,36 +4872,34 @@ function useOverlay() {
4553
4872
  };
4554
4873
  return Caret.getAbsolutePosition();
4555
4874
  }, [match]);
4556
- const close = useCallback(() => store.overlay.close(), [store]);
4875
+ const close = useCallback(() => overlay.close(), [overlay]);
4557
4876
  return {
4558
4877
  match,
4559
4878
  style,
4560
4879
  select: useCallback((value) => {
4561
4880
  if (!match) return;
4562
4881
  const mark = createMarkFromOverlay(match, value.value, value.meta);
4563
- store.overlay.select({
4882
+ overlay.select({
4564
4883
  mark,
4565
4884
  match
4566
4885
  });
4567
- store.overlay.close();
4568
- }, [match, store]),
4886
+ overlay.close();
4887
+ }, [match, overlay]),
4569
4888
  close,
4570
4889
  ref: useMemo(() => ({
4571
4890
  get current() {
4572
- return store.overlay.element();
4891
+ return overlay.element();
4573
4892
  },
4574
4893
  set current(v) {
4575
- store.overlay.element(v);
4894
+ overlay.element(v);
4576
4895
  }
4577
- }), [store])
4896
+ }), [overlay])
4578
4897
  };
4579
4898
  }
4580
4899
  //#endregion
4581
4900
  //#region src/components/Suggestions/Suggestions.tsx
4582
4901
  const Suggestions = () => {
4583
- const storeCtx = useContext(StoreContext);
4584
- if (!storeCtx) throw new Error("Store not found");
4585
- const store = storeCtx;
4902
+ const container = useMarkput((s) => s.dom.container);
4586
4903
  const { match, select, style, ref } = useOverlay();
4587
4904
  const [active, setActive] = useState(NaN);
4588
4905
  const data = match?.option.overlay?.data ?? [];
@@ -4593,7 +4910,6 @@ const Suggestions = () => {
4593
4910
  const filteredRef = useRef(filtered);
4594
4911
  filteredRef.current = filtered;
4595
4912
  useEffect(() => {
4596
- const container = store.slots.container();
4597
4913
  if (!container) return;
4598
4914
  const handler = (event) => {
4599
4915
  const result = navigateSuggestions(event.key, activeRef.current, length);
@@ -4617,7 +4933,11 @@ const Suggestions = () => {
4617
4933
  };
4618
4934
  container.addEventListener("keydown", handler);
4619
4935
  return () => container.removeEventListener("keydown", handler);
4620
- }, [length, select]);
4936
+ }, [
4937
+ container,
4938
+ length,
4939
+ select
4940
+ ]);
4621
4941
  if (!filtered.length) return null;
4622
4942
  return /* @__PURE__ */ jsx(Popup, {
4623
4943
  ref,
@@ -4649,8 +4969,14 @@ OverlayRenderer.displayName = "OverlayRenderer";
4649
4969
  //#endregion
4650
4970
  //#region src/components/MarkedInput.tsx
4651
4971
  function MarkedInput(props) {
4652
- const [store] = useState(() => new Store());
4653
- store.props.set(props);
4972
+ const [store] = useState(() => {
4973
+ const nextStore = new Store();
4974
+ nextStore.props.set(props);
4975
+ return nextStore;
4976
+ });
4977
+ useLayoutEffect(() => {
4978
+ store.props.set(props);
4979
+ });
4654
4980
  useLayoutEffect(() => {
4655
4981
  store.lifecycle.mounted();
4656
4982
  return () => store.lifecycle.unmounted();
@@ -4663,31 +4989,34 @@ function MarkedInput(props) {
4663
4989
  }
4664
4990
  //#endregion
4665
4991
  //#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();
4992
+ const useMark = () => {
4993
+ const { store, token } = useTokenContext();
4994
+ const readOnly = useMarkput((s) => s.props.readOnly);
4672
4995
  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,
4996
+ return useMemo(() => MarkController.fromToken(store, token), [
4676
4997
  store,
4677
- token
4678
- }), [store, token]);
4679
- useUncontrolledInit(ref, options, token);
4680
- useEffect(() => {
4681
- mark.readOnly = readOnly;
4682
- }, [mark, readOnly]);
4683
- return mark;
4998
+ token,
4999
+ readOnly
5000
+ ]);
5001
+ };
5002
+ //#endregion
5003
+ //#region src/lib/hooks/useMarkInfo.tsx
5004
+ const useMarkInfo = () => {
5005
+ const { store, token } = useTokenContext();
5006
+ if (token.type !== "mark") throw new Error("useMarkInfo must be called within a mark token context");
5007
+ const index = store.parsing.index();
5008
+ const path = index.pathFor(token);
5009
+ if (!path) throw new Error("Mark token is not indexed");
5010
+ const address = index.addressFor(path);
5011
+ if (!address) throw new Error("Mark token path is stale");
5012
+ return {
5013
+ address,
5014
+ depth: findToken(store.parsing.tokens(), token)?.depth ?? 0,
5015
+ hasNestedMarks: token.children.some((child) => child.type === "mark"),
5016
+ key: index.key(path)
5017
+ };
4684
5018
  };
4685
- function useUncontrolledInit(ref, options, token) {
4686
- useEffect(() => {
4687
- if (!options.controlled && ref.current) ref.current.textContent = token.content;
4688
- }, []);
4689
- }
4690
5019
  //#endregion
4691
- export { MarkHandler, MarkedInput, MarkputHandler, annotate, denote, useMark, useMarkput, useOverlay };
5020
+ export { MarkController, MarkedInput, MarkputHandler, annotate, denote, useMark, useMarkInfo, useMarkput, useOverlay };
4692
5021
 
4693
5022
  //# sourceMappingURL=index.js.map