@barocss/math-editor 0.1.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.
Files changed (123) hide show
  1. package/ADAPTERS.md +170 -0
  2. package/CHANGELOG.md +13 -0
  3. package/EMBEDDING.md +95 -0
  4. package/IMPLEMENTATION.md +111 -0
  5. package/LATEX-SCOPE.md +7 -0
  6. package/LICENSE +23 -0
  7. package/LOCALIZATION.md +97 -0
  8. package/README.md +151 -0
  9. package/RELEASING.md +31 -0
  10. package/ROADMAP.md +102 -0
  11. package/SUPPORT.md +160 -0
  12. package/SYMBOLS.md +96 -0
  13. package/VALIDATION.md +85 -0
  14. package/dist/core.d.ts +11 -0
  15. package/dist/core.d.ts.map +1 -0
  16. package/dist/core.js +11 -0
  17. package/dist/core.js.map +1 -0
  18. package/dist/dom/elements.d.ts +3 -0
  19. package/dist/dom/elements.d.ts.map +1 -0
  20. package/dist/dom/elements.js +9 -0
  21. package/dist/dom/elements.js.map +1 -0
  22. package/dist/dom/toolbar.d.ts +11 -0
  23. package/dist/dom/toolbar.d.ts.map +1 -0
  24. package/dist/dom/toolbar.js +46 -0
  25. package/dist/dom/toolbar.js.map +1 -0
  26. package/dist/dom.d.ts +32 -0
  27. package/dist/dom.d.ts.map +1 -0
  28. package/dist/dom.js +581 -0
  29. package/dist/dom.js.map +1 -0
  30. package/dist/editor-labels.d.ts +9 -0
  31. package/dist/editor-labels.d.ts.map +1 -0
  32. package/dist/editor-labels.js +31 -0
  33. package/dist/editor-labels.js.map +1 -0
  34. package/dist/enter-policy.d.ts +11 -0
  35. package/dist/enter-policy.d.ts.map +1 -0
  36. package/dist/enter-policy.js +13 -0
  37. package/dist/enter-policy.js.map +1 -0
  38. package/dist/i18n.d.ts +20 -0
  39. package/dist/i18n.d.ts.map +1 -0
  40. package/dist/i18n.js +58 -0
  41. package/dist/i18n.js.map +1 -0
  42. package/dist/index.d.ts +12 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +12 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/lines.d.ts +7 -0
  47. package/dist/lines.d.ts.map +1 -0
  48. package/dist/lines.js +51 -0
  49. package/dist/lines.js.map +1 -0
  50. package/dist/locales/en.js +214 -0
  51. package/dist/locales/en.json +213 -0
  52. package/dist/locales/ko.js +214 -0
  53. package/dist/locales/ko.json +213 -0
  54. package/dist/math-editor-toolbar.d.ts +24 -0
  55. package/dist/math-editor-toolbar.d.ts.map +1 -0
  56. package/dist/math-editor-toolbar.js +21 -0
  57. package/dist/math-editor-toolbar.js.map +1 -0
  58. package/dist/math-editor.d.ts +23 -0
  59. package/dist/math-editor.d.ts.map +1 -0
  60. package/dist/math-editor.js +692 -0
  61. package/dist/math-editor.js.map +1 -0
  62. package/dist/matrix.d.ts +29 -0
  63. package/dist/matrix.d.ts.map +1 -0
  64. package/dist/matrix.js +125 -0
  65. package/dist/matrix.js.map +1 -0
  66. package/dist/model.d.ts +74 -0
  67. package/dist/model.d.ts.map +1 -0
  68. package/dist/model.js +262 -0
  69. package/dist/model.js.map +1 -0
  70. package/dist/outputs.d.ts +13 -0
  71. package/dist/outputs.d.ts.map +1 -0
  72. package/dist/outputs.js +61 -0
  73. package/dist/outputs.js.map +1 -0
  74. package/dist/range.d.ts +30 -0
  75. package/dist/range.d.ts.map +1 -0
  76. package/dist/range.js +275 -0
  77. package/dist/range.js.map +1 -0
  78. package/dist/react.d.ts +11 -0
  79. package/dist/react.d.ts.map +1 -0
  80. package/dist/react.js +20 -0
  81. package/dist/react.js.map +1 -0
  82. package/dist/session.d.ts +50 -0
  83. package/dist/session.d.ts.map +1 -0
  84. package/dist/session.js +116 -0
  85. package/dist/session.js.map +1 -0
  86. package/dist/solid.d.ts +12 -0
  87. package/dist/solid.d.ts.map +1 -0
  88. package/dist/solid.js +9 -0
  89. package/dist/solid.js.map +1 -0
  90. package/dist/suggestions.d.ts +27 -0
  91. package/dist/suggestions.d.ts.map +1 -0
  92. package/dist/suggestions.js +125 -0
  93. package/dist/suggestions.js.map +1 -0
  94. package/dist/svelte.d.ts +5 -0
  95. package/dist/svelte.d.ts.map +1 -0
  96. package/dist/svelte.js +14 -0
  97. package/dist/svelte.js.map +1 -0
  98. package/dist/symbol-browser.d.ts +8 -0
  99. package/dist/symbol-browser.d.ts.map +1 -0
  100. package/dist/symbol-browser.js +32 -0
  101. package/dist/symbol-browser.js.map +1 -0
  102. package/dist/symbols.d.ts +3 -0
  103. package/dist/symbols.d.ts.map +1 -0
  104. package/dist/symbols.js +94 -0
  105. package/dist/symbols.js.map +1 -0
  106. package/dist/templates.d.ts +16 -0
  107. package/dist/templates.d.ts.map +1 -0
  108. package/dist/templates.js +86 -0
  109. package/dist/templates.js.map +1 -0
  110. package/dist/tokens.d.ts +11 -0
  111. package/dist/tokens.d.ts.map +1 -0
  112. package/dist/tokens.js +24 -0
  113. package/dist/tokens.js.map +1 -0
  114. package/dist/vue.d.ts +5 -0
  115. package/dist/vue.d.ts.map +1 -0
  116. package/dist/vue.js +16 -0
  117. package/dist/vue.js.map +1 -0
  118. package/dist/web-component.d.ts +10 -0
  119. package/dist/web-component.d.ts.map +1 -0
  120. package/dist/web-component.js +67 -0
  121. package/dist/web-component.js.map +1 -0
  122. package/package.json +124 -0
  123. package/src/style.css +781 -0
