@barocss/math-editor 0.4.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/API-SESSION.md +35 -1
  2. package/API-WEB-COMPONENT.md +2 -0
  3. package/CHANGELOG.md +109 -0
  4. package/CLIPBOARD.md +54 -0
  5. package/EDITING-SCENARIOS.md +209 -0
  6. package/GETTING-STARTED.md +54 -0
  7. package/IMPLEMENTATION.md +105 -0
  8. package/KEYBOARD.md +41 -0
  9. package/LATEX-GUIDE.md +52 -2
  10. package/LATEX-MODEL.md +12 -0
  11. package/LATEX-SCOPE.md +10 -1
  12. package/README.md +119 -23
  13. package/RELEASING.md +95 -10
  14. package/RENDERING-TESTS.md +79 -0
  15. package/ROADMAP.md +238 -6
  16. package/STYLING.md +28 -1
  17. package/TEXT-EDITORS.md +157 -0
  18. package/VALIDATION.md +433 -0
  19. package/dist/context-tools.d.ts +21 -0
  20. package/dist/context-tools.js +37 -0
  21. package/dist/dom/caret-geometry.js +1 -1
  22. package/dist/dom/context-keyboard.d.ts +2 -0
  23. package/dist/dom/context-keyboard.js +22 -0
  24. package/dist/dom/help.d.ts +5 -0
  25. package/dist/dom/help.js +104 -0
  26. package/dist/dom/menu-position.d.ts +3 -0
  27. package/dist/dom/menu-position.js +49 -4
  28. package/dist/dom/readable-layout.d.ts +3 -0
  29. package/dist/dom/readable-layout.js +52 -0
  30. package/dist/dom/toolbar-catalog.d.ts +1 -1
  31. package/dist/dom/toolbar.d.ts +2 -0
  32. package/dist/dom/toolbar.js +8 -1
  33. package/dist/dom.d.ts +13 -1
  34. package/dist/dom.js +323 -51
  35. package/dist/enter-policy.js +1 -1
  36. package/dist/latex.js +11 -0
  37. package/dist/lines.d.ts +2 -0
  38. package/dist/lines.js +15 -0
  39. package/dist/locales/en.js +33 -3
  40. package/dist/locales/en.json +33 -3
  41. package/dist/locales/ko.js +33 -3
  42. package/dist/locales/ko.json +33 -3
  43. package/dist/math-editor-toolbar.js +2 -1
  44. package/dist/math-editor.d.ts +4 -1
  45. package/dist/math-editor.js +506 -296
  46. package/dist/math-layout.d.ts +12 -0
  47. package/dist/math-layout.js +59 -0
  48. package/dist/math-spacing.d.ts +13 -0
  49. package/dist/math-spacing.js +87 -0
  50. package/dist/model.d.ts +6 -0
  51. package/dist/model.js +72 -5
  52. package/dist/range.d.ts +10 -2
  53. package/dist/range.js +31 -6
  54. package/dist/root-transform.d.ts +19 -0
  55. package/dist/root-transform.js +69 -0
  56. package/dist/selection-shortcuts.d.ts +13 -0
  57. package/dist/selection-shortcuts.js +35 -0
  58. package/dist/session.d.ts +1 -0
  59. package/dist/session.js +1 -1
  60. package/dist/suggestions.d.ts +9 -0
  61. package/dist/suggestions.js +53 -0
  62. package/dist/symbols.d.ts +1 -1
  63. package/dist/symbols.js +1 -0
  64. package/dist/vertical-navigation.d.ts +5 -0
  65. package/dist/vertical-navigation.js +33 -0
  66. package/dist/web-component.js +9 -1
  67. package/package.json +11 -3
  68. package/src/fonts/KaTeX_Math-Italic.woff2 +0 -0
  69. package/src/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  70. package/src/fonts/README.md +18 -0
  71. package/src/shapes/README.md +21 -0
  72. package/src/shapes/parenthesis-bottom.svg +1 -0
  73. package/src/shapes/parenthesis-top.svg +1 -0
  74. package/src/shapes/parenthesis.svg +1 -0
  75. package/src/shapes/radical.svg +1 -0
  76. package/src/style.css +882 -61
@@ -1,4 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { followReadableMathLayout } from './dom/readable-layout.js';
3
+ import { attachMathHelp, requestMathHelp } from './dom/help.js';
4
+ import { mathEnterAction } from './enter-policy.js';
5
+ import { mathLayout } from './math-layout.js';
6
+ import { wrapSelectionShortcut } from './selection-shortcuts.js';
7
+ import { transformRoot } from './root-transform.js';
8
+ import { structureEditingContext, changeContextFence } from './context-tools.js';
9
+ import { focusContextAction } from './dom/context-keyboard.js';
2
10
  import { setMathPresentation } from './presentation.js';
3
11
  import { PresentationControls } from './presentation-controls.js';
4
12
  import { createMathPreferences } from './preferences.js';
@@ -8,33 +16,35 @@ import { insertLatex } from './latex-insertion.js';
8
16
  import { LatexPastePanel } from './latex-paste-panel.js';
9
17
  import { followMathTheme } from './dom/theme.js';
10
18
  import { matrixPointerRange } from './dom/matrix-selection.js';
11
- import { positionMathMenu } from './dom/menu-position.js';
12
- import { fences } from './fences.js';
19
+ import { positionMathMenu, followMathMenuPosition } from './dom/menu-position.js';
20
+ import { fences, fencePairs } from './fences.js';
13
21
  import { parseLatex } from './latex.js';
14
22
  import { createEditorLabels } from './editor-labels.js';
15
23
  import { MathEditorToolbar } from './math-editor-toolbar.js';
16
24
  import { SymbolBrowser } from './symbol-browser.js';
17
25
  import { insertTemplate } from './templates.js';
18
26
  import { translate, mathLocaleDirection, } from './i18n.js';
19
- import { extendKeyboardRange, collapseKeyboardRange, wrapRange, wrappingKinds, copyRange, deleteRange, fragmentLatex, MATH_CLIPBOARD_TYPE, parseFragment, pasteFragment, resolveRange, } from './range.js';
27
+ import { extendKeyboardRange, isTokenNavigationKey, collapseKeyboardRange, wrapRange, wrappingKinds, copyRange, deleteRange, fragmentLatex, MATH_CLIPBOARD_TYPE, parseFragment, pasteFragment, resolveRange, } from './range.js';
28
+ import { mathRunSpacing } from './math-spacing.js';
20
29
  import { tokenizeMathText, tokenIndexAt } from './tokens.js';
21
- import { splitLine, joinPreviousLine } from './lines.js';
22
- import { moveVertical } from './vertical-navigation.js';
30
+ import { splitLine, joinPreviousLine, joinNextLine } from './lines.js';
31
+ import { createVerticalNavigation } from './vertical-navigation.js';
23
32
  import { renderedCaretGeometry } from './dom/caret-geometry.js';
24
33
  import { insertIdentityMatrix, insertMatrix, activeGrid, MATRIX_MAX_SIZE, matrixEnvironments, resizeMatrix, setMatrixEnvironment, } from './matrix.js';
25
34
  import { MATH_MATRIX_CLIPBOARD_TYPE, parseMatrixFragment, matrixFragment, matrixFromTSV, matrixRangeBetween, resolveMatrixRange, extendMatrixRange, focusMatrixCell, copyMatrixRange, clearMatrixRange, pasteMatrixFragment, transposeMatrix, } from './matrix-range.js';
