@barocss/math-editor 0.5.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 (49) hide show
  1. package/API-SESSION.md +25 -1
  2. package/CHANGELOG.md +48 -0
  3. package/CLIPBOARD.md +54 -0
  4. package/EDITING-SCENARIOS.md +55 -20
  5. package/GETTING-STARTED.md +54 -0
  6. package/IMPLEMENTATION.md +10 -0
  7. package/KEYBOARD.md +41 -0
  8. package/LATEX-GUIDE.md +30 -2
  9. package/README.md +52 -10
  10. package/RELEASING.md +22 -8
  11. package/ROADMAP.md +106 -1
  12. package/STYLING.md +28 -1
  13. package/TEXT-EDITORS.md +157 -0
  14. package/VALIDATION.md +102 -0
  15. package/dist/dom/caret-geometry.js +1 -1
  16. package/dist/dom/help.d.ts +5 -0
  17. package/dist/dom/help.js +104 -0
  18. package/dist/dom/menu-position.js +4 -3
  19. package/dist/dom/readable-layout.d.ts +3 -0
  20. package/dist/dom/readable-layout.js +52 -0
  21. package/dist/dom/toolbar-catalog.d.ts +1 -1
  22. package/dist/dom/toolbar.d.ts +2 -0
  23. package/dist/dom/toolbar.js +8 -1
  24. package/dist/dom.d.ts +11 -1
  25. package/dist/dom.js +138 -15
  26. package/dist/enter-policy.js +1 -1
  27. package/dist/lines.d.ts +2 -0
  28. package/dist/lines.js +15 -0
  29. package/dist/locales/en.js +21 -3
  30. package/dist/locales/en.json +21 -3
  31. package/dist/locales/ko.js +21 -3
  32. package/dist/locales/ko.json +21 -3
  33. package/dist/math-editor-toolbar.js +2 -1
  34. package/dist/math-editor.d.ts +1 -0
  35. package/dist/math-editor.js +135 -39
  36. package/dist/model.d.ts +2 -0
  37. package/dist/model.js +23 -1
  38. package/dist/range.d.ts +10 -2
  39. package/dist/range.js +31 -6
  40. package/dist/selection-shortcuts.d.ts +13 -0
  41. package/dist/selection-shortcuts.js +35 -0
  42. package/dist/session.d.ts +1 -0
  43. package/dist/session.js +1 -1
  44. package/dist/symbols.d.ts +1 -1
  45. package/dist/symbols.js +1 -0
  46. package/dist/vertical-navigation.d.ts +5 -0
  47. package/dist/vertical-navigation.js +33 -0
  48. package/package.json +1 -1
  49. package/src/style.css +227 -57
package/dist/dom.js CHANGED
@@ -1,5 +1,8 @@
1
+ import { followReadableMathLayout } from './dom/readable-layout.js';
2
+ import { attachMathHelp, requestMathHelp } from './dom/help.js';
1
3
  import { mathRunSpacing } from './math-spacing.js';
2
4
  import { mathLayout } from './math-layout.js';
5
+ import { wrapSelectionShortcut } from './selection-shortcuts.js';
3
6
  import { transformRoot } from './root-transform.js';
4
7
  import { structureEditingContext, changeContextFence } from './context-tools.js';
5
8
  import { focusContextAction } from './dom/context-keyboard.js';
@@ -16,16 +19,16 @@ import { mountMathToolbar } from './dom/toolbar.js';
16
19
  export { mountMathToolbar } from './dom/toolbar.js';
17
20
  import { createMathSession, } from './session.js';
18
21
  import { translate, mathLocaleDirection } from './i18n.js';
19
- import { documentRows, isLiteralText, textNodes, setText, moveCaret, unwrapPrevious, unwrapEmptySlot, gridDeletionTarget, removeGrid, isGrid, toLatex, } from './model.js';
22
+ import { documentRows, isLiteralText, textNodes, setText, moveCaret, unwrapPrevious, unwrapNext, unwrapEmptySlot, gridDeletionTarget, removeGrid, isGrid, toLatex, } from './model.js';
20
23
  import { activeGrid, activeMatrix, resizeMatrix } from './matrix.js';
21
24
  import { MATH_MATRIX_CLIPBOARD_TYPE, parseMatrixFragment, matrixFragment, matrixFromTSV, matrixRangeBetween, resolveMatrixRange, extendMatrixRange, focusMatrixCell, } from './matrix-range.js';
22
- import { joinPreviousLine } from './lines.js';
23
- import { moveVertical } from './vertical-navigation.js';
25
+ import { joinPreviousLine, joinNextLine } from './lines.js';
26
+ import { createVerticalNavigation } from './vertical-navigation.js';
24
27
  import { renderedCaretGeometry } from './dom/caret-geometry.js';
25
28
  import { extendsMathSelection, nativeSelectionAnchor, createSelectionMenuState, textSelectionBounds, } from './dom/selection.js';
26
- import { acceptSuggestion, findStateSuggestions, mathStructures } from './suggestions.js';
29
+ import { acceptSuggestion, findStateSuggestions, mathStructures, } from './suggestions.js';
27
30
  import { tokenizeMathText, tokenIndexAt } from './tokens.js';
28
- import { fragmentLatex, parseFragment, MATH_CLIPBOARD_TYPE, extendKeyboardRange, collapseKeyboardRange, resolveRange, copyRange, wrappingKinds, } from './range.js';
31
+ import { fragmentLatex, parseFragment, MATH_CLIPBOARD_TYPE, extendKeyboardRange, isTokenNavigationKey, collapseKeyboardRange, resolveRange, copyRange, wrappingKinds, } from './range.js';
29
32
  /** Native DOM renderer. No React or framework runtime is imported by this module. */
30
33
  export function mountMathEditor(host, initial = {}) {
31
34
  const doc = host.ownerDocument;
@@ -75,6 +78,7 @@ export function mountMathEditor(host, initial = {}) {
75
78
  onClose: closeLatex,
76
79
  });
77
80
  };
81
+ const stopHelp = attachMathHelp(root, () => session.getSnapshot().locale);
78
82
  const t = (value) => translate(session.getSnapshot().locale, value);
79
83
  const currentInput = () => surface.querySelector('input');