@@ -0,0 +1,692 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createEditorLabels } from './editor-labels.js';
3
+ import { MathEditorToolbar } from './math-editor-toolbar.js';
4
+ import { SymbolBrowser } from './symbol-browser.js';
5
+ import { insertTemplate } from './templates.js';
6
+ import { translate, mathLocaleDirection, } from './i18n.js';
7
+ import { wrapRange, wrappingKinds, copyRange, deleteRange, fragmentLatex, MATH_CLIPBOARD_TYPE, parseFragment, pasteFragment, resolveRange, } from './range.js';
8
+ import { tokenizeMathText, tokenIndexAt } from './tokens.js';
9
+ import { splitLine, joinPreviousLine, moveLineVertical } from './lines.js';
10
+ import { insertIdentityMatrix, insertMatrix, activeGrid, MATRIX_MAX_SIZE, matrixEnvironments, moveMatrixVertical, resizeMatrix, setMatrixEnvironment, } from './matrix.js';
11
+ import { createPortal } from 'react-dom';
12
+ import { acceptSuggestion, findSuggestions, mathStructures } from './suggestions.js';
13
+ import { useId, useLayoutEffect, useRef, useState } from 'react';
14
+ import { gridDeletionTarget, removeGrid, row, documentRows, isGrid, commit, createHistory, initialState, insertStructure, moveCaret, redo, setText, textNodes, toLatex, undo, unwrapPrevious, } from './model.js';
15
+ const structures = mathStructures;
16
+ /** Native text fields own IME and local text selection; the tree owns structural editing. */
17
+ export function MathEditor({ defaultValue, onChange, onExit, label, locale = 'ko', showLineNumbers = true, toolbar = true, toolbarEnd, showTokenLegend = true, }) {
18
+ const t = (key, parameters) => translate(locale, key, parameters);
19
+ const labels = createEditorLabels(locale);
20
+ const [history, setHistory] = useState(() => createHistory(initialState(defaultValue)));
21
+ const historyRef = useRef(history);
22
+ const [menuPosition, setMenuPosition] = useState();
23
+ const [symbolsOpen, setSymbolsOpen] = useState(false);
24
+ const [editing, setEditing] = useState(false);
25
+ const [range, setRange] = useState();
26
+ const [clipboardMessage, setClipboardMessage] = useState('');
27
+ const surface = useRef(null);
28
+ const drag = useRef(undefined);
29
+ const [focused, setFocused] = useState(false);
30
+ const [selectedGrid, setSelectedGrid] = useState();
31
+ const [dismissed, setDismissed] = useState('');
32
+ const [candidateIndex, setCandidateIndex] = useState(0);
33
+ const [composing, setComposing] = useState(false);
34
+ const compositionStart = useRef(null);
35
+ const compositionToken = useRef(null);
36
+ // Keep the active input identity stable until IME composition commits. Splitting it early loses drafts.
37
+ const tokensFor = (node) => {
38
+ const frozen = compositionToken.current;
39
+ if (!frozen || frozen.id !== node.id)
40
+ return tokenizeMathText(node.text);
41
+ const delta = node.text.length - frozen.length;
42
+ return frozen.parts.map((part, index) => {
43
+ const start = part.start + (index > frozen.index ? delta : 0), end = part.end + (index >= frozen.index ? delta : 0);
44
+ return {
45
+ ...part,
46
+ start,
47
+ end,
48
+ text: node.text.slice(start, end),
49
+ kind: index === frozen.index ? 'draft' : part.kind,
50
+ };
51
+ });
52
+ };
53
+ const inputs = useRef(new Map());
54
+ const focusNext = useRef(false);
55
+ const suggestionId = useId();
56
+ const state = history.present;
57
+ const matrixContext = activeGrid(state);
58
+ const active = textNodes(state.document).find((n) => n.id === state.caret.id);
59
+ const searchCaret = composing ? state.caret.end : state.caret.start;
60
+ const matches = findSuggestions(active.text, searchCaret, locale);
61
+ const query = matches.query;
62
+ const queryKey = `${active.id}:${searchCaret}:${query}`;
63
+ const candidates = editing &&
64
+ !symbolsOpen &&
65
+ !range &&
66
+ focused &&
67
+ (composing || state.caret.start === state.caret.end) &&
68
+ dismissed !== queryKey
69
+ ? matches.candidates
70
+ : [];
71
+ const selectedIndex = Math.min(candidateIndex, Math.max(0, candidates.length - 1));
72
+ // All React history updates pass here. Caret movement must not produce host save events.
73
+ const publish = (next, focus = false, notify = true) => {
74
+ const changed = next.present.document !== historyRef.current.present.document;
75
+ if (changed) {
76
+ setSelectedGrid(undefined);
77
+ setRange(undefined);
78
+ }
79
+ if (focus)
80
+ setEditing(true);
81
+ historyRef.current = next;
82
+ focusNext.current = focus;
83
+ setHistory(next);
84
+ if (changed && notify)
85
+ onChange?.(next.present.document, toLatex(next.present.document));
86
+ };
87
+ const apply = (next) => {
88
+ setSelectedGrid(undefined);
89
+ publish(commit(historyRef.current, next), true);
90
+ };
91
+ const select = (caret) => {
92
+ // React can emit a selection event for the old input after a structural keydown.
93
+ if (focusNext.current)
94
+ return;
95
+ const previous = historyRef.current.present.caret;
96
+ if (previous.id !== caret.id || previous.start !== caret.start || previous.end !== caret.end)
97
+ setSelectedGrid(undefined);
98
+ publish({ ...historyRef.current, present: { ...historyRef.current.present, caret } });
99
+ };
100
+ useLayoutEffect(() => {
101
+ if (!focusNext.current)
102
+ return;
103
+ const caret = history.present.caret;
104
+ const node = textNodes(history.present.document).find((n) => n.id === caret.id);
105
+ if (!node) {
106
+ focusNext.current = false;
107
+ return;
108
+ }
109
+ const parts = tokensFor(node), index = tokenIndexAt(parts, caret.start, caret.affinity), part = parts[index];
110
+ const input = inputs.current.get(`${caret.id}:${index}`);
111
+ input?.focus();
112
+ input?.setSelectionRange(Math.max(0, caret.start - part.start), Math.min(part.text.length, caret.end - part.start));
113
+ focusNext.current = false;
114
+ }, [history, editing]);
115
+ useLayoutEffect(() => {
116
+ if (!candidates.length) {
117
+ setMenuPosition(undefined);
118
+ return;
119
+ }
120
+ const position = () => {
121
+ const parts = tokensFor(active), index = tokenIndexAt(parts, searchCaret, state.caret.affinity);
122
+ const input = inputs.current.get(`${active.id}:${index}`);
123
+ if (!input)
124
+ return;
125
+ const rect = input.getBoundingClientRect();
126
+ const mirror = input.previousElementSibling;
127
+ let x = rect.left;
128
+ if (mirror?.firstChild?.nodeType === 3) {
129
+ const range = document.createRange();
130
+ range.setStart(mirror.firstChild, Math.max(0, Math.min(searchCaret - parts[index].start, mirror.textContent?.length ?? 0)));
131
+ range.collapse(true);
132
+ x = range.getBoundingClientRect().left;
133
+ }
134
+ const below = window.innerHeight - rect.bottom - 16;
135
+ const height = Math.min(340, candidates.length * 54 + 78, Math.max(below, rect.top - 16));
136
+ setMenuPosition({
137
+ left: Math.max(8, Math.min(x, window.innerWidth - 344)),
138
+ top: below >= height ? rect.bottom + 8 : Math.max(8, rect.top - height - 8),
139
+ maxHeight: height,
140
+ });
141
+ };
142
+ position();
143
+ window.addEventListener('resize', position);
144
+ window.addEventListener('scroll', position, true);
145
+ return () => {
146
+ window.removeEventListener('resize', position);
147
+ window.removeEventListener('scroll', position, true);
148
+ };
149
+ }, [queryKey, candidates.length, active.id]);
150
+ useLayoutEffect(() => {
151
+ document
152
+ .getElementById(`${suggestionId}-${selectedIndex}`)
153
+ ?.scrollIntoView({ block: 'nearest' });
154
+ }, [selectedIndex, queryKey]);
155
+ const choose = (index) => {
156
+ if (compositionStart.current || composing)
157
+ return;
158
+ const candidate = candidates[index];
159
+ if (!candidate)
160
+ return;
161
+ apply(acceptSuggestion(historyRef.current.present, query, candidate));
162
+ setCandidateIndex(0);
163
+ setDismissed(`${active.id}:${state.caret.start}:${query}`);
164
+ };
165
+ const navigate = (direction) => {
166
+ const current = historyRef.current.present;
167
+ const node = textNodes(current.document).find((n) => n.id === current.caret.id);
168
+ const parts = tokensFor(node), index = tokenIndexAt(parts, current.caret.start, current.caret.affinity);
169
+ const target = parts[index + direction];
170
+ const next = target
171
+ ? {
172
+ ...current,
173
+ caret: {
174
+ id: node.id,
175
+ start: direction > 0 ? target.start : target.end,
176
+ end: direction > 0 ? target.start : target.end,
177
+ affinity: direction > 0 ? 'forward' : 'backward',
178
+ },
179
+ }
180
+ : moveCaret(current, direction);
181
+ if (next === current)
182
+ onExit?.(direction);
183
+ else
184
+ publish({ ...historyRef.current, present: next }, true);
185
+ return next !== current || !!onExit;
186
+ };
187
+ const pointAt = (x, y) => {
188
+ const fields = [
189
+ ...(surface.current?.querySelectorAll('.me-run[data-run-id]') ?? []),
190
+ ];
191
+ let best, distance = Infinity;
192
+ for (const field of fields) {
193
+ const rect = field.getBoundingClientRect();
194
+ const d = Math.max(rect.left - x, 0, x - rect.right) ** 2 +
195
+ Math.max(rect.top - y, 0, y - rect.bottom) ** 2;
196
+ if (d < distance) {
197
+ best = field;
198
+ distance = d;
199
+ }
200
+ }
201
+ if (!best)
202
+ return;
203
+ const text = best.dataset.value ?? '', mirror = best.querySelector('.me-measure');
204
+ let index = 0, nearest = Infinity;
205
+ if (mirror?.firstChild?.nodeType === 3) {
206
+ for (let at = 0; at <= text.length; at++) {
207
+ const r = document.createRange();
208
+ r.setStart(mirror.firstChild, at);
209
+ r.collapse(true);
210
+ const d = Math.abs(r.getBoundingClientRect().left - x);
211
+ if (d < nearest) {
212
+ nearest = d;
213
+ index = at;
214
+ }
215
+ }
216
+ }
217
+ return {
218
+ id: best.dataset.runId,
219
+ offset: Number(best.dataset.tokenStart) + index,
220
+ affinity: text.length > 0 && index === text.length ? 'backward' : 'forward',
221
+ };
222
+ };
223
+ const activate = (point) => {
224
+ setRange(undefined);
225
+ setFocused(true);
226
+ setSelectedGrid(undefined);
227
+ publish({
228
+ ...historyRef.current,
229
+ present: {
230
+ ...historyRef.current.present,
231
+ caret: {
232
+ id: point.id,
233
+ start: point.offset,
234
+ end: point.offset,
235
+ affinity: point.affinity ?? 'forward',
236
+ },
237
+ },
238
+ }, true);
239
+ };
240
+ // Promote a pointer drag to a model range, including a drag that starts in the active input.
241
+ const beginPointer = (event) => {
242
+ if (event.button !== 0 ||
243
+ compositionStart.current ||
244
+ (event.target instanceof Element && event.target.closest('.me-line-number')))
245
+ return;
246
+ const point = pointAt(event.clientX, event.clientY);
247
+ if (!point)
248
+ return;
249
+ const input = event.target instanceof HTMLInputElement;
250
+ drag.current = { anchor: point, x: event.clientX, y: event.clientY, moving: false, input };
251
+ if (!input) {
252
+ event.preventDefault();
253
+ event.currentTarget.setPointerCapture(event.pointerId);
254
+ }
255
+ };
256
+ const movePointer = (event) => {
257
+ const current = drag.current;
258
+ if (!current || Math.hypot(event.clientX - current.x, event.clientY - current.y) < 4)
259
+ return;
260
+ const point = pointAt(event.clientX, event.clientY);
261
+ if (!point)
262
+ return;
263
+ const input = surface.current?.querySelector('input');
264
+ if (current.input && !current.moving && input) {
265
+ const rect = input.getBoundingClientRect();
266
+ if (event.clientX >= rect.left &&
267
+ event.clientX <= rect.right &&
268
+ event.clientY >= rect.top &&
269
+ event.clientY <= rect.bottom)
270
+ return;
271
+ }
272
+ event.preventDefault();
273
+ current.moving = true;
274
+ setEditing(false);
275
+ setRange({ anchor: current.anchor, focus: point });
276
+ setSelectedGrid(undefined);
277
+ event.currentTarget.setPointerCapture(event.pointerId);
278
+ event.currentTarget.focus();
279
+ };
280
+ const endPointer = (event) => {
281
+ const current = drag.current;
282
+ drag.current = undefined;
283
+ if (!current)
284
+ return;
285
+ if (event.currentTarget.hasPointerCapture(event.pointerId))
286
+ event.currentTarget.releasePointerCapture(event.pointerId);
287
+ if (!current.moving && !current.input) {
288
+ const point = pointAt(event.clientX, event.clientY);
289
+ if (point)
290
+ activate(point);
291
+ }
292
+ };
293
+ const resolvedRange = range ? resolveRange(state.document, range) : undefined;
294
+ const selectedText = (id, start, end) => {
295
+ if (!resolvedRange)
296
+ return;
297
+ const visit = (r) => r.children.some((n) => (n.type === 'text' ? n.id === id : n.slots.some(visit)));
298
+ for (const span of resolvedRange) {
299
+ let at = 0;
300
+ for (const n of span.row.children) {
301
+ const size = n.type === 'text' ? n.text.length : 1;
302
+ if (n.type === 'text' && n.id === id && start + at < span.end && end + at > span.start)
303
+ return [Math.max(start, span.start - at) - start, Math.min(end, span.end - at) - start];
304
+ if (n.type !== 'text' && at >= span.start && at + 1 <= span.end && n.slots.some(visit))
305
+ return [0, end - start];
306
+ at += size;
307
+ }
308
+ }
309
+ };
310
+ // Structured clipboard data preserves editable math; plain LaTeX is the interoperability fallback.
311
+ const copySelection = (event, cut = false) => {
312
+ if (!range || compositionStart.current)
313
+ return;
314
+ const fragment = copyRange(historyRef.current.present.document, range);
315
+ if (!fragment)
316
+ return;
317
+ event.preventDefault();
318
+ event.stopPropagation();
319
+ event.clipboardData.setData(MATH_CLIPBOARD_TYPE, JSON.stringify(fragment));
320
+ event.clipboardData.setData('text/plain', toLatex(fragmentLatex(fragment)));
321
+ if (cut)
322
+ apply(deleteRange(historyRef.current.present, range));
323
+ setClipboardMessage(cut ? 'clipboard.cut' : 'clipboard.copied');
324
+ };
325
+ const pasteSelection = (event) => {
326
+ if (compositionStart.current)
327
+ return;
328
+ const source = event.clipboardData.getData(MATH_CLIPBOARD_TYPE);
329
+ const fragment = source ? parseFragment(source) : undefined;
330
+ const text = event.clipboardData.getData('text/plain');
331
+ if (!fragment && !text)
332
+ return;
333
+ event.preventDefault();
334
+ event.stopPropagation();
335
+ const payload = fragment ?? {
336
+ version: 1,
337
+ rows: text
338
+ .replace(/\r\n?/g, '\n')
339
+ .split('\n')
340
+ .map((value) => row(value)),
341
+ };
342
+ const next = pasteFragment(historyRef.current.present, payload, range);
343
+ if (next === historyRef.current.present) {
344
+ setClipboardMessage('clipboard.multilineRejected');
345
+ return;
346
+ }
347
+ apply(next);
348
+ setClipboardMessage('clipboard.pasted');
349
+ };
350
+ const selectionKey = (event) => {
351
+ if (event.target instanceof HTMLInputElement || compositionStart.current)
352
+ return;
353
+ const current = historyRef.current.present, nodes = textNodes(current.document);
354
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a') {
355
+ event.preventDefault();
356
+ setRange({
357
+ anchor: { id: nodes[0].id, offset: 0 },
358
+ focus: { id: nodes.at(-1).id, offset: nodes.at(-1).text.length },
359
+ });
360
+ return;
361
+ }
362
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'z') {
363
+ event.preventDefault();
364
+ publish((event.shiftKey ? redo : undo)(historyRef.current), false);
365
+ return;
366
+ }
367
+ if (event.metaKey || event.ctrlKey || event.altKey)
368
+ return;
369
+ if (range && ['Backspace', 'Delete'].includes(event.key)) {
370
+ event.preventDefault();
371
+ apply(deleteRange(current, range));
372
+ return;
373
+ }
374
+ if (event.key === 'Escape') {
375
+ setRange(undefined);
376
+ return;
377
+ }
378
+ if (event.key === 'Enter' || event.key === 'F2') {
379
+ event.preventDefault();
380
+ activate(range?.focus ?? { id: current.caret.id, offset: current.caret.start });
381
+ }
382
+ };
383
+ const selectionFragment = range ? copyRange(state.document, range) : undefined;
384
+ const canWrap = selectionFragment?.rows.length === 1;
385
+ const insertTool = (kind) => {
386
+ const current = historyRef.current.present;
387
+ const selection = range ??
388
+ (current.caret.start !== current.caret.end
389
+ ? {
390
+ anchor: { id: current.caret.id, offset: current.caret.start },
391
+ focus: { id: current.caret.id, offset: current.caret.end },
392
+ }
393
+ : undefined);
394
+ if (selection && wrappingKinds.includes(kind)) {
395
+ apply(wrapRange(current, selection, kind));
396
+ return;
397
+ }
398
+ if (range)
399
+ return;
400
+ apply(insertStructure(historyRef.current.present, kind, ['fraction', 'superscript', 'subscript'].includes(kind)));
401
+ };
402
+ // Nested slots share the same caret model. Only the active lexical token mounts an input.
403
+ const renderRow = (r, name, role = 'expression') => (_jsx("span", { className: "me-row", "data-math-role": role, children: r.children.map((node) => {
404
+ if (isGrid(node))
405
+ return (_jsxs("span", { className: "me-matrix", "data-selected": selectedGrid === node.id || undefined, "data-structure": node.type, "data-environment": node.type === 'matrix'
406
+ ? node.environment
407
+ : node.type === 'cases'
408
+ ? 'Bmatrix'
409
+ : 'matrix', children: [_jsx("span", { className: "me-matrix-delimiter me-matrix-left", "aria-hidden": "true" }), _jsx("span", { className: "me-matrix-grid", style: { gridTemplateColumns: `repeat(${node.columns}, max-content)` }, children: node.slots.map((cell, index) => (_jsx("span", { className: "me-matrix-cell", children: renderRow(cell, labels.grid(name, node.type, index, node.columns), 'group') }, cell.id))) }), _jsx("span", { className: "me-matrix-delimiter me-matrix-right", "aria-hidden": "true" })] }, node.id));
410
+ if (node.type !== 'text' && ['sum', 'product', 'integral'].includes(node.type))
411
+ return (_jsxs("span", { className: "me-structure me-large-operator", "data-structure": node.type, children: [_jsxs("span", { className: "me-limits", children: [_jsx("span", { className: "me-limit", children: renderRow(node.slots[1], labels.slot(name, node.type, 1), 'upper') }), _jsx("span", { className: "me-operator-glyph", "aria-hidden": "true", children: { sum: '∑', product: '∏', integral: '∫' }[node.type] }), _jsx("span", { className: "me-limit", children: renderRow(node.slots[0], labels.slot(name, node.type, 0), 'lower') })] }), renderRow(node.slots[2], labels.slot(name, node.type, 2))] }, node.id));
412
+ if (node.type !== 'text')
413
+ return (_jsxs("span", { className: `me-structure me-${node.type}`, "data-structure": node.type, children: [['parentheses', 'brackets', 'absolute'].includes(node.type) && (_jsx("span", { className: "me-delimiter", "aria-hidden": "true", children: { parentheses: '(', brackets: '[', absolute: '|' }[node.type] })), node.type === 'root' && (_jsx("span", { "aria-hidden": "true", className: "me-radical", children: "\u221A" })), node.slots.map((slot, index) => (_jsx("span", { className: "me-slot", children: renderRow(slot, labels.slot(name, node.type, index), node.type === 'fraction'
414
+ ? index === 0
415
+ ? 'numerator'
416
+ : 'denominator'
417
+ : node.type === 'superscript'
418
+ ? index === 0
419
+ ? 'base'
420
+ : 'exponent'
421
+ : node.type === 'subscript'
422
+ ? index === 0
423
+ ? 'base'
424
+ : 'index'
425
+ : node.type === 'root'
426
+ ? 'radicand'
427
+ : 'group') }, slot.id))), ['parentheses', 'brackets', 'absolute'].includes(node.type) && (_jsx("span", { className: "me-delimiter", "aria-hidden": "true", children: { parentheses: ')', brackets: ']', absolute: '|' }[node.type] }))] }, node.id));
428
+ const parts = tokensFor(node);
429
+ return parts.map((token, tokenIndex) => {
430
+ const key = `${node.id}:${tokenIndex}`;
431
+ const selected = selectedText(node.id, token.start, token.end);
432
+ const isActive = editing &&
433
+ !range &&
434
+ state.caret.id === node.id &&
435
+ tokenIndexAt(parts, state.caret.start, state.caret.affinity) === tokenIndex;
436
+ const caretFrom = (input) => ({
437
+ id: node.id,
438
+ start: token.start + (input.selectionStart ?? 0),
439
+ end: token.start + (input.selectionEnd ?? 0),
440
+ affinity: input.selectionStart === 0 ? 'forward' : 'backward',
441
+ });
442
+ const replaceToken = (input) => {
443
+ const current = historyRef.current.present;
444
+ const run = textNodes(current.document).find((n) => n.id === node.id);
445
+ return setText({ ...current, caret: { ...current.caret, id: node.id } }, run.text.slice(0, token.start) + input.value + run.text.slice(token.end), token.start + (input.selectionStart ?? 0), token.start + (input.selectionEnd ?? 0));
446
+ };
447
+ return (_jsxs("span", { className: `me-run${!token.text ? ' me-empty' : ''}${!node.text && r.children.length > 1 ? ' me-empty-boundary' : ''}`, "data-token-kind": token.kind, "data-run-id": node.id, "data-token-start": token.start, "data-token-end": token.end, "data-field-name": name, "data-value": token.text, "data-range-selected": !!selected || undefined, children: [_jsx("span", { className: "me-measure", "aria-hidden": "true", children: token.text || (r.children.length === 1 ? '□' : '') }), isActive ? (_jsx("input", { ref: (element) => {
448
+ if (element)
449
+ inputs.current.set(key, element);
450
+ else
451
+ inputs.current.delete(key);
452
+ }, "aria-label": name, "aria-description": t({
453
+ variable: 'token.variable',
454
+ constant: 'token.constant',
455
+ symbol: 'token.symbol',
456
+ space: 'token.space',
457
+ empty: 'token.empty',
458
+ draft: 'token.composing',
459
+ }[token.kind]), "data-token-kind": token.kind, "data-run-id": node.id, "data-token-start": token.start, "aria-autocomplete": "list", "aria-controls": isActive && candidates.length ? suggestionId : undefined, "aria-expanded": isActive && candidates.length > 0, role: "combobox", "aria-activedescendant": isActive && !composing && candidates.length
460
+ ? `${suggestionId}-${selectedIndex}`
461
+ : undefined, className: "me-input", value: token.text, placeholder: r.children.length === 1 ? '□' : '', autoComplete: "off", spellCheck: false, onPointerDown: () => setSelectedGrid(undefined), onFocus: (event) => {
462
+ setFocused(true);
463
+ select(caretFrom(event.currentTarget));
464
+ }, onSelect: (event) => select(caretFrom(event.currentTarget)), onCompositionStart: () => {
465
+ compositionStart.current = historyRef.current;
466
+ compositionToken.current = {
467
+ id: node.id,
468
+ index: tokenIndex,
469
+ length: node.text.length,
470
+ parts,
471
+ };
472
+ setComposing(true);
473
+ }, onCompositionEnd: (event) => {
474
+ const next = replaceToken(event.currentTarget);
475
+ const base = compositionStart.current;
476
+ compositionStart.current = null;
477
+ compositionToken.current = null;
478
+ setComposing(false);
479
+ publish(commit(base ?? historyRef.current, next), true);
480
+ }, onChange: (event) => {
481
+ setSelectedGrid(undefined);
482
+ const current = historyRef.current, next = replaceToken(event.currentTarget);
483
+ publish(compositionStart.current
484
+ ? { ...current, present: next }
485
+ : commit(current, next), !compositionStart.current, !compositionStart.current);
486
+ setCandidateIndex(0);
487
+ setDismissed('');
488
+ }, onKeyDown: (event) => {
489
+ event.stopPropagation();
490
+ if (event.nativeEvent.isComposing ||
491
+ compositionStart.current ||
492
+ event.keyCode === 229)
493
+ return;
494
+ const current = historyRef.current.present;
495
+ if (selectedGrid) {
496
+ if (!event.metaKey &&
497
+ !event.ctrlKey &&
498
+ !event.altKey &&
499
+ !event.shiftKey &&
500
+ ['Backspace', 'Delete'].includes(event.key)) {
501
+ event.preventDefault();
502
+ if (!event.repeat)
503
+ apply(removeGrid(current, selectedGrid));
504
+ return;
505
+ }
506
+ setSelectedGrid(undefined);
507
+ if (event.key === 'Escape') {
508
+ event.preventDefault();
509
+ return;
510
+ }
511
+ }
512
+ if (!event.metaKey &&
513
+ !event.ctrlKey &&
514
+ !event.altKey &&
515
+ !event.shiftKey &&
516
+ ['Backspace', 'Delete'].includes(event.key)) {
517
+ const grid = gridDeletionTarget(current, event.key === 'Backspace' ? -1 : 1);
518
+ if (grid) {
519
+ event.preventDefault();
520
+ const empty = grid.slots.every((slot) => slot.children.every((child) => child.type === 'text' && !child.text));
521
+ if (empty)
522
+ apply(removeGrid(current, grid.id));
523
+ else
524
+ setSelectedGrid(grid.id);
525
+ return;
526
+ }
527
+ }
528
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'z') {
529
+ event.preventDefault();
530
+ publish((event.shiftKey ? redo : undo)(historyRef.current), true);
531
+ return;
532
+ }
533
+ if (event.altKey &&
534
+ event.shiftKey &&
535
+ !event.ctrlKey &&
536
+ !event.metaKey &&
537
+ activeGrid(current) &&
538
+ ['ArrowUp', 'ArrowLeft'].includes(event.key)) {
539
+ event.preventDefault();
540
+ apply(resizeMatrix(current, event.key === 'ArrowUp' ? 'row' : 'column', 'delete'));
541
+ return;
542
+ }
543
+ if (event.ctrlKey || event.metaKey || event.altKey)
544
+ return;
545
+ if (activeGrid(current) &&
546
+ event.shiftKey &&
547
+ ['Enter', ' '].includes(event.key)) {
548
+ event.preventDefault();
549
+ apply(resizeMatrix(current, event.key === 'Enter' ? 'row' : 'column', 'insert'));
550
+ return;
551
+ }
552
+ if (candidates.length && ['ArrowDown', 'ArrowUp'].includes(event.key)) {
553
+ event.preventDefault();
554
+ setCandidateIndex((selectedIndex + (event.key === 'ArrowDown' ? 1 : candidates.length - 1)) %
555
+ candidates.length);
556
+ return;
557
+ }
558
+ if (event.key === 'Enter' && candidates.length) {
559
+ event.preventDefault();
560
+ choose(selectedIndex);
561
+ return;
562
+ }
563
+ if (event.key === 'Enter') {
564
+ event.preventDefault();
565
+ if (activeGrid(current)) {
566
+ if (activeGrid(current).matrix.type !== 'matrix')
567
+ apply(resizeMatrix(current, 'row', 'insert'));
568
+ }
569
+ else
570
+ apply(splitLine(current));
571
+ return;
572
+ }
573
+ if (event.key === 'Escape') {
574
+ if (candidates.length)
575
+ setDismissed(queryKey);
576
+ else {
577
+ setEditing(false);
578
+ surface.current?.focus();
579
+ }
580
+ return;
581
+ }
582
+ if (!event.shiftKey &&
583
+ current.caret.start === current.caret.end &&
584
+ activeGrid(current) &&
585
+ ['ArrowUp', 'ArrowDown'].includes(event.key)) {
586
+ event.preventDefault();
587
+ publish({
588
+ ...historyRef.current,
589
+ present: moveMatrixVertical(current, event.key === 'ArrowUp' ? -1 : 1),
590
+ }, true);
591
+ return;
592
+ }
593
+ if (!event.shiftKey &&
594
+ current.caret.start === current.caret.end &&
595
+ ['ArrowUp', 'ArrowDown'].includes(event.key)) {
596
+ const next = moveLineVertical(current, event.key === 'ArrowUp' ? -1 : 1);
597
+ if (next !== current) {
598
+ event.preventDefault();
599
+ publish({ ...historyRef.current, present: next }, true);
600
+ return;
601
+ }
602
+ }
603
+ if (event.key === 'Tab') {
604
+ if (navigate(event.shiftKey ? -1 : 1))
605
+ event.preventDefault();
606
+ return;
607
+ }
608
+ if (event.shiftKey || current.caret.start !== current.caret.end)
609
+ return;
610
+ if (event.key === 'ArrowLeft' && current.caret.start === token.start) {
611
+ if (navigate(-1))
612
+ event.preventDefault();
613
+ }
614
+ if (event.key === 'ArrowRight' && current.caret.end === token.end) {
615
+ if (navigate(1))
616
+ event.preventDefault();
617
+ }
618
+ if (event.key === 'Backspace' &&
619
+ current.caret.start === token.start &&
620
+ token.start > 0) {
621
+ event.preventDefault();
622
+ const count = Array.from(node.text.slice(0, token.start)).at(-1).length;
623
+ apply(setText(current, node.text.slice(0, token.start - count) + node.text.slice(token.start), token.start - count));
624
+ return;
625
+ }
626
+ if (event.key === 'Delete' &&
627
+ current.caret.end === token.end &&
628
+ token.end < node.text.length) {
629
+ event.preventDefault();
630
+ const count = Array.from(node.text.slice(token.end))[0].length;
631
+ apply(setText(current, node.text.slice(0, token.end) + node.text.slice(token.end + count), token.end));
632
+ return;
633
+ }
634
+ if (event.key === 'Backspace' && current.caret.start === 0) {
635
+ const joined = joinPreviousLine(current);
636
+ const next = joined !== current ? joined : unwrapPrevious(current);
637
+ if (next !== current) {
638
+ event.preventDefault();
639
+ apply(next);
640
+ }
641
+ }
642
+ } })) : (_jsx("span", { className: "me-preview-token", "data-token-kind": token.kind, "aria-label": name, role: "button", tabIndex: -1, children: selected && token.text ? (_jsxs("span", { children: [token.text.slice(0, selected[0]), _jsx("mark", { className: "me-range-highlight", children: token.text.slice(selected[0], selected[1]) }), token.text.slice(selected[1])] })) : (token.text || (r.children.length === 1 ? '□' : '\u200b')) }))] }, key));
643
+ });
644
+ }) }, r.id));
645
+ return (_jsxs("div", { className: "me-editor", onCopy: copySelection, onCut: (event) => copySelection(event, true), onPaste: pasteSelection, role: "group", lang: locale, dir: mathLocaleDirection(locale), "aria-label": label ?? t('editor.label'), onBlur: (event) => {
646
+ if (!event.currentTarget.contains(event.relatedTarget) &&
647
+ !compositionStart.current &&
648
+ !focusNext.current &&
649
+ !drag.current) {
650
+ setFocused(false);
651
+ setEditing(false);
652
+ }
653
+ }, children: [toolbar && (_jsx(MathEditorToolbar, { locale: locale, composing: composing, hasRange: !!range, canWrap: canWrap, canUndo: !!history.past.length, canRedo: !!history.future.length, symbolsOpen: symbolsOpen, onInsertStructure: insertTool, onMatrixPreset: (value) => apply(value.startsWith('identity-')
654
+ ? insertIdentityMatrix(historyRef.current.present, Number(value.slice(9)))
655
+ : insertMatrix(historyRef.current.present, Number(value))), onTemplate: (id) => apply(insertTemplate(historyRef.current.present, id, range)), onToggleSymbols: () => setSymbolsOpen((value) => !value), onInsertSymbol: (glyph) => {
656
+ const current = historyRef.current.present, caret = current.caret;
657
+ const value = textNodes(current.document).find((node) => node.id === caret.id).text;
658
+ apply(setText(current, value.slice(0, caret.start) + glyph + value.slice(caret.end), caret.start + glyph.length));
659
+ }, onUndo: () => publish(undo(historyRef.current), true), onRedo: () => publish(redo(historyRef.current), true), children: toolbarEnd })), toolbar && symbolsOpen && (_jsx(SymbolBrowser, { locale: locale, onClose: () => {
660
+ setSymbolsOpen(false);
661
+ surface.current?.focus();
662
+ }, onInsert: (glyph) => {
663
+ apply(pasteFragment(historyRef.current.present, { version: 1, rows: [row(glyph)] }, range));
664
+ setSymbolsOpen(false);
665
+ } })), showTokenLegend && (_jsxs("div", { className: "me-token-legend", "aria-label": t('token.legend'), children: [_jsx("span", { "data-kind": "variable", children: t('token.variable') }), _jsx("span", { "data-kind": "constant", children: t('token.constant') }), _jsx("span", { "data-kind": "symbol", children: t('token.symbol') }), _jsx("small", { children: t('editor.interactionHint') })] })), clipboardMessage && (_jsx("div", { className: "me-grid-selection-hint", role: "status", children: t(clipboardMessage) })), selectedGrid && (_jsx("div", { className: "me-grid-selection-hint", role: "status", children: t('grid.selectionHint') })), toolbar && matrixContext && (_jsxs("div", { className: "me-matrix-tools", role: "toolbar", "aria-label": t(matrixContext.matrix.type === 'matrix' ? 'matrix.edit' : 'grid.editRows'), children: [_jsxs("span", { children: [matrixContext.rows, " \u00D7 ", matrixContext.matrix.columns, ' ', _jsxs("small", { children: ["\u00B7", ' ', t('grid.position', { row: matrixContext.row + 1, column: matrixContext.column + 1 })] })] }), matrixContext.matrix.type === 'matrix' && (_jsx("select", { "aria-label": t('matrix.delimiters'), value: matrixContext.matrix.environment, disabled: composing, onChange: (event) => apply(setMatrixEnvironment(historyRef.current.present, event.target.value)), children: matrixEnvironments.map((item) => (_jsx("option", { value: item.value, children: t(item.label) }, item.value))) })), ['row', 'column']
666
+ .filter((axis) => axis === 'row' || matrixContext.matrix.type === 'matrix')
667
+ .flatMap((axis) => ['insert', 'delete'].map((action) => {
668
+ const size = axis === 'row' ? matrixContext.rows : matrixContext.matrix.columns;
669
+ return (_jsx("button", { type: "button", title: action === 'delete'
670
+ ? t(axis === 'row' ? 'grid.deleteRowShortcut' : 'grid.deleteColumnShortcut')
671
+ : undefined, "aria-keyshortcuts": action === 'delete'
672
+ ? axis === 'row'
673
+ ? 'Alt+Shift+ArrowUp'
674
+ : 'Alt+Shift+ArrowLeft'
675
+ : undefined, disabled: composing || (action === 'delete' ? size === 1 : size >= MATRIX_MAX_SIZE), onMouseDown: (event) => event.preventDefault(), onClick: () => apply(resizeMatrix(historyRef.current.present, axis, action)), children: t(axis === 'row'
676
+ ? action === 'insert'
677
+ ? 'grid.addRow'
678
+ : 'grid.deleteRow'
679
+ : action === 'insert'
680
+ ? 'grid.addColumn'
681
+ : 'grid.deleteColumn') }, `${axis}-${action}`));
682
+ })), _jsx("small", { children: matrixContext.matrix.type === 'matrix'
683
+ ? t('matrix.keyboardHint')
684
+ : `${t(matrixContext.matrix.type === 'aligned' ? 'aligned.inputHint' : 'cases.inputHint')} · ${t('grid.keyboardHint')}` })] })), candidates.length > 0 &&
685
+ menuPosition &&
686
+ createPortal(_jsxs("div", { className: "me-suggestion-panel", lang: locale, dir: mathLocaleDirection(locale), style: menuPosition, onMouseDown: (event) => event.preventDefault(), children: [_jsxs("div", { className: "me-suggestion-heading", children: [t('suggestion.create', { query }), ' ', _jsx("span", { children: t(composing ? 'suggestion.composing' : 'suggestion.keyboardHint') })] }), _jsx("div", { className: "me-suggestions", id: suggestionId, role: "listbox", "aria-label": t('suggestion.label'), children: candidates.map((c, index) => (_jsxs("button", { type: "button", id: `${suggestionId}-${index}`, role: "option", disabled: composing, "aria-selected": !composing && index === selectedIndex, onClick: () => choose(index), children: [_jsx("span", { className: "me-candidate-glyph", children: c.glyph }), _jsxs("span", { children: [_jsx("strong", { children: c.label }), _jsx("small", { children: c.detail })] })] }, c.id))) }), _jsx("div", { className: "me-suggestion-footer", children: t(composing ? 'suggestion.imeHint' : 'suggestion.keepSource') })] }), document.body), _jsx("div", { className: "me-surface", dir: "ltr", ref: surface, tabIndex: 0, "aria-label": t('selection.surface'), "data-mode": editing ? 'edit' : 'preview', onPointerDown: beginPointer, onPointerMove: movePointer, onPointerUp: endPointer, onPointerCancel: () => {
687
+ drag.current = undefined;
688
+ }, onKeyDown: selectionKey, children: _jsx("div", { className: "me-document-lines", children: documentRows(state.document).map((line, index) => (_jsxs("div", { className: "me-document-line", children: [showLineNumbers && documentRows(state.document).length > 1 && (_jsx("span", { className: "me-line-number", "aria-hidden": "true", children: index + 1 })), renderRow(line, labels.line(index))] }, line.id))) }) }), toolbar && selectionFragment && (_jsxs("div", { className: "me-wrap-tools", role: "toolbar", "aria-label": t('selection.toolbar'), children: [_jsx("span", { children: t(canWrap ? 'selection.highlighted' : 'selection.singleRowRequired') }), structures
689
+ .filter((s) => ['fraction', 'root', 'superscript', 'parentheses'].includes(s.kind))
690
+ .map((s) => (_jsxs("button", { type: "button", disabled: !canWrap, onMouseDown: (event) => event.preventDefault(), onClick: () => insertTool(s.kind), "aria-label": t('selection.wrap', { kind: t(s.label) }), children: [s.glyph, " ", t(s.label)] }, s.kind)))] }))] }));
691
+ }
692
+ //# sourceMappingURL=math-editor.js.map