26
35
  import { createPortal } from 'react-dom';
27
- import { acceptSuggestion, findSuggestions, mathStructures } from './suggestions.js';
36
+ import { acceptSuggestion, findStateSuggestions, mathStructures } from './suggestions.js';
28
37
  import { useId, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
29
- import { gridDeletionTarget, removeGrid, row, documentRows, isLiteralText, isGrid, commit, createHistory, initialState, insertStructure, moveCaret, redo, setText, textNodes, toLatex, undo, unwrapPrevious, } from './model.js';
38
+ import { gridDeletionTarget, removeGrid, row, documentRows, isLiteralText, isGrid, commit, createHistory, initialState, insertStructure, moveCaret, redo, setText, textNodes, toLatex, undo, unwrapPrevious, unwrapNext, unwrapEmptySlot, } from './model.js';
30
39
  const structures = mathStructures;
31
40
  /** Native text fields own IME and local text selection; the tree owns structural editing. */
32
- export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus = false, enterBehavior = 'newline', onCommit, onCancel, showPopovers = true, defaultValue, excludedStructures = [], multiline = true, onChange, onExit, label, locale = 'ko', showLineNumbers = true, toolbar = true, toolbarMaxItems = 8, toolbarEnd, showTokenLegend = true, }) {
41
+ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus = false, enterBehavior = 'newline', onCommit, onCancel, showPopovers = true, defaultValue, excludedStructures = [], multiline = true, onChange, onExit, label, locale = 'ko', showLineNumbers = true, toolbar = true, toolbarMaxItems = 8, contextTools = true, toolbarEnd, showTokenLegend = true, }) {
33
42
  const t = (key, parameters) => translate(locale, key, parameters);
34
43
  const labels = createEditorLabels(locale);
35
44
  const [history, setHistory] = useState(() => createHistory(initialState(defaultValue)));
36
45
  const historyRef = useRef(history);
37
46
  const suggestionMenu = useRef(null);
47
+ const contextFooter = useRef(null);
38
48
  const selectionMenu = useRef(null);
39
49
  const [ownPreferences] = useState(createMathPreferences);
40
50
  const preferences = suppliedPreferences ?? ownPreferences;
@@ -46,6 +56,15 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
46
56
  const [matrixRange, setMatrixRange] = useState();
47
57
  const [clipboardMessage, setClipboardMessage] = useState('');
48
58
  const surface = useRef(null);
59
+ useLayoutEffect(() => {
60
+ if (surface.current)
61
+ return followReadableMathLayout(surface.current);
62
+ }, []);
63
+ useLayoutEffect(() => {
64
+ const root = surface.current?.closest('.me-editor');
65
+ if (root)
66
+ return attachMathHelp(root, () => locale);
67
+ }, [locale]);
49
68
  const drag = useRef(undefined);
50
69
  const [focused, setFocused] = useState(false);
51
70
  const [selectedGrid, setSelectedGrid] = useState();
@@ -77,11 +96,16 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
77
96
  const focusNext = useRef(autoFocus);
78
97
  const suggestionId = useId();
79
98
  const state = history.present;
99
+ const structureContext = contextTools && showPopovers && editing && focused && !range && !matrixRange
100
+ ? structureEditingContext(state)
101
+ : undefined;
102
+ const rootContext = structureContext?.root;
103
+ const fenceContext = structureContext?.fence;
80
104
  const matrixContext = activeGrid(state);
81
105
  const matrixSelection = matrixRange && resolveMatrixRange(state.document, matrixRange);
82
106
  const active = textNodes(state.document).find((n) => n.id === state.caret.id);
83
107
  const searchCaret = composing ? state.caret.end : state.caret.start;
84
- const matches = findSuggestions(active.text, searchCaret, locale);
108
+ const matches = findStateSuggestions(state, locale, searchCaret);
85
109
  matches.candidates = matches.candidates.filter((candidate) => !candidate.kind || !excludedStructures.includes(candidate.kind));
86
110
  const query = matches.query;
87
111
  const operandMenuKey = useRef('');
@@ -99,9 +123,11 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
99
123
  : [];
100
124
  const selectedIndex = Math.min(candidateIndex, Math.max(0, candidates.length - 1));
101
125
  // All React history updates pass here. Caret movement must not produce host save events.
126
+ const verticalNavigation = useRef(createVerticalNavigation());
102
127
  const publish = (next, focus = false, notify = true) => {
103
128
  const changed = next.present.document !== historyRef.current.present.document;
104
129
  if (changed) {
130
+ verticalNavigation.current.reset();
105
131
  setSelectedGrid(undefined);
106
132
  setRange(undefined);
107
133
  setMatrixRange(undefined);
@@ -142,6 +168,11 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
142
168
  };
143
169
  useImperativeHandle(apiRef, () => ({
144
170
  pasteLatex,
171
+ showHelp() {
172
+ const root = surface.current?.closest('.me-editor');
173
+ if (root)
174
+ requestMathHelp(root);
175
+ },
145
176
  importLatex(source) {
146
177
  const result = parseLatex(source, { multiline, excludedStructures });
147
178
  if (!result.ok)
@@ -214,12 +245,12 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
214
245
  ? followMathTheme(surface.current.closest('.me-editor'), suggestionMenu.current)
215
246
  : undefined;
216
247
  position();
217
- window.addEventListener('resize', position);
218
- window.addEventListener('scroll', position, true);
248
+ const stopPosition = surface.current
249
+ ? followMathMenuPosition(surface.current, position)
250
+ : undefined;
219
251
  return () => {
220
252
  stopTheme?.();
221
- window.removeEventListener('resize', position);
222
- window.removeEventListener('scroll', position, true);
253
+ stopPosition?.();
223
254
  };
224
255
  }, [queryKey, candidates.length, active.id, showPopovers]);
225
256
  useLayoutEffect(() => {
@@ -229,10 +260,11 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
229
260
  if (option && list) {
230
261
  const itemRect = option.getBoundingClientRect();
231
262
  const listRect = list.getBoundingClientRect();
232
- if (itemRect.top < listRect.top)
233
- list.scrollTop -= listRect.top - itemRect.top;
234
- else if (itemRect.bottom > listRect.bottom)
235
- list.scrollTop += itemRect.bottom - listRect.bottom;
263
+ const scaleY = listRect.height / list.offsetHeight || 1;
264
+ list.scrollTop +=
265
+ (itemRect.top + itemRect.height / 2 - listRect.top) / scaleY -
266
+ list.clientTop -
267
+ list.clientHeight / 2;
236
268
  }
237
269
  }, [selectedIndex, queryKey]);
238
270
  const choose = (index) => {
@@ -453,6 +485,12 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
453
485
  return;
454
486
  const source = event.clipboardData.getData(MATH_CLIPBOARD_TYPE);
455
487
  const fragment = source ? parseFragment(source) : undefined;
488
+ if (source && !fragment) {
489
+ event.preventDefault();
490
+ event.stopPropagation();
491
+ setClipboardMessage('clipboard.invalid');
492
+ return;
493
+ }
456
494
  const text = event.clipboardData.getData('text/plain');
457
495
  const matrixSource = event.clipboardData.getData(MATH_MATRIX_CLIPBOARD_TYPE);
458
496
  const cells = matrixSource ? parseMatrixFragment(matrixSource) : matrixFromTSV(text);
@@ -499,6 +537,10 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
499
537
  .split('\n')
500
538
  .map((value) => row(value)),
501
539
  };
540
+ if (!multiline && payload.rows.length > 1) {
541
+ setClipboardMessage('clipboard.multilineRejected');
542
+ return;
543
+ }
502
544
  const next = pasteFragment(historyRef.current.present, payload, range);
503
545
  if (next === historyRef.current.present) {
504
546
  setClipboardMessage('clipboard.multilineRejected');
@@ -544,6 +586,35 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
544
586
  }
545
587
  return false;
546
588
  };
589
+ const tokenNavigation = (event) => {
590
+ if (compositionStart.current ||
591
+ event.nativeEvent.isComposing ||
592
+ matrixRange ||
593
+ !isTokenNavigationKey(event, event.currentTarget.ownerDocument.defaultView.navigator.platform))
594
+ return false;
595
+ event.preventDefault();
596
+ event.stopPropagation();
597
+ verticalNavigation.current.reset();
598
+ const state = historyRef.current.present;
599
+ const selection = range ??
600
+ (state.caret.start !== state.caret.end
601
+ ? {
602
+ anchor: { id: state.caret.id, offset: state.caret.start },
603
+ focus: { id: state.caret.id, offset: state.caret.end },
604
+ }
605
+ : undefined);
606
+ if (event.shiftKey) {
607
+ setRange(extendKeyboardRange(state, selection, event.key, 'token'));
608
+ setEditing(false);
609
+ surface.current?.focus();
610
+ }
611
+ else {
612
+ const point = collapseKeyboardRange(state.document, selection, event.key) ??
613
+ extendKeyboardRange(state, undefined, event.key, 'token').focus;
614
+ activate({ ...point, affinity: event.key === 'ArrowLeft' ? 'forward' : 'backward' });
615
+ }
616
+ return true;
617
+ };
547
618
  const keyboardSelect = (event) => {
548
619
  if (!event.shiftKey ||
549
620
  event.altKey ||
@@ -557,10 +628,26 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
557
628
  surface.current?.focus();
558
629
  return true;
559
630
  };
631
+ const directSelectionKey = (event) => {
632
+ if (compositionStart.current || event.nativeEvent.isComposing)
633
+ return false;
634
+ const current = historyRef.current.present;
635
+ const next = wrapSelectionShortcut(current, range, event);
636
+ if (!next)
637
+ return false;
638
+ event.preventDefault();
639
+ event.stopPropagation();
640
+ if (next !== current)
641
+ apply(next);
642
+ return true;
643
+ };
560
644
  const selectionKey = (event) => {
561
645
  if (event.target instanceof HTMLInputElement || compositionStart.current)
562
646
  return;
563
- if (matrixSelectionKey(event) || keyboardSelect(event))
647
+ if (matrixSelectionKey(event) ||
648
+ tokenNavigation(event) ||
649
+ keyboardSelect(event) ||
650
+ directSelectionKey(event))
564
651
  return;
565
652
  const current = historyRef.current.present, nodes = textNodes(current.document);
566
653
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a') {
@@ -578,15 +665,7 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
578
665
  }
579
666
  if (event.metaKey || event.ctrlKey)
580
667
  return;
581
- const point = !event.altKey && collapseKeyboardRange(current.document, range, event.key);
582
- if (point) {
583
- event.preventDefault();
584
- activate(point);
585
- return;
586
- }
587
- if (range &&
588
- selectionChoices.length &&
589
- (event.key === 'Enter' || (event.altKey && ['ArrowDown', 'ArrowUp'].includes(event.key)))) {
668
+ if (range && selectionChoices.length && ['Enter', 'ArrowDown', 'ArrowUp'].includes(event.key)) {
590
669
  event.preventDefault();
591
670
  if (event.key === 'Enter') {
592
671
  if (canWrap)
@@ -597,6 +676,12 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
597
676
  selectionChoices.length);
598
677
  return;
599
678
  }
679
+ const point = !event.altKey && collapseKeyboardRange(current.document, range, event.key);
680
+ if (point) {
681
+ event.preventDefault();
682
+ activate(point);
683
+ return;
684
+ }
600
685
  if (event.altKey)
601
686
  return;
602
687
  // A model selection must behave like a text selection when typing resumes.
@@ -622,13 +707,13 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
622
707
  };
623
708
  const selectedRange = matrixRange
624
709
  ? undefined
625
- : range ??
710
+ : (range ??
626
711
  (state.caret.start !== state.caret.end
627
712
  ? {
628
713
  anchor: { id: state.caret.id, offset: state.caret.start },
629
714
  focus: { id: state.caret.id, offset: state.caret.end },
630
715
  }
631
- : undefined);
716
+ : undefined));
632
717
  const selectionFragment = selectedRange ? copyRange(state.document, selectedRange) : undefined;
633
718
  const canWrap = selectionFragment?.rows.length === 1 &&
634
719
  !isLiteralText(state.document, selectedRange.anchor.id);
@@ -682,12 +767,12 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
682
767
  ? followMathTheme(surface.current.closest('.me-editor'), selectionMenu.current)
683
768
  : undefined;
684
769
  position();
685
- window.addEventListener('scroll', position, true);
686
- window.addEventListener('resize', position);
770
+ const stopPosition = surface.current
771
+ ? followMathMenuPosition(surface.current, position)
772
+ : undefined;
687
773
  return () => {
688
- window.removeEventListener('scroll', position, true);
774
+ stopPosition?.();
689
775
  stopTheme?.();
690
- window.removeEventListener('resize', position);
691
776
  };
692
777
  }, [range, state.document, state.caret, showPopovers]);
693
778
  useLayoutEffect(() => {
@@ -696,10 +781,9 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
696
781
  if (!list || !option)
697
782
  return;
698
783
  const item = option.getBoundingClientRect(), box = list.getBoundingClientRect();
699
- if (item.top < box.top)
700
- list.scrollTop -= box.top - item.top;
701
- else if (item.bottom > box.bottom)
702
- list.scrollTop += item.bottom - box.bottom;
784
+ const scaleY = box.height / list.offsetHeight || 1;
785
+ list.scrollTop +=
786
+ (item.top + item.height / 2 - box.top) / scaleY - list.clientTop - list.clientHeight / 2;
703
787
  }, [selectionIndex]);
704
788
  const insertTool = (kind) => {
705
789
  if (excludedStructures.includes(kind) || matrixRange)
@@ -721,292 +805,379 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
721
805
  apply(insertStructure(historyRef.current.present, kind, ['fraction', 'binomial', 'superscript', 'subscript'].includes(kind)));
722
806
  };
723
807
  // Nested slots share the same caret model. Only the active lexical token mounts an input.
724
- const renderRow = (r, name, role = 'expression') => (_jsx("span", { className: "me-row", "data-math-role": role, children: r.children.map((node) => {
725
- if (isGrid(node))
726
- return (_jsxs("span", { className: "me-matrix", "data-selected": selectedGrid === node.id || undefined, "data-structure": node.type, "data-matrix-id": node.id, "data-environment": node.type === 'matrix'
727
- ? node.environment
728
- : node.type === 'cases'
729
- ? 'Bmatrix'
730
- : '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", "data-cell-index": index, "data-cell-selected": (matrixSelection?.matrix.id === node.id &&
731
- matrixSelection.indices.includes(index)) ||
732
- undefined, 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));
733
- if (node.type === 'limit' || node.type === 'limsup' || node.type === 'liminf')
734
- return (_jsxs("span", { className: "me-structure me-limit-operator", "data-structure": node.type, "data-math-style": node.mathStyle, "data-limit-placement": node.limits === false ? 'nolimits' : node.limits ? 'limits' : 'auto', children: [_jsxs("span", { className: "me-limits", children: [_jsx("span", { className: "me-limit-glyph", "aria-hidden": "true", children: node.type === 'limit' ? 'lim' : node.type === 'limsup' ? 'lim sup' : 'lim inf' }), _jsx("span", { className: "me-limit me-slot", children: renderRow(node.slots[0], labels.slot(name, node.type, 0), 'lower') })] }), _jsx("span", { className: "me-slot", children: renderRow(node.slots[1], labels.slot(name, node.type, 1)) })] }, node.id));
735
- if (node.type !== 'text' &&
736
- [
737
- 'sum',
738
- 'product',
739
- 'integral',
740
- 'doubleIntegral',
741
- 'tripleIntegral',
742
- 'contourIntegral',
743
- ].includes(node.type))
744
- return (_jsxs("span", { className: "me-structure me-large-operator", "data-limit-placement": node.limits === false ? 'nolimits' : node.limits ? 'limits' : 'auto', "data-structure": node.type, "data-math-style": node.mathStyle, 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: {
745
- sum: '',
746
- product: '∏',
747
- integral: '',
748
- doubleIntegral: '',
749
- tripleIntegral: '',
750
- contourIntegral: '',
751
- }[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));
752
- if (node.type !== 'text')
753
- return (_jsxs("span", { className: `me-structure me-${node.type}`, "data-structure": node.type, "data-math-style": node.mathStyle, children: [!!fences(node) && (_jsx("span", { className: "me-delimiter", "data-fence": fences(node)[0], "aria-hidden": "true", children: fences(node)[0] })), ['root', 'indexedRoot'].includes(node.type) && (_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'
754
- ? index === 0
755
- ? 'numerator'
756
- : 'denominator'
757
- : node.type === 'superscript'
808
+ const layout = mathLayout(state.document);
809
+ const renderRow = (r, name, role = 'expression') => {
810
+ const rowSpacing = mathRunSpacing(r);
811
+ return (_jsx("span", { className: "me-row", "data-layout-style": layout.rows.get(r.id), "data-math-role": role, children: r.children.map((node) => {
812
+ if (isGrid(node))
813
+ return (_jsxs("span", { className: "me-matrix", "data-selected": selectedGrid === node.id || undefined, "data-structure": node.type, "data-matrix-id": node.id, "data-environment": node.type === 'matrix'
814
+ ? node.environment
815
+ : node.type === 'cases'
816
+ ? 'Bmatrix'
817
+ : '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", "data-cell-index": index, "data-cell-selected": (matrixSelection?.matrix.id === node.id &&
818
+ matrixSelection.indices.includes(index)) ||
819
+ undefined, 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));
820
+ if (node.type === 'limit' || node.type === 'limsup' || node.type === 'liminf')
821
+ return (_jsxs("span", { className: "me-structure me-limit-operator", "data-operator-style": layout.operators.get(node.id)?.style, "data-layout-limits": layout.operators.get(node.id)?.placement, "data-structure": node.type, "data-math-style": node.mathStyle, "data-limit-placement": node.limits === false ? 'nolimits' : node.limits ? 'limits' : 'auto', children: [_jsxs("span", { className: "me-limits", children: [_jsx("span", { className: "me-limit-glyph", "aria-hidden": "true", children: node.type === 'limit' ? 'lim' : node.type === 'limsup' ? 'lim sup' : 'lim inf' }), _jsx("span", { className: "me-limit me-slot", children: renderRow(node.slots[0], labels.slot(name, node.type, 0), 'lower') })] }), _jsx("span", { className: "me-slot", children: renderRow(node.slots[1], labels.slot(name, node.type, 1)) })] }, node.id));
822
+ if (node.type !== 'text' &&
823
+ [
824
+ 'sum',
825
+ 'product',
826
+ 'integral',
827
+ 'doubleIntegral',
828
+ 'tripleIntegral',
829
+ 'contourIntegral',
830
+ ].includes(node.type))
831
+ return (_jsxs("span", { className: "me-structure me-large-operator", "data-operator-style": layout.operators.get(node.id)?.style, "data-layout-limits": layout.operators.get(node.id)?.placement, "data-limit-placement": node.limits === false ? 'nolimits' : node.limits ? 'limits' : 'auto', "data-structure": node.type, "data-math-style": node.mathStyle, 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: {
832
+ sum: '',
833
+ product: '',
834
+ integral: '',
835
+ doubleIntegral: '',
836
+ tripleIntegral: '',
837
+ contourIntegral: '',
838
+ }[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));
839
+ if (node.type !== 'text')
840
+ return (_jsxs("span", { className: `me-structure me-${node.type}`, "data-structure": node.type, "data-math-style": node.mathStyle, "data-math-spacing": rowSpacing.has(node.id) ? 'true' : undefined, style: rowSpacing.has(node.id)
841
+ ? {
842
+ '--me-space-before': rowSpacing.get(node.id)?.before ?? 0,
843
+ '--me-space-after': rowSpacing.get(node.id)?.after ?? 0,
844
+ '--me-tight-before': rowSpacing.get(node.id)?.tightBefore ?? 0,
845
+ '--me-tight-after': rowSpacing.get(node.id)?.tightAfter ?? 0,
846
+ }
847
+ : undefined, children: [!!fences(node) && (_jsx("span", { className: "me-delimiter", "data-fence": fences(node)[0], "aria-hidden": "true", children: fences(node)[0] })), ['root', 'indexedRoot'].includes(node.type) && (_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'
758
848
  ? index === 0
759
- ? 'base'
760
- : 'exponent'
761
- : node.type === 'subscript'
849
+ ? 'numerator'
850
+ : 'denominator'
851
+ : node.type === 'superscript'
762
852
  ? index === 0
763
853
  ? 'base'
764
- : 'index'
765
- : node.type === 'root'
766
- ? 'radicand'
767
- : 'group') }, slot.id))), !!fences(node) && (_jsx("span", { className: "me-delimiter", "data-fence": fences(node)[1], "aria-hidden": "true", children: fences(node)[1] }))] }, node.id));
768
- const parts = tokensFor(node);
769
- return parts.map((token, tokenIndex) => {
770
- const key = `${node.id}:${tokenIndex}`;
771
- const selected = selectedText(node.id, token.start, token.end);
772
- const isActive = editing &&
773
- !range &&
774
- !matrixRange &&
775
- state.caret.id === node.id &&
776
- tokenIndexAt(parts, state.caret.start, state.caret.affinity) === tokenIndex;
777
- const caretFrom = (input) => ({
778
- id: node.id,
779
- start: token.start + (input.selectionStart ?? 0),
780
- end: token.start + (input.selectionEnd ?? 0),
781
- affinity: input.selectionStart === 0 ? 'forward' : 'backward',
782
- });
783
- const replaceToken = (input) => {
784
- const current = historyRef.current.present;
785
- const run = textNodes(current.document).find((n) => n.id === node.id);
786
- 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));
787
- };
788
- 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) => {
789
- if (element)
790
- inputs.current.set(key, element);
791
- else
792
- inputs.current.delete(key);
793
- }, "aria-label": name, "aria-description": t({
794
- variable: 'token.variable',
795
- constant: 'token.constant',
796
- symbol: 'token.symbol',
797
- space: 'token.space',
798
- empty: 'token.empty',
799
- draft: 'token.composing',
800
- }[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
801
- ? `${suggestionId}-${selectedIndex}`
802
- : undefined, className: "me-input", value: token.text, placeholder: r.children.length === 1 ? '□' : '', autoComplete: "off", spellCheck: false, onPointerDown: () => setSelectedGrid(undefined), onFocus: (event) => {
803
- setFocused(true);
804
- select(caretFrom(event.currentTarget));
805
- }, onSelect: (event) => select(caretFrom(event.currentTarget)), onCompositionStart: () => {
806
- compositionStart.current = historyRef.current;
807
- compositionToken.current = {
808
- id: node.id,
809
- index: tokenIndex,
810
- length: node.text.length,
811
- parts,
812
- };
813
- setComposing(true);
814
- }, onCompositionEnd: (event) => {
815
- const next = replaceToken(event.currentTarget);
816
- const base = compositionStart.current;
817
- compositionStart.current = null;
818
- compositionToken.current = null;
819
- setComposing(false);
820
- publish(commit(base ?? historyRef.current, next), true);
821
- }, onChange: (event) => {
822
- setSelectedGrid(undefined);
823
- const current = historyRef.current, next = replaceToken(event.currentTarget);
824
- publish(compositionStart.current
825
- ? { ...current, present: next }
826
- : commit(current, next), !compositionStart.current, !compositionStart.current);
827
- setCandidateIndex(0);
828
- setDismissed('');
829
- }, onKeyDown: (event) => {
830
- event.stopPropagation();
831
- if (event.nativeEvent.isComposing ||
832
- compositionStart.current ||
833
- event.keyCode === 229)
834
- return;
835
- if (matrixSelectionKey(event) || keyboardSelect(event))
836
- return;
837
- const current = historyRef.current.present;
838
- if (selectedGrid) {
854
+ : 'exponent'
855
+ : node.type === 'subscript'
856
+ ? index === 0
857
+ ? 'base'
858
+ : 'index'
859
+ : node.type === 'root'
860
+ ? 'radicand'
861
+ : 'group') }, slot.id))), !!fences(node) && (_jsx("span", { className: "me-delimiter", "data-fence": fences(node)[1], "aria-hidden": "true", children: fences(node)[1] }))] }, node.id));
862
+ const parts = tokensFor(node);
863
+ const spacing = isLiteralText(state.document, node.id) ? undefined : rowSpacing;
864
+ return parts.map((token, tokenIndex) => {
865
+ const key = `${node.id}:${tokenIndex}`;
866
+ const selected = selectedText(node.id, token.start, token.end);
867
+ const isActive = editing &&
868
+ !range &&
869
+ !matrixRange &&
870
+ state.caret.id === node.id &&
871
+ tokenIndexAt(parts, state.caret.start, state.caret.affinity) === tokenIndex;
872
+ const caretFrom = (input) => ({
873
+ id: node.id,
874
+ start: token.start + (input.selectionStart ?? 0),
875
+ end: token.start + (input.selectionEnd ?? 0),
876
+ affinity: input.selectionStart === 0 ? 'forward' : 'backward',
877
+ });
878
+ const replaceToken = (input) => {
879
+ const current = historyRef.current.present;
880
+ const run = textNodes(current.document).find((n) => n.id === node.id);
881
+ 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));
882
+ };
883
+ 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-math-spacing": spacing ? 'true' : undefined, style: spacing
884
+ ? {
885
+ '--me-space-before': spacing.get(`${node.id}:${token.start}`)?.before ?? 0,
886
+ '--me-space-after': spacing.get(`${node.id}:${token.start}`)?.after ?? 0,
887
+ '--me-tight-before': spacing.get(`${node.id}:${token.start}`)?.tightBefore ?? 0,
888
+ '--me-tight-after': spacing.get(`${node.id}:${token.start}`)?.tightAfter ?? 0,
889
+ }
890
+ : undefined, "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) => {
891
+ if (element)
892
+ inputs.current.set(key, element);
893
+ else
894
+ inputs.current.delete(key);
895
+ }, "aria-label": name, "aria-description": t({
896
+ variable: 'token.variable',
897
+ constant: 'token.constant',
898
+ symbol: 'token.symbol',
899
+ space: 'token.space',
900
+ empty: 'token.empty',
901
+ draft: 'token.composing',
902
+ }[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
903
+ ? `${suggestionId}-${selectedIndex}`
904
+ : undefined, className: "me-input", value: token.text, placeholder: r.children.length === 1 ? '□' : '', autoComplete: "off", spellCheck: false, onPointerDown: () => setSelectedGrid(undefined), onFocus: (event) => {
905
+ setFocused(true);
906
+ select(caretFrom(event.currentTarget));
907
+ }, onSelect: (event) => select(caretFrom(event.currentTarget)), onCompositionStart: () => {
908
+ compositionStart.current = historyRef.current;
909
+ compositionToken.current = {
910
+ id: node.id,
911
+ index: tokenIndex,
912
+ length: node.text.length,
913
+ parts,
914
+ };
915
+ setComposing(true);
916
+ }, onCompositionEnd: (event) => {
917
+ const next = replaceToken(event.currentTarget);
918
+ const base = compositionStart.current;
919
+ compositionStart.current = null;
920
+ compositionToken.current = null;
921
+ setComposing(false);
922
+ publish(commit(base ?? historyRef.current, next), true);
923
+ }, onChange: (event) => {
924
+ setSelectedGrid(undefined);
925
+ const current = historyRef.current, next = replaceToken(event.currentTarget);
926
+ publish(compositionStart.current
927
+ ? { ...current, present: next }
928
+ : commit(current, next), !compositionStart.current, !compositionStart.current);
929
+ setCandidateIndex(0);
930
+ setDismissed('');
931
+ }, onKeyDown: (event) => {
932
+ event.stopPropagation();
933
+ if (event.nativeEvent.isComposing ||
934
+ compositionStart.current ||
935
+ event.keyCode === 229)
936
+ return;
937
+ // A held arrow can reach this handler before React emits onSelect.
938
+ // Boundary navigation must use the input's current native selection.
939
+ select(caretFrom(event.currentTarget));
940
+ if (matrixSelectionKey(event) ||
941
+ tokenNavigation(event) ||
942
+ keyboardSelect(event) ||
943
+ directSelectionKey(event))
944
+ return;
945
+ const current = historyRef.current.present;
946
+ if (!event.shiftKey &&
947
+ !event.altKey &&
948
+ !event.ctrlKey &&
949
+ !event.metaKey &&
950
+ ['Backspace', 'Delete'].includes(event.key)) {
951
+ const unwrapped = unwrapEmptySlot(current);
952
+ if (unwrapped !== current) {
953
+ event.preventDefault();
954
+ apply(unwrapped);
955
+ return;
956
+ }
957
+ }
958
+ if (selectedGrid) {
959
+ if (!event.metaKey &&
960
+ !event.ctrlKey &&
961
+ !event.altKey &&
962
+ !event.shiftKey &&
963
+ ['Backspace', 'Delete'].includes(event.key)) {
964
+ event.preventDefault();
965
+ if (!event.repeat)
966
+ apply(removeGrid(current, selectedGrid));
967
+ return;
968
+ }
969
+ setSelectedGrid(undefined);
970
+ if (event.key === 'Escape') {
971
+ event.preventDefault();
972
+ return;
973
+ }
974
+ }
839
975
  if (!event.metaKey &&
840
976
  !event.ctrlKey &&
841
977
  !event.altKey &&
842
978
  !event.shiftKey &&
843
979
  ['Backspace', 'Delete'].includes(event.key)) {
980
+ const grid = gridDeletionTarget(current, event.key === 'Backspace' ? -1 : 1);
981
+ if (grid) {
982
+ event.preventDefault();
983
+ const empty = grid.slots.every((slot) => slot.children.every((child) => child.type === 'text' && !child.text));
984
+ if (empty)
985
+ apply(removeGrid(current, grid.id));
986
+ else
987
+ setSelectedGrid(grid.id);
988
+ return;
989
+ }
990
+ }
991
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'z') {
844
992
  event.preventDefault();
845
- if (!event.repeat)
846
- apply(removeGrid(current, selectedGrid));
993
+ publish((event.shiftKey ? redo : undo)(historyRef.current), true);
847
994
  return;
848
995
  }
849
- setSelectedGrid(undefined);
850
- if (event.key === 'Escape') {
996
+ if (event.altKey &&
997
+ event.shiftKey &&
998
+ !event.ctrlKey &&
999
+ !event.metaKey &&
1000
+ activeGrid(current) &&
1001
+ ['ArrowUp', 'Backspace'].includes(event.key)) {
851
1002
  event.preventDefault();
1003
+ apply(resizeMatrix(current, event.key === 'ArrowUp' ? 'row' : 'column', 'delete'));
852
1004
  return;
853
1005
  }
854
- }
855
- if (!event.metaKey &&
856
- !event.ctrlKey &&
857
- !event.altKey &&
858
- !event.shiftKey &&
859
- ['Backspace', 'Delete'].includes(event.key)) {
860
- const grid = gridDeletionTarget(current, event.key === 'Backspace' ? -1 : 1);
861
- if (grid) {
1006
+ if (event.altKey &&
1007
+ !event.shiftKey &&
1008
+ !event.ctrlKey &&
1009
+ !event.metaKey &&
1010
+ event.key === 'ArrowDown') {
862
1011
  event.preventDefault();
863
- const empty = grid.slots.every((slot) => slot.children.every((child) => child.type === 'text' && !child.text));
864
- if (empty)
865
- apply(removeGrid(current, grid.id));
1012
+ setDismissed('');
1013
+ setCandidateIndex(Math.max(0, matches.candidates.findIndex((item) => item.transformFenceId || item.transformRootId)));
1014
+ operandMenuKey.current = queryKey;
1015
+ return;
1016
+ }
1017
+ if (event.ctrlKey || event.metaKey || event.altKey)
1018
+ return;
1019
+ if (activeGrid(current) &&
1020
+ event.shiftKey &&
1021
+ ['Enter', ' '].includes(event.key)) {
1022
+ event.preventDefault();
1023
+ apply(resizeMatrix(current, event.key === 'Enter' ? 'row' : 'column', 'insert'));
1024
+ return;
1025
+ }
1026
+ if (selectedRange &&
1027
+ !event.metaKey &&
1028
+ !event.ctrlKey &&
1029
+ !event.altKey &&
1030
+ ['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)) {
1031
+ event.preventDefault();
1032
+ if (event.key === 'Enter') {
1033
+ if (canWrap)
1034
+ insertTool(selectionChoices[selectionIndex % selectionChoices.length].kind);
1035
+ }
866
1036
  else
867
- setSelectedGrid(grid.id);
1037
+ setSelectionIndex((selectionIndex +
1038
+ (event.key === 'ArrowDown' ? 1 : selectionChoices.length - 1)) %
1039
+ selectionChoices.length);
1040
+ return;
1041
+ }
1042
+ if (candidates.length && ['ArrowDown', 'ArrowUp'].includes(event.key)) {
1043
+ event.preventDefault();
1044
+ const initialOperandChoice = (candidates[0]?.wrapOperand ||
1045
+ candidates[0]?.transformRootId ||
1046
+ candidates[0]?.transformFenceId) &&
1047
+ operandMenuKey.current !== queryKey;
1048
+ operandMenuKey.current = queryKey;
1049
+ setCandidateIndex(initialOperandChoice
1050
+ ? event.key === 'ArrowDown'
1051
+ ? 0
1052
+ : candidates.length - 1
1053
+ : (selectedIndex +
1054
+ (event.key === 'ArrowDown' ? 1 : candidates.length - 1)) %
1055
+ candidates.length);
868
1056
  return;
869
1057
  }
870
- }
871
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'z') {
872
- event.preventDefault();
873
- publish((event.shiftKey ? redo : undo)(historyRef.current), true);
874
- return;
875
- }
876
- if (event.altKey &&
877
- event.shiftKey &&
878
- !event.ctrlKey &&
879
- !event.metaKey &&
880
- activeGrid(current) &&
881
- ['ArrowUp', 'ArrowLeft'].includes(event.key)) {
882
- event.preventDefault();
883
- apply(resizeMatrix(current, event.key === 'ArrowUp' ? 'row' : 'column', 'delete'));
884
- return;
885
- }
886
- if (event.ctrlKey || event.metaKey || event.altKey)
887
- return;
888
- if (activeGrid(current) &&
889
- event.shiftKey &&
890
- ['Enter', ' '].includes(event.key)) {
891
- event.preventDefault();
892
- apply(resizeMatrix(current, event.key === 'Enter' ? 'row' : 'column', 'insert'));
893
- return;
894
- }
895
- if (selectedRange &&
896
- !event.metaKey &&
897
- !event.ctrlKey &&
898
- !event.altKey &&
899
- ['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)) {
900
- event.preventDefault();
901
1058
  if (event.key === 'Enter') {
902
- if (canWrap)
903
- insertTool(selectionChoices[selectionIndex % selectionChoices.length].kind);
1059
+ event.preventDefault();
1060
+ const action = mathEnterAction({
1061
+ mode: multiline ? 'block' : 'inline',
1062
+ behavior: enterBehavior,
1063
+ shiftKey: event.shiftKey,
1064
+ gridKind: activeGrid(current)?.matrix.type,
1065
+ hasSuggestion: candidates.length > 0 &&
1066
+ ((!candidates[0].wrapOperand &&
1067
+ !candidates[0].transformRootId &&
1068
+ !candidates[0].transformFenceId) ||
1069
+ operandMenuKey.current === queryKey),
1070
+ });
1071
+ if (action === 'suggestion')
1072
+ choose(selectedIndex);
1073
+ else if (action === 'grid-row')
1074
+ apply(resizeMatrix(current, 'row', 'insert'));
1075
+ else if (action === 'newline')
1076
+ apply(splitLine(current));
1077
+ else if (action === 'commit')
1078
+ onCommit?.();
1079
+ return;
904
1080
  }
905
- else
906
- setSelectionIndex((selectionIndex +
907
- (event.key === 'ArrowDown' ? 1 : selectionChoices.length - 1)) %
908
- selectionChoices.length);
909
- return;
910
- }
911
- if (candidates.length && ['ArrowDown', 'ArrowUp'].includes(event.key)) {
912
- event.preventDefault();
913
- const initialOperandChoice = candidates[0]?.wrapOperand && operandMenuKey.current !== queryKey;
914
- operandMenuKey.current = queryKey;
915
- setCandidateIndex(initialOperandChoice
916
- ? event.key === 'ArrowDown'
917
- ? 0
918
- : candidates.length - 1
919
- : (selectedIndex +
920
- (event.key === 'ArrowDown' ? 1 : candidates.length - 1)) %
921
- candidates.length);
922
- return;
923
- }
924
- if (event.key === 'Enter' &&
925
- candidates.length &&
926
- (!candidates[0].wrapOperand || operandMenuKey.current === queryKey)) {
927
- event.preventDefault();
928
- choose(selectedIndex);
929
- return;
930
- }
931
- if (event.key === 'Enter') {
932
- event.preventDefault();
933
- if (enterBehavior === 'commit' && !event.shiftKey) {
934
- onCommit?.();
1081
+ if (event.key === 'Escape') {
1082
+ event.preventDefault();
1083
+ if (candidates.length)
1084
+ setDismissed(queryKey);
1085
+ else if (onCancel)
1086
+ onCancel();
1087
+ else {
1088
+ setEditing(false);
1089
+ surface.current?.focus();
1090
+ }
935
1091
  return;
936
1092
  }
937
- if (activeGrid(current)) {
938
- if (activeGrid(current).matrix.type !== 'matrix')
939
- apply(resizeMatrix(current, 'row', 'insert'));
1093
+ if (!event.shiftKey &&
1094
+ current.caret.start === current.caret.end &&
1095
+ ['ArrowUp', 'ArrowDown'].includes(event.key)) {
1096
+ const next = verticalNavigation.current.move(current, event.key === 'ArrowUp' ? -1 : 1, surface.current ? renderedCaretGeometry(surface.current) : undefined);
1097
+ if (next !== current) {
1098
+ event.preventDefault();
1099
+ publish({ ...historyRef.current, present: next }, true);
1100
+ return;
1101
+ }
940
1102
  }
941
- else if (multiline)
942
- apply(splitLine(current));
943
- return;
944
- }
945
- if (event.key === 'Escape') {
946
- event.preventDefault();
947
- if (candidates.length)
948
- setDismissed(queryKey);
949
- else if (onCancel)
950
- onCancel();
951
- else {
952
- setEditing(false);
953
- surface.current?.focus();
1103
+ if (event.key === 'Tab') {
1104
+ if (navigate(event.shiftKey ? -1 : 1))
1105
+ event.preventDefault();
1106
+ return;
954
1107
  }
955
- return;
956
- }
957
- if (!event.shiftKey &&
958
- current.caret.start === current.caret.end &&
959
- ['ArrowUp', 'ArrowDown'].includes(event.key)) {
960
- const next = moveVertical(current, event.key === 'ArrowUp' ? -1 : 1, surface.current ? renderedCaretGeometry(surface.current) : undefined);
961
- if (next !== current) {
962
- event.preventDefault();
963
- publish({ ...historyRef.current, present: next }, true);
1108
+ if (event.shiftKey || current.caret.start !== current.caret.end)
964
1109
  return;
1110
+ if (event.key === 'ArrowLeft' && current.caret.start === token.start) {
1111
+ if (navigate(-1))
1112
+ event.preventDefault();
965
1113
  }
966
- }
967
- if (event.key === 'Tab') {
968
- if (navigate(event.shiftKey ? -1 : 1))
969
- event.preventDefault();
970
- return;
971
- }
972
- if (event.shiftKey || current.caret.start !== current.caret.end)
973
- return;
974
- if (event.key === 'ArrowLeft' && current.caret.start === token.start) {
975
- if (navigate(-1))
976
- event.preventDefault();
977
- }
978
- if (event.key === 'ArrowRight' && current.caret.end === token.end) {
979
- if (navigate(1))
1114
+ if (event.key === 'ArrowRight' && current.caret.end === token.end) {
1115
+ if (navigate(1))
1116
+ event.preventDefault();
1117
+ }
1118
+ if (event.key === 'Backspace' &&
1119
+ current.caret.start === token.start &&
1120
+ token.start > 0) {
980
1121
  event.preventDefault();
981
- }
982
- if (event.key === 'Backspace' &&
983
- current.caret.start === token.start &&
984
- token.start > 0) {
985
- event.preventDefault();
986
- const count = Array.from(node.text.slice(0, token.start)).at(-1).length;
987
- apply(setText(current, node.text.slice(0, token.start - count) + node.text.slice(token.start), token.start - count));
988
- return;
989
- }
990
- if (event.key === 'Delete' &&
991
- current.caret.end === token.end &&
992
- token.end < node.text.length) {
993
- event.preventDefault();
994
- const count = Array.from(node.text.slice(token.end))[0].length;
995
- apply(setText(current, node.text.slice(0, token.end) + node.text.slice(token.end + count), token.end));
996
- return;
997
- }
998
- if (event.key === 'Backspace' && current.caret.start === 0) {
999
- const joined = joinPreviousLine(current);
1000
- const next = joined !== current ? joined : unwrapPrevious(current);
1001
- if (next !== current) {
1122
+ const count = Array.from(node.text.slice(0, token.start)).at(-1).length;
1123
+ apply(setText(current, node.text.slice(0, token.start - count) + node.text.slice(token.start), token.start - count));
1124
+ return;
1125
+ }
1126
+ if (event.key === 'Delete' &&
1127
+ current.caret.end === token.end &&
1128
+ token.end < node.text.length) {
1002
1129
  event.preventDefault();
1003
- apply(next);
1130
+ const count = Array.from(node.text.slice(token.end))[0].length;
1131
+ apply(setText(current, node.text.slice(0, token.end) + node.text.slice(token.end + count), token.end));
1132
+ return;
1004
1133
  }
1005
- }
1006
- } })) : (_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));
1007
- });
1008
- }) }, r.id));
1009
- return (_jsxs("div", { className: "me-editor", "data-math-editor-owner": suggestionId, onKeyDownCapture: (event) => {
1134
+ if (event.key === 'Delete' && current.caret.end === node.text.length) {
1135
+ const joined = joinNextLine(current);
1136
+ const next = joined !== current ? joined : unwrapNext(current);
1137
+ if (next !== current) {
1138
+ event.preventDefault();
1139
+ apply(next);
1140
+ }
1141
+ }
1142
+ if (event.key === 'Backspace' && current.caret.start === 0) {
1143
+ const joined = joinPreviousLine(current);
1144
+ const next = joined !== current ? joined : unwrapPrevious(current);
1145
+ if (next !== current) {
1146
+ event.preventDefault();
1147
+ apply(next);
1148
+ }
1149
+ }
1150
+ } })) : (_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));
1151
+ });
1152
+ }) }, r.id));
1153
+ };
1154
+ return (_jsxs("div", { className: "me-editor", "data-math-editor-owner": suggestionId, onPointerDownCapture: () => verticalNavigation.current.reset(), onKeyDownCapture: (event) => {
1155
+ if (event.target.closest('.me-help'))
1156
+ return;
1157
+ if (!['ArrowUp', 'ArrowDown', 'Escape'].includes(event.key) ||
1158
+ event.shiftKey ||
1159
+ event.altKey ||
1160
+ event.ctrlKey ||
1161
+ event.metaKey)
1162
+ verticalNavigation.current.reset();
1163
+ if (event.key === 'F6' &&
1164
+ structureContext &&
1165
+ !composing &&
1166
+ !event.nativeEvent.isComposing) {
1167
+ const footer = contextFooter.current;
1168
+ const button = footer?.querySelector('button:not(:disabled)');
1169
+ if (button) {
1170
+ event.preventDefault();
1171
+ event.stopPropagation();
1172
+ if (footer?.contains(event.target))
1173
+ surface.current
1174
+ ?.querySelector('input.me-input')
1175
+ ?.focus({ preventScroll: true });
1176
+ else if (footer)
1177
+ focusContextAction(footer, 'F6');
1178
+ return;
1179
+ }
1180
+ }
1010
1181
  if (!event.nativeEvent.isComposing &&
1011
1182
  !composing &&
1012
1183
  event.altKey &&
@@ -1104,7 +1275,7 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
1104
1275
  : undefined, "aria-keyshortcuts": action === 'delete'
1105
1276
  ? axis === 'row'
1106
1277
  ? 'Alt+Shift+ArrowUp'
1107
- : 'Alt+Shift+ArrowLeft'
1278
+ : 'Alt+Shift+Backspace'
1108
1279
  : undefined, disabled: composing ||
1109
1280
  !!matrixRange ||
1110
1281
  !!range ||
@@ -1121,7 +1292,46 @@ export function MathEditor({ preferences: suppliedPreferences, apiRef, autoFocus
1121
1292
  candidates.length > 0 &&
1122
1293
  createPortal(_jsxs("div", { className: "me-suggestion-panel", "data-math-editor-owner": suggestionId, lang: locale, dir: mathLocaleDirection(locale), ref: suggestionMenu, onMouseDown: (event) => event.preventDefault(), children: [_jsxs("div", { className: "me-suggestion-heading", children: [_jsx("span", { className: "me-suggestion-query", 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", { className: "me-candidate-copy", 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') })] }), surface.current?.closest('dialog, [role="dialog"]') ?? 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: () => {
1123
1294
  drag.current = undefined;
1124
- }, 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))) }) }), matrixSelection && (_jsx("div", { className: "me-grid-selection-hint", role: "status", children: t('matrix.rangeHint', { rows: matrixSelection.rows, columns: matrixSelection.columns }) })), showPopovers &&
1295
+ }, 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))) }) }), structureContext && (_jsxs("div", { className: "me-context-footer", ref: contextFooter, role: "group", "aria-label": t('context.label'), onMouseDown: (event) => event.preventDefault(), onKeyDown: (event) => {
1296
+ event.stopPropagation();
1297
+ if (composing || event.nativeEvent.isComposing)
1298
+ return;
1299
+ if (focusContextAction(event.currentTarget, event.key)) {
1300
+ event.preventDefault();
1301
+ return;
1302
+ }
1303
+ if (event.key === 'Escape') {
1304
+ event.preventDefault();
1305
+ surface.current
1306
+ ?.querySelector('input.me-input')
1307
+ ?.focus({ preventScroll: true });
1308
+ }
1309
+ }, children: [fenceContext && (_jsxs(_Fragment, { children: [_jsx("span", { className: "me-context-label", children: t('context.current', {
1310
+ kind: t('presentation.fence') + ' ' + fences(fenceContext).join(' … '),
1311
+ }) }), _jsx("span", { className: "me-context-actions", children: Object.entries(fencePairs)
1312
+ .filter(([kind]) => !excludedStructures.includes(kind))
1313
+ .map(([kind, pair]) => {
1314
+ const label = t(mathStructures.find((item) => item.kind === kind).label);
1315
+ return (_jsx("button", { type: "button", className: "me-context-button", "aria-label": label, title: label, "aria-pressed": fenceContext.type === kind, disabled: composing, onClick: () => {
1316
+ if (!composing) {
1317
+ apply(changeContextFence(historyRef.current.present, fenceContext.id, kind));
1318
+ surface.current
1319
+ ?.querySelector('input.me-input')
1320
+ ?.focus({ preventScroll: true });
1321
+ }
1322
+ }, children: pair.join(' □ ') }, kind));
1323
+ }) }), _jsx("span", { className: "me-context-hint", children: t('context.fenceKeyboardHint') })] })), rootContext && (_jsxs(_Fragment, { children: [_jsx("span", { className: "me-context-label", children: t('context.current', { kind: t(`context.${rootContext.kind}`) }) }), _jsxs("span", { className: "me-context-actions", children: [rootContext.indexCaret && (_jsx("button", { type: "button", className: "me-context-button", disabled: composing, onClick: () => {
1324
+ const caret = rootContext.indexCaret;
1325
+ publish({
1326
+ ...historyRef.current,
1327
+ present: { ...historyRef.current.present, caret },
1328
+ }, true);
1329
+ }, children: t('context.editIndex') })), _jsx("button", { type: "button", className: "me-context-button", disabled: composing ||
1330
+ !rootContext.canTransform ||
1331
+ excludedStructures.includes(rootContext.target), onClick: () => {
1332
+ if (!composing)
1333
+ apply(transformRoot(historyRef.current.present, rootContext.id, rootContext.target));
1334
+ }, children: t('suggestion.transform', { kind: t(`context.${rootContext.target}`) }) })] }), _jsx("span", { className: "me-context-hint", children: t(rootContext.canTransform ? 'context.keyboardHint' : 'context.squareIndexRequired') })] }))] })), matrixSelection && (_jsx("div", { className: "me-grid-selection-hint", role: "status", children: t('matrix.rangeHint', { rows: matrixSelection.rows, columns: matrixSelection.columns }) })), showPopovers &&
1125
1335
  selectionFragment &&
1126
1336
  createPortal(_jsxs("div", { className: "me-suggestion-panel", "data-math-editor-owner": suggestionId, ref: selectionMenu, role: "presentation", "aria-label": t('selection.toolbar'), onKeyDown: selectionKey, children: [_jsx("div", { className: "me-suggestion-heading", children: t(canWrap ? 'selection.highlighted' : 'selection.singleRowRequired') }), _jsx("div", { className: "me-suggestions", role: "listbox", "aria-label": t('selection.toolbar'), children: selectionChoices.map((s, index) => (_jsxs("button", { type: "button", role: "option", "aria-selected": index === selectionIndex, disabled: !canWrap, onMouseDown: (event) => event.preventDefault(), onClick: () => insertTool(s.kind), "aria-label": t('selection.wrap', { kind: t(s.label) }), children: [_jsx("span", { className: "me-candidate-glyph", children: s.glyph }), _jsxs("span", { className: "me-candidate-copy", children: [_jsx("strong", { children: t(s.label) }), _jsx("small", { children: t(s.detail) })] })] }, s.kind))) })] }), surface.current?.closest('dialog, [role="dialog"]') ?? document.body)] }));
1127
1337
  }