80
84
  const cancelDrag = () => {
@@ -87,6 +91,7 @@ export function mountMathEditor(host, initial = {}) {
87
91
  const notify = (key) => {
88
92
  status.textContent = t(key);
89
93
  };
94
+ const verticalNavigation = createVerticalNavigation();
90
95
  const apply = (next) => {
91
96
  editing = true;
92
97
  selectedGrid = undefined;
@@ -102,12 +107,15 @@ export function mountMathEditor(host, initial = {}) {
102
107
  const candidates = () => {
103
108
  const snapshot = session.getSnapshot(), state = snapshot.state, active = textNodes(state.document).find((n) => n.id === state.caret.id);
104
109
  const result = findStateSuggestions(state, snapshot.locale);
105
- if (!editing ||
110
+ if (options.suggestionMenu === false ||
111
+ !editing ||
106
112
  isLiteralText(state.document, state.caret.id) ||
107
113
  snapshot.range ||
108
114
  snapshot.matrixRange ||
109
115
  dismissed === `${active.id}:${active.text}:${state.caret.start}`)
110
116
  result.candidates = [];
117
+ else if (options.filterSuggestions)
118
+ result.candidates = [...options.filterSuggestions(result.candidates, result.query, state)];
111
119
  return result;
112
120
  };
113
121
  const choose = (index) => {
@@ -167,12 +175,18 @@ export function mountMathEditor(host, initial = {}) {
167
175
  const itemRect = option.getBoundingClientRect();
168
176
  const listRect = list.getBoundingClientRect();
169
177
  const scaleY = listRect.height / list.offsetHeight || 1;
170
- if (itemRect.top < listRect.top)
171
- list.scrollTop -= (listRect.top - itemRect.top) / scaleY;
172
- else if (itemRect.bottom > listRect.bottom)
173
- list.scrollTop += (itemRect.bottom - listRect.bottom) / scaleY;
178
+ list.scrollTop +=
179
+ (itemRect.top + itemRect.height / 2 - listRect.top) / scaleY -
180
+ list.clientTop -
181
+ list.clientHeight / 2;
174
182
  };
175
183
  const renderMenu = () => {
184
+ if (options.suggestionMenu === false) {
185
+ menu.hidden = true;
186
+ menu.replaceChildren();
187
+ currentInput()?.setAttribute('aria-expanded', 'false');
188
+ return;
189
+ }
176
190
  syncMathTheme(root, menu);
177
191
  const result = candidates(), input = currentInput();
178
192
  menu.replaceChildren();
@@ -201,8 +215,9 @@ export function mountMathEditor(host, initial = {}) {
201
215
  const list = element('div', 'me-suggestions');
202
216
  list.setAttribute('role', 'listbox');
203
217
  list.setAttribute('aria-label', t('selection.toolbar'));
204
- for (const item of mathStructures.filter((item) => wrappingKinds.includes(item.kind))) {
205
- const button = suggestionButton(item.glyph, t(item.label), t(item.detail));
218
+ for (const item of mathStructures.filter((item) => wrappingKinds.includes(item.kind) &&
219
+ (!options.selectionKinds || options.selectionKinds.includes(item.kind)))) {
220
+ const button = suggestionButton(item.glyph, t(item.kind === 'fraction' ? 'selection.asNumerator' : item.label), t(item.detail));
206
221
  button.setAttribute('aria-selected', String(list.children.length === candidateIndex));
207
222
  button.disabled = !canWrap;
208
223
  button.setAttribute('aria-label', translate(snapshot.locale, 'selection.wrap', { kind: t(item.label) }));
@@ -212,6 +227,18 @@ export function mountMathEditor(host, initial = {}) {
212
227
  perform({ type: 'structure', kind: item.kind });
213
228
  };
214
229
  list.append(button);
230
+ if (item.kind === 'fraction') {
231
+ const denominator = suggestionButton('□/▧', t('selection.asDenominator'), t(item.detail));
232
+ denominator.setAttribute('aria-selected', String(list.children.length === candidateIndex));
233
+ denominator.setAttribute('aria-label', t('selection.asDenominator'));
234
+ denominator.disabled = !canWrap;
235
+ denominator.onclick = () => {
236
+ if (!snapshot.range)
237
+ session.selectRange(selectedRange);
238
+ perform({ type: 'structure', kind: 'fraction', fractionSlot: 'denominator' });
239
+ };
240
+ list.append(denominator);
241
+ }
215
242
  }
216
243
  menu.append(list);
217
244
  const selected = surface.querySelectorAll('.me-dom-selected, .me-range-highlight');
@@ -475,7 +502,12 @@ export function mountMathEditor(host, initial = {}) {
475
502
  dismissed = '';
476
503
  apply(replaceToken());
477
504
  });
478
- input.onkeydown = keydown;
505
+ input.onkeydown = (event) => {
506
+ // Native arrow movement can precede select/keyup, especially during key repeat.
507
+ // Read the live caret before testing token and structure boundaries.
508
+ syncCaret();
509
+ keydown(event);
510
+ };
479
511
  run.append(input);
480
512
  }
481
513
  else {
@@ -663,6 +695,38 @@ export function mountMathEditor(host, initial = {}) {
663
695
  }
664
696
  return false;
665
697
  }
698
+ function tokenNavigation(event) {
699
+ if (composing || !isTokenNavigationKey(event, win.navigator.platform))
700
+ return false;
701
+ const snapshot = session.getSnapshot();
702
+ if (snapshot.matrixRange)
703
+ return false;
704
+ event.preventDefault();
705
+ const state = snapshot.state;
706
+ const selection = snapshot.range ??
707
+ (state.caret.start !== state.caret.end
708
+ ? {
709
+ anchor: { id: state.caret.id, offset: state.caret.start },
710
+ focus: { id: state.caret.id, offset: state.caret.end },
711
+ }
712
+ : undefined);
713
+ if (event.shiftKey) {
714
+ session.selectRange(extendKeyboardRange(state, selection, event.key, 'token'));
715
+ surface.focus();
716
+ }
717
+ else {
718
+ const point = collapseKeyboardRange(state.document, selection, event.key) ??
719
+ extendKeyboardRange(state, undefined, event.key, 'token').focus;
720
+ editing = true;
721
+ session.select({
722
+ id: point.id,
723
+ start: point.offset,
724
+ end: point.offset,
725
+ affinity: event.key === 'ArrowLeft' ? 'forward' : 'backward',
726
+ });
727
+ }
728
+ return true;
729
+ }
666
730
  function keyboardSelect(event) {
667
731
  if (composing ||
668
732
  event.isComposing ||
@@ -678,6 +742,18 @@ export function mountMathEditor(host, initial = {}) {
678
742
  surface.focus();
679
743
  return true;
680
744
  }
745
+ function directSelectionKey(event) {
746
+ if (composing || event.isComposing)
747
+ return false;
748
+ const snapshot = session.getSnapshot();
749
+ const next = wrapSelectionShortcut(snapshot.state, snapshot.range, event);
750
+ if (!next)
751
+ return false;
752
+ event.preventDefault();
753
+ if (next !== snapshot.state)
754
+ apply(next);
755
+ return true;
756
+ }
681
757
  function selectionMenuKey(event) {
682
758
  if (menu.hidden ||
683
759
  menu.dataset.kind !== 'selection' ||
@@ -713,6 +789,12 @@ export function mountMathEditor(host, initial = {}) {
713
789
  return true;
714
790
  }
715
791
  function keydown(event) {
792
+ if (!['ArrowUp', 'ArrowDown', 'Escape'].includes(event.key) ||
793
+ event.shiftKey ||
794
+ event.altKey ||
795
+ event.ctrlKey ||
796
+ event.metaKey)
797
+ verticalNavigation.reset();
716
798
  event.stopPropagation();
717
799
  if (event.isComposing || composing)
718
800
  return;
@@ -739,7 +821,9 @@ export function mountMathEditor(host, initial = {}) {
739
821
  return;
740
822
  }
741
823
  if (matrixSelectionKey(event) ||
824
+ tokenNavigation(event) ||
742
825
  keyboardSelect(event) ||
826
+ directSelectionKey(event) ||
743
827
  selectionMenuKey(event) ||
744
828
  keyboardCollapse(event))
745
829
  return;
@@ -789,6 +873,17 @@ export function mountMathEditor(host, initial = {}) {
789
873
  }
790
874
  return;
791
875
  }
876
+ const acceptsSuggestion = event.key === 'Enter' &&
877
+ !event.shiftKey &&
878
+ result.candidates.length > 0 &&
879
+ ((!result.candidates[0].wrapOperand &&
880
+ !result.candidates[0].transformRootId &&
881
+ !result.candidates[0].transformFenceId) ||
882
+ operandMenuKey === `${active.id}:${active.text}:${c.start}`);
883
+ if (options.onEditKeyDown?.(event, snapshot, acceptsSuggestion)) {
884
+ event.preventDefault();
885
+ return;
886
+ }
792
887
  if (event.key === 'Enter') {
793
888
  event.preventDefault();
794
889
  const action = mathEnterAction({
@@ -819,7 +914,7 @@ export function mountMathEditor(host, initial = {}) {
819
914
  if (activeGrid(state) &&
820
915
  event.altKey &&
821
916
  event.shiftKey &&
822
- ['ArrowUp', 'ArrowLeft'].includes(event.key)) {
917
+ ['ArrowUp', 'Backspace'].includes(event.key)) {
823
918
  event.preventDefault();
824
919
  apply(resizeMatrix(state, event.key === 'ArrowUp' ? 'row' : 'column', 'delete'));
825
920
  return;
@@ -858,7 +953,7 @@ export function mountMathEditor(host, initial = {}) {
858
953
  }
859
954
  if (!event.shiftKey && ['ArrowUp', 'ArrowDown'].includes(event.key)) {
860
955
  const direction = event.key === 'ArrowUp' ? -1 : 1;
861
- const next = moveVertical(state, direction, renderedCaretGeometry(surface));
956
+ const next = verticalNavigation.move(state, direction, renderedCaretGeometry(surface));
862
957
  if (next !== state) {
863
958
  event.preventDefault();
864
959
  session.select(next.caret);
@@ -896,6 +991,14 @@ export function mountMathEditor(host, initial = {}) {
896
991
  }
897
992
  return;
898
993
  }
994
+ if (event.key === 'Delete' && c.end === active.text.length) {
995
+ const joined = joinNextLine(state);
996
+ const next = joined !== state ? joined : unwrapNext(state);
997
+ if (next !== state) {
998
+ event.preventDefault();
999
+ apply(next);
1000
+ }
1001
+ }
899
1002
  if (event.key === 'Backspace' && c.start === 0) {
900
1003
  const joined = joinPreviousLine(state), next = joined !== state ? joined : unwrapPrevious(state);
901
1004
  if (next !== state) {
@@ -945,6 +1048,7 @@ export function mountMathEditor(host, initial = {}) {
945
1048
  };
946
1049
  };
947
1050
  surface.onpointerdown = (event) => {
1051
+ verticalNavigation.reset();
948
1052
  if (event.button || composing || drag || event.target.closest('.me-line-number'))
949
1053
  return;
950
1054
  const point = pointAt(event.clientX, event.clientY);
@@ -1050,6 +1154,8 @@ export function mountMathEditor(host, initial = {}) {
1050
1154
  doc.addEventListener('pointercancel', cancelPointer, true);
1051
1155
  win.addEventListener('blur', cancelDrag);
1052
1156
  surface.onkeydown = (event) => {
1157
+ if (event.target !== currentInput())
1158
+ verticalNavigation.reset();
1053
1159
  if (event.target === currentInput())
1054
1160
  return;
1055
1161
  event.stopPropagation();
@@ -1063,7 +1169,9 @@ export function mountMathEditor(host, initial = {}) {
1063
1169
  return;
1064
1170
  }
1065
1171
  if (matrixSelectionKey(event) ||
1172
+ tokenNavigation(event) ||
1066
1173
  keyboardSelect(event) ||
1174
+ directSelectionKey(event) ||
1067
1175
  selectionMenuKey(event) ||
1068
1176
  keyboardCollapse(event))
1069
1177
  return;
@@ -1129,6 +1237,12 @@ export function mountMathEditor(host, initial = {}) {
1129
1237
  if (composing)
1130
1238
  return;
1131
1239
  const data = event.clipboardData, fragment = parseFragment(data?.getData(MATH_CLIPBOARD_TYPE) ?? ''), text = data?.getData('text/plain') ?? '';
1240
+ if (data?.getData(MATH_CLIPBOARD_TYPE) && !fragment) {
1241
+ event.preventDefault();
1242
+ event.stopPropagation();
1243
+ notify('clipboard.invalid');
1244
+ return;
1245
+ }
1132
1246
  const matrixSource = data?.getData(MATH_MATRIX_CLIPBOARD_TYPE) ?? '';
1133
1247
  const matrix = matrixSource ? parseMatrixFragment(matrixSource) : matrixFromTSV(text);
1134
1248
  const snapshot = session.getSnapshot();
@@ -1189,6 +1303,8 @@ export function mountMathEditor(host, initial = {}) {
1189
1303
  toolbar?.setDisabled(composing);
1190
1304
  };
1191
1305
  const unsubscribe = session.subscribe((snapshot, changed) => {
1306
+ if (changed)
1307
+ verticalNavigation.reset();
1192
1308
  if (changed) {
1193
1309
  cancelDrag();
1194
1310
  selectionMenu.reset();
@@ -1197,6 +1313,7 @@ export function mountMathEditor(host, initial = {}) {
1197
1313
  if (changed)
1198
1314
  options.onChange?.(snapshot.state.document, snapshot.latex);
1199
1315
  });
1316
+ const stopReadableLayout = followReadableMathLayout(surface);
1200
1317
  const stopPosition = followMathMenuPosition(root, () => {
1201
1318
  if (!menu.hidden)
1202
1319
  drawMenu();
@@ -1205,6 +1322,10 @@ export function mountMathEditor(host, initial = {}) {
1205
1322
  render();
1206
1323
  return {
1207
1324
  session,
1325
+ showHelp() {
1326
+ if (!destroyed)
1327
+ requestMathHelp(root);
1328
+ },
1208
1329
  focus() {
1209
1330
  editing = true;
1210
1331
  render();
@@ -1223,10 +1344,12 @@ export function mountMathEditor(host, initial = {}) {
1223
1344
  destroyed = true;
1224
1345
  cancelDrag();
1225
1346
  unsubscribe();
1347
+ stopHelp();
1226
1348
  latexPanel?.destroy();
1227
1349
  toolbar?.destroy();
1228
1350
  root.removeEventListener('focusout', blur);
1229
1351
  stopPosition();
1352
+ stopReadableLayout();
1230
1353
  doc.removeEventListener('pointermove', movePointer, true);
1231
1354
  doc.removeEventListener('pointerup', endPointer, true);
1232
1355
  doc.removeEventListener('pointercancel', cancelPointer, true);
@@ -4,7 +4,7 @@ export function mathEnterAction(context) {
4
4
  return 'suggestion';
5
5
  if (context.gridKind && context.shiftKey)
6
6
  return 'grid-row';
7
- if (context.mode === 'inline' || context.behavior === 'commit')
7
+ if (context.mode === 'inline' || (context.behavior === 'commit' && !context.shiftKey))
8
8
  return 'commit';
9
9
  if (context.gridKind)
10
10
  return context.gridKind === 'matrix' ? 'none' : 'grid-row';
package/dist/lines.d.ts CHANGED
@@ -3,4 +3,6 @@ import { type MathState } from './model.js';
3
3
  export declare function splitLine(state: MathState): MathState;
4
4
  /** Backspace at the start of a top-level line merges it with the previous line. */
5
5
  export declare function joinPreviousLine(state: MathState): MathState;
6
+ /** Delete at the end of a top-level line joins the following line without moving the caret. */
7
+ export declare function joinNextLine(state: MathState): MathState;
6
8
  export declare function moveLineVertical(state: MathState, direction: -1 | 1): MathState;
package/dist/lines.js CHANGED
@@ -39,6 +39,21 @@ export function joinPreviousLine(state) {
39
39
  delete document.additionalLines;
40
40
  return { document, caret: { id: tail.id, start: offset, end: offset } };
41
41
  }
42
+ /** Delete at the end of a top-level line joins the following line without moving the caret. */
43
+ export function joinNextLine(state) {
44
+ if (state.caret.start !== state.caret.end)
45
+ return state;
46
+ const lines = documentRows(state.document);
47
+ const index = lines.findIndex((line) => line.children.at(-1)?.id === state.caret.id);
48
+ const tail = lines[index]?.children.at(-1);
49
+ const head = lines[index + 1]?.children[0];
50
+ if (index < 0 ||
51
+ tail?.type !== 'text' ||
52
+ head?.type !== 'text' ||
53
+ state.caret.end !== tail.text.length)
54
+ return state;
55
+ return joinPreviousLine({ ...state, caret: { id: head.id, start: 0, end: 0 } });
56
+ }
42
57
  export function moveLineVertical(state, direction) {
43
58
  const lines = documentRows(state.document);
44
59
  const index = lines.findIndex((line) => line.children.some((node) => node.type === 'text' && node.id === state.caret.id));
@@ -92,7 +92,8 @@ export default {
92
92
  "grid.addColumn": "Add column",
93
93
  "grid.deleteColumn": "Delete column",
94
94
  "grid.deleteRowShortcut": "Delete row: Alt(⌥)+Shift+↑",
95
- "grid.deleteColumnShortcut": "Delete column: Alt(⌥)+Shift+←",
95
+ "grid.deleteColumnShortcut": "Delete column: Alt(⌥)+Shift+Backspace",
96
+ "clipboard.invalid": "The copied formula data is invalid. The formula was not changed.",
96
97
  "clipboard.cut": "Selected math cut.",
97
98
  "clipboard.copied": "Selected math copied as structure and LaTeX.",
98
99
  "clipboard.pasted": "Pasted.",
@@ -101,13 +102,15 @@ export default {
101
102
  "grid.selectionHint": "Structure selected · Backspace/Delete to remove · Esc to cancel",
102
103
  "selection.toolbar": "Wrap selected math",
103
104
  "selection.highlighted": "Wrap highlighted range",
105
+ "selection.asNumerator": "Use as numerator",
106
+ "selection.asDenominator": "Use as denominator",
104
107
  "selection.singleRowRequired": "Select a range within one row.",
105
108
  "suggestion.label": "Math suggestions",
106
109
  "suggestion.composing": "Composing · Choose after committing",
107
110
  "suggestion.keyboardHint": "↑ ↓ to choose · Enter to apply",
108
111
  "suggestion.imeHint": "Suggestion preview · Enter and arrows are reserved for the IME",
109
112
  "suggestion.keepSource": "Keep typing, or use Space / Esc to keep the source",
110
- "matrix.keyboardHint": "Shift+Enter add row · Shift+Space add column · Alt(⌥)+Shift+↑ delete row · Alt(⌥)+Shift+← delete column",
113
+ "matrix.keyboardHint": "Shift+Enter add row · Shift+Space add column · Alt(⌥)+Shift+↑ delete row · Alt(⌥)+Shift+Backspace delete column",
111
114
  "aligned.inputHint": "Left: left-hand side · Right: start with =",
112
115
  "cases.inputHint": "Left: expression · Right: condition",
113
116
  "grid.keyboardHint": "Enter add row · Alt(⌥)+Shift+↑ delete row",
@@ -155,6 +158,7 @@ export default {
155
158
  "template.detail": "Editable math template",
156
159
  "template.quadratic": "Quadratic formula",
157
160
  "template.pythagorean": "Pythagorean theorem",
161
+ "symbol.prime": "Prime",
158
162
  "symbol.degree": "Degree",
159
163
  "symbol.angle": "Angle",
160
164
  "symbol.plus": "Plus",
@@ -353,7 +357,21 @@ export default {
353
357
  "context.indexedRoot": "Indexed root",
354
358
  "context.fenceKeyboardHint": "F6: tools · ← →: choose · Enter: apply · Esc: back to input",
355
359
  "suggestion.transformFence": "Change brackets: {kind}",
356
- "suggestion.transformFenceDetail": "Keep the enclosed formula and cursor position"
360
+ "suggestion.transformFenceDetail": "Keep the enclosed formula and cursor position",
361
+ "help.title": "Keyboard & clipboard help",
362
+ "help.intro": "Keep typing with these controls. Close help to return to your formula.",
363
+ "help.units": "Move by a variable, number or whole structure. Add Shift to extend selection. Inside a slot, reach the current token edge before leaving the structure.",
364
+ "help.slots": "Move to the next or previous editable slot.",
365
+ "help.select": "Extend the formula selection.",
366
+ "help.wrap": "With math selected: wrap in brackets, absolute value, a fraction, a power or a subscript.",
367
+ "help.suggestions": "Open suggestions and available transformations, even without a toolbar.",
368
+ "help.menu": "Choose a suggestion, apply it, or dismiss the menu.",
369
+ "help.clipboard": "Copy, cut or paste. Editor data preserves structure when the browser retains it. Ordinary text pastes literally.",
370
+ "help.latex": "Open Paste as LaTeX. Paste the source, then Ctrl/Cmd+Enter to insert. Invalid input leaves the formula unchanged.",
371
+ "help.history": "Undo or redo an edit.",
372
+ "help.lines": "Enter follows the host/mode policy. Shift+Enter adds a grid row or a block line. Inline mode never adds a top-level formula line.",
373
+ "help.open": "Open this help while the formula has focus.",
374
+ "help.platform": "Cmd on macOS, Ctrl on Windows/Linux. Alt is Option on macOS. Some keyboards require Fn+F1. Host or OS shortcuts can take precedence."
357
375
  },
358
376
  "aliases": {}
359
377
  };
@@ -91,7 +91,8 @@
91
91
  "grid.addColumn": "Add column",
92
92
  "grid.deleteColumn": "Delete column",
93
93
  "grid.deleteRowShortcut": "Delete row: Alt(⌥)+Shift+↑",
94
- "grid.deleteColumnShortcut": "Delete column: Alt(⌥)+Shift+←",
94
+ "grid.deleteColumnShortcut": "Delete column: Alt(⌥)+Shift+Backspace",
95
+ "clipboard.invalid": "The copied formula data is invalid. The formula was not changed.",
95
96
  "clipboard.cut": "Selected math cut.",
96
97
  "clipboard.copied": "Selected math copied as structure and LaTeX.",
97
98
  "clipboard.pasted": "Pasted.",
@@ -100,13 +101,15 @@
100
101
  "grid.selectionHint": "Structure selected · Backspace/Delete to remove · Esc to cancel",
101
102
  "selection.toolbar": "Wrap selected math",
102
103
  "selection.highlighted": "Wrap highlighted range",
104
+ "selection.asNumerator": "Use as numerator",
105
+ "selection.asDenominator": "Use as denominator",
103
106
  "selection.singleRowRequired": "Select a range within one row.",
104
107
  "suggestion.label": "Math suggestions",
105
108
  "suggestion.composing": "Composing · Choose after committing",
106
109
  "suggestion.keyboardHint": "↑ ↓ to choose · Enter to apply",
107
110
  "suggestion.imeHint": "Suggestion preview · Enter and arrows are reserved for the IME",
108
111
  "suggestion.keepSource": "Keep typing, or use Space / Esc to keep the source",
109
- "matrix.keyboardHint": "Shift+Enter add row · Shift+Space add column · Alt(⌥)+Shift+↑ delete row · Alt(⌥)+Shift+← delete column",
112
+ "matrix.keyboardHint": "Shift+Enter add row · Shift+Space add column · Alt(⌥)+Shift+↑ delete row · Alt(⌥)+Shift+Backspace delete column",
110
113
  "aligned.inputHint": "Left: left-hand side · Right: start with =",
111
114
  "cases.inputHint": "Left: expression · Right: condition",
112
115
  "grid.keyboardHint": "Enter add row · Alt(⌥)+Shift+↑ delete row",
@@ -154,6 +157,7 @@
154
157
  "template.detail": "Editable math template",
155
158
  "template.quadratic": "Quadratic formula",
156
159
  "template.pythagorean": "Pythagorean theorem",
160
+ "symbol.prime": "Prime",
157
161
  "symbol.degree": "Degree",
158
162
  "symbol.angle": "Angle",
159
163
  "symbol.plus": "Plus",
@@ -352,7 +356,21 @@
352
356
  "context.indexedRoot": "Indexed root",
353
357
  "context.fenceKeyboardHint": "F6: tools · ← →: choose · Enter: apply · Esc: back to input",
354
358
  "suggestion.transformFence": "Change brackets: {kind}",
355
- "suggestion.transformFenceDetail": "Keep the enclosed formula and cursor position"
359
+ "suggestion.transformFenceDetail": "Keep the enclosed formula and cursor position",
360
+ "help.title": "Keyboard & clipboard help",
361
+ "help.intro": "Keep typing with these controls. Close help to return to your formula.",
362
+ "help.units": "Move by a variable, number or whole structure. Add Shift to extend selection. Inside a slot, reach the current token edge before leaving the structure.",
363
+ "help.slots": "Move to the next or previous editable slot.",
364
+ "help.select": "Extend the formula selection.",
365
+ "help.wrap": "With math selected: wrap in brackets, absolute value, a fraction, a power or a subscript.",
366
+ "help.suggestions": "Open suggestions and available transformations, even without a toolbar.",
367
+ "help.menu": "Choose a suggestion, apply it, or dismiss the menu.",
368
+ "help.clipboard": "Copy, cut or paste. Editor data preserves structure when the browser retains it. Ordinary text pastes literally.",
369
+ "help.latex": "Open Paste as LaTeX. Paste the source, then Ctrl/Cmd+Enter to insert. Invalid input leaves the formula unchanged.",
370
+ "help.history": "Undo or redo an edit.",
371
+ "help.lines": "Enter follows the host/mode policy. Shift+Enter adds a grid row or a block line. Inline mode never adds a top-level formula line.",
372
+ "help.open": "Open this help while the formula has focus.",
373
+ "help.platform": "Cmd on macOS, Ctrl on Windows/Linux. Alt is Option on macOS. Some keyboards require Fn+F1. Host or OS shortcuts can take precedence."
356
374
  },
357
375
  "aliases": {}
358
376
  }
@@ -92,7 +92,8 @@ export default {
92
92
  "grid.addColumn": "열 추가",
93
93
  "grid.deleteColumn": "열 삭제",
94
94
  "grid.deleteRowShortcut": "행 삭제: Alt(⌥)+Shift+↑",
95
- "grid.deleteColumnShortcut": "열 삭제: Alt(⌥)+Shift+←",
95
+ "grid.deleteColumnShortcut": "열 삭제: Alt(⌥)+Shift+Backspace",
96
+ "clipboard.invalid": "복사한 수식 데이터가 올바르지 않습니다. 기존 수식은 유지됩니다.",
96
97
  "clipboard.cut": "선택한 수식을 잘라냈습니다.",
97
98
  "clipboard.copied": "선택한 수식을 구조와 LaTeX로 복사했습니다.",
98
99
  "clipboard.pasted": "붙여넣었습니다.",
@@ -101,13 +102,15 @@ export default {
101
102
  "grid.selectionHint": "수식 구조 전체 선택 · Backspace/Delete로 삭제 · Esc로 취소",
102
103
  "selection.toolbar": "선택한 수식 감싸기",
103
104
  "selection.highlighted": "강조된 범위 감싸기",
105
+ "selection.asNumerator": "분자로 넣기",
106
+ "selection.asDenominator": "분모로 넣기",
104
107
  "selection.singleRowRequired": "한 줄 안의 범위를 선택해 주세요.",
105
108
  "suggestion.label": "수식 추천",
106
109
  "suggestion.composing": "한글 조합 중 · 확정 후 선택",
107
110
  "suggestion.keyboardHint": "↑ ↓ 선택 · Enter 적용",
108
111
  "suggestion.imeHint": "후보 미리보기 · Enter와 방향키는 한글 입력에 사용됩니다",
109
112
  "suggestion.keepSource": "계속 입력하거나 Space · Esc로 원문 유지",
110
- "matrix.keyboardHint": "Shift+Enter 행 추가 · Shift+Space 열 추가 · Alt(⌥)+Shift+↑ 행 삭제 · Alt(⌥)+Shift+← 열 삭제",
113
+ "matrix.keyboardHint": "Shift+Enter 행 추가 · Shift+Space 열 추가 · Alt(⌥)+Shift+↑ 행 삭제 · Alt(⌥)+Shift+Backspace 열 삭제",
111
114
  "aligned.inputHint": "왼쪽: 좌변 · 오른쪽: =부터 입력",
112
115
  "cases.inputHint": "왼쪽: 식 · 오른쪽: 조건",
113
116
  "grid.keyboardHint": "Enter 행 추가 · Alt(⌥)+Shift+↑ 행 삭제",
@@ -155,6 +158,7 @@ export default {
155
158
  "template.detail": "편집 가능한 수식 템플릿",
156
159
  "template.quadratic": "근의 공식",
157
160
  "template.pythagorean": "피타고라스 정리",
161
+ "symbol.prime": "프라임",
158
162
  "symbol.degree": "도",
159
163
  "symbol.angle": "각 기호",
160
164
  "symbol.plus": "더하기",
@@ -353,7 +357,21 @@ export default {
353
357
  "context.indexedRoot": "n제곱근",
354
358
  "context.fenceKeyboardHint": "F6: 도구로 이동 · ← →: 선택 · Enter: 적용 · Esc: 입력으로",
355
359
  "suggestion.transformFence": "괄호 변경: {kind}",
356
- "suggestion.transformFenceDetail": "안의 수식과 커서 위치를 유지합니다"
360
+ "suggestion.transformFenceDetail": "안의 수식과 커서 위치를 유지합니다",
361
+ "help.title": "단축키와 복사·붙여넣기",
362
+ "help.intro": "키보드로 수식을 편집하는 방법입니다. 도움말을 닫으면 수식으로 돌아갑니다.",
363
+ "help.units": "변수·숫자·구조 단위로 이동합니다. Shift를 함께 누르면 선택합니다. 구조 안에서는 현재 항목의 경계로 이동한 뒤 구조 밖으로 나갑니다.",
364
+ "help.slots": "다음 또는 이전 입력 칸으로 이동합니다.",
365
+ "help.select": "수식의 선택 범위를 늘립니다.",
366
+ "help.wrap": "수식을 선택한 상태에서 괄호·절대값·분수·지수·아래첨자로 감쌉니다.",
367
+ "help.suggestions": "툴바 없이도 제안 목록과 가능한 변형을 엽니다.",
368
+ "help.menu": "제안을 고르고 적용하거나 목록을 닫습니다.",
369
+ "help.clipboard": "복사·잘라내기·붙여넣기입니다. 브라우저가 편집기 데이터를 유지하면 구조도 보존됩니다. 일반 텍스트는 글자로 붙여넣습니다.",
370
+ "help.latex": "LaTeX 붙여넣기를 엽니다. 원문을 붙여넣고 Ctrl/Cmd+Enter로 삽입합니다. 잘못된 입력은 기존 수식을 바꾸지 않습니다.",
371
+ "help.history": "편집을 실행 취소하거나 다시 실행합니다.",
372
+ "help.lines": "Enter는 모드와 연동 설정을 따릅니다. Shift+Enter는 행렬의 행 또는 블록 수식의 줄을 추가합니다. 인라인 모드는 수식의 바깥 줄을 추가하지 않습니다.",
373
+ "help.open": "수식에 포커스가 있을 때 도움말을 엽니다.",
374
+ "help.platform": "macOS에서는 Cmd, Windows/Linux에서는 Ctrl을 사용합니다. macOS의 Alt는 Option입니다. 일부 키보드는 Fn+F1이 필요합니다. 운영체제나 연동 편집기의 단축키가 우선할 수 있습니다."
357
375
  },
358
376
  "aliases": {
359
377
  "function-sin": ["사인"],
@@ -91,7 +91,8 @@
91
91
  "grid.addColumn": "열 추가",
92
92
  "grid.deleteColumn": "열 삭제",
93
93
  "grid.deleteRowShortcut": "행 삭제: Alt(⌥)+Shift+↑",
94
- "grid.deleteColumnShortcut": "열 삭제: Alt(⌥)+Shift+←",
94
+ "grid.deleteColumnShortcut": "열 삭제: Alt(⌥)+Shift+Backspace",
95
+ "clipboard.invalid": "복사한 수식 데이터가 올바르지 않습니다. 기존 수식은 유지됩니다.",
95
96
  "clipboard.cut": "선택한 수식을 잘라냈습니다.",
96
97
  "clipboard.copied": "선택한 수식을 구조와 LaTeX로 복사했습니다.",
97
98
  "clipboard.pasted": "붙여넣었습니다.",
@@ -100,13 +101,15 @@
100
101
  "grid.selectionHint": "수식 구조 전체 선택 · Backspace/Delete로 삭제 · Esc로 취소",
101
102
  "selection.toolbar": "선택한 수식 감싸기",
102
103
  "selection.highlighted": "강조된 범위 감싸기",
104
+ "selection.asNumerator": "분자로 넣기",
105
+ "selection.asDenominator": "분모로 넣기",
103
106
  "selection.singleRowRequired": "한 줄 안의 범위를 선택해 주세요.",
104
107
  "suggestion.label": "수식 추천",
105
108
  "suggestion.composing": "한글 조합 중 · 확정 후 선택",
106
109
  "suggestion.keyboardHint": "↑ ↓ 선택 · Enter 적용",
107
110
  "suggestion.imeHint": "후보 미리보기 · Enter와 방향키는 한글 입력에 사용됩니다",
108
111
  "suggestion.keepSource": "계속 입력하거나 Space · Esc로 원문 유지",
109
- "matrix.keyboardHint": "Shift+Enter 행 추가 · Shift+Space 열 추가 · Alt(⌥)+Shift+↑ 행 삭제 · Alt(⌥)+Shift+← 열 삭제",
112
+ "matrix.keyboardHint": "Shift+Enter 행 추가 · Shift+Space 열 추가 · Alt(⌥)+Shift+↑ 행 삭제 · Alt(⌥)+Shift+Backspace 열 삭제",
110
113
  "aligned.inputHint": "왼쪽: 좌변 · 오른쪽: =부터 입력",
111
114
  "cases.inputHint": "왼쪽: 식 · 오른쪽: 조건",
112
115
  "grid.keyboardHint": "Enter 행 추가 · Alt(⌥)+Shift+↑ 행 삭제",
@@ -154,6 +157,7 @@
154
157
  "template.detail": "편집 가능한 수식 템플릿",
155
158
  "template.quadratic": "근의 공식",
156
159
  "template.pythagorean": "피타고라스 정리",
160
+ "symbol.prime": "프라임",
157
161
  "symbol.degree": "도",
158
162
  "symbol.angle": "각 기호",
159
163
  "symbol.plus": "더하기",
@@ -352,7 +356,21 @@
352
356
  "context.indexedRoot": "n제곱근",
353
357
  "context.fenceKeyboardHint": "F6: 도구로 이동 · ← →: 선택 · Enter: 적용 · Esc: 입력으로",
354
358
  "suggestion.transformFence": "괄호 변경: {kind}",
355
- "suggestion.transformFenceDetail": "안의 수식과 커서 위치를 유지합니다"
359
+ "suggestion.transformFenceDetail": "안의 수식과 커서 위치를 유지합니다",
360
+ "help.title": "단축키와 복사·붙여넣기",
361
+ "help.intro": "키보드로 수식을 편집하는 방법입니다. 도움말을 닫으면 수식으로 돌아갑니다.",
362
+ "help.units": "변수·숫자·구조 단위로 이동합니다. Shift를 함께 누르면 선택합니다. 구조 안에서는 현재 항목의 경계로 이동한 뒤 구조 밖으로 나갑니다.",
363
+ "help.slots": "다음 또는 이전 입력 칸으로 이동합니다.",
364
+ "help.select": "수식의 선택 범위를 늘립니다.",
365
+ "help.wrap": "수식을 선택한 상태에서 괄호·절대값·분수·지수·아래첨자로 감쌉니다.",
366
+ "help.suggestions": "툴바 없이도 제안 목록과 가능한 변형을 엽니다.",
367
+ "help.menu": "제안을 고르고 적용하거나 목록을 닫습니다.",
368
+ "help.clipboard": "복사·잘라내기·붙여넣기입니다. 브라우저가 편집기 데이터를 유지하면 구조도 보존됩니다. 일반 텍스트는 글자로 붙여넣습니다.",
369
+ "help.latex": "LaTeX 붙여넣기를 엽니다. 원문을 붙여넣고 Ctrl/Cmd+Enter로 삽입합니다. 잘못된 입력은 기존 수식을 바꾸지 않습니다.",
370
+ "help.history": "편집을 실행 취소하거나 다시 실행합니다.",
371
+ "help.lines": "Enter는 모드와 연동 설정을 따릅니다. Shift+Enter는 행렬의 행 또는 블록 수식의 줄을 추가합니다. 인라인 모드는 수식의 바깥 줄을 추가하지 않습니다.",
372
+ "help.open": "수식에 포커스가 있을 때 도움말을 엽니다.",
373
+ "help.platform": "macOS에서는 Cmd, Windows/Linux에서는 Ctrl을 사용합니다. macOS의 Alt는 Option입니다. 일부 키보드는 Fn+F1이 필요합니다. 운영체제나 연동 편집기의 단축키가 우선할 수 있습니다."
356
374
  },
357
375
  "aliases": {
358
376
  "function-sin": ["사인"],
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { requestMathHelp } from './dom/help.js';
2
3
  import { useState } from 'react';
3
4
  import { mathStructures } from './suggestions.js';
4
5
  import { wrappingKinds } from './range.js';
@@ -17,7 +18,7 @@ export function MathEditorToolbar({ locale, kinds, maxItems = 8, excludedStructu
17
18
  const visibleCount = Math.max(0, Math.floor(Number.isFinite(maxItems) ? maxItems : 8));
18
19
  const items = mathStructures.filter((s) => s.kind && !excludedStructures.includes(s.kind) && (!kinds || kinds.includes(s.kind)));
19
20
  const t = (key, parameters) => translate(locale, key, parameters);
20
- return (_jsxs("div", { className: "me-tools", role: "toolbar", "aria-label": t('toolbar.label'), children: [(expanded ? items : items.slice(0, visibleCount)).map((s) => (_jsxs("button", { type: "button", "aria-label": t(s.label), disabled: composing || (hasRange && (!canWrap || !wrappingKinds.includes(s.kind))), onMouseDown: (event) => event.preventDefault(), onClick: () => onInsertStructure(s.kind), children: [_jsx("span", { children: s.glyph }), t(s.label)] }, s.kind))), expanded && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", "data-math-tool": "quick", disabled: composing || cellRange, onMouseDown: (event) => event.preventDefault(), onClick: onQuick, children: t('quick.title') }), _jsx("button", { type: "button", "data-math-tool": "paste-latex", disabled: composing || cellRange, onMouseDown: (event) => event.preventDefault(), onClick: onPasteLatex, title: t('clipboard.latexShortcut'), children: t('clipboard.latexTitle') }), _jsxs("select", { disabled: composing || hasRange, className: "me-matrix-size", "aria-label": t('matrix.preset'), value: "", onChange: (event) => {
21
+ return (_jsxs("div", { className: "me-tools", role: "toolbar", "aria-label": t('toolbar.label'), children: [_jsx("button", { type: "button", "data-math-tool": "help", disabled: composing, title: `${t('help.title')} (F1)`, "aria-keyshortcuts": "F1", onMouseDown: (event) => event.preventDefault(), onClick: (event) => requestMathHelp(event.currentTarget), children: t('help.title') }), (expanded ? items : items.slice(0, visibleCount)).map((s) => (_jsxs("button", { type: "button", "aria-label": t(s.label), disabled: composing || (hasRange && (!canWrap || !wrappingKinds.includes(s.kind))), onMouseDown: (event) => event.preventDefault(), onClick: () => onInsertStructure(s.kind), children: [_jsx("span", { children: s.glyph }), t(s.label)] }, s.kind))), expanded && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", "data-math-tool": "quick", disabled: composing || cellRange, onMouseDown: (event) => event.preventDefault(), onClick: onQuick, children: t('quick.title') }), _jsx("button", { type: "button", "data-math-tool": "paste-latex", disabled: composing || cellRange, onMouseDown: (event) => event.preventDefault(), onClick: onPasteLatex, title: t('clipboard.latexShortcut'), children: t('clipboard.latexTitle') }), _jsxs("select", { disabled: composing || hasRange, className: "me-matrix-size", "aria-label": t('matrix.preset'), value: "", onChange: (event) => {
21
22
  const value = event.target.value;
22
23
  onMatrixPreset(value);
23
24
  }, children: [_jsx("option", { value: "", disabled: true, children: t('matrix.presetPlaceholder') }), [2, 3, 4].map((size) => (_jsx("option", { value: size, children: t('matrix.insert', { rows: size, columns: size }) }, size))), [2, 3, 4].map((size) => (_jsx("option", { value: `identity-${size}`, children: t('matrix.insertIdentity', { rows: size, columns: size }) }, `identity-${size}`)))] }), _jsxs("select", { className: "me-matrix-size", "aria-label": t('template.label'), value: "", disabled: composing || cellRange, onChange: (event) => onTemplate(event.target.value), children: [_jsx("option", { value: "", disabled: true, children: t('template.placeholder') }), mathTemplates.map((item) => (_jsx("option", { value: item.id, children: t(item.label, item.labelParameters) }, item.id)))] }), _jsx("button", { type: "button", "aria-expanded": symbolsOpen, disabled: composing || cellRange, onClick: onToggleSymbols, children: t('symbols.title') }), _jsx("span", { className: "me-divider" }), symbols.map((s) => (_jsx("button", { type: "button", "aria-label": t(s.label), disabled: composing || hasRange, onMouseDown: (event) => event.preventDefault(), onClick: () => onInsertSymbol(s.value), children: s.value }, s.label)))] })), _jsx("button", { type: "button", className: "me-toolbar-toggle", "aria-expanded": expanded, onMouseDown: (event) => event.preventDefault(), onClick: () => setExpanded((value) => !value), children: t(expanded ? 'toolbar.less' : 'toolbar.more') }), _jsx("button", { type: "button", disabled: !canUndo || composing, onMouseDown: (event) => event.preventDefault(), onClick: onUndo, children: t('history.undo') }), _jsx("button", { type: "button", disabled: !canRedo || composing, onMouseDown: (event) => event.preventDefault(), onClick: onRedo, children: t('history.redo') }), children] }));