@barocss/math-editor 0.2.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/ADAPTERS.md +25 -14
  2. package/API-JAVASCRIPT.md +1 -1
  3. package/API-REACT.md +5 -1
  4. package/API-SESSION.md +178 -6
  5. package/API-SOLID.md +1 -1
  6. package/API-SVELTE.md +1 -1
  7. package/API-VUE.md +1 -1
  8. package/API-WEB-COMPONENT.md +3 -1
  9. package/CHANGELOG.md +106 -0
  10. package/EDITING-SCENARIOS.md +174 -0
  11. package/EMBEDDING.md +4 -0
  12. package/IMPLEMENTATION.md +122 -2
  13. package/JSON-MODEL.md +1 -0
  14. package/LATEX-GUIDE.md +30 -1
  15. package/LATEX-MODEL.md +12 -0
  16. package/LATEX-SCOPE.md +12 -3
  17. package/LICENSE +1 -3
  18. package/README.md +201 -39
  19. package/RELEASING.md +81 -10
  20. package/RENDERING-TESTS.md +79 -0
  21. package/ROADMAP.md +220 -11
  22. package/STYLING.md +157 -0
  23. package/SUPPORT.md +24 -12
  24. package/VALIDATION.md +444 -4
  25. package/dist/context-tools.d.ts +21 -0
  26. package/dist/context-tools.js +37 -0
  27. package/dist/core.d.ts +5 -1
  28. package/dist/core.js +5 -1
  29. package/dist/document-codec.d.ts +0 -1
  30. package/dist/document-codec.js +0 -1
  31. package/dist/dom/caret-geometry.d.ts +3 -0
  32. package/dist/dom/caret-geometry.js +41 -0
  33. package/dist/dom/context-keyboard.d.ts +2 -0
  34. package/dist/dom/context-keyboard.js +22 -0
  35. package/dist/dom/elements.d.ts +1 -2
  36. package/dist/dom/elements.js +2 -3
  37. package/dist/dom/latex-panel.d.ts +10 -0
  38. package/dist/dom/latex-panel.js +59 -0
  39. package/dist/dom/matrix-selection.d.ts +11 -0
  40. package/dist/dom/matrix-selection.js +30 -0
  41. package/dist/dom/menu-position.d.ts +17 -2
  42. package/dist/dom/menu-position.js +129 -16
  43. package/dist/dom/presentation-controls.d.ts +4 -0
  44. package/dist/dom/presentation-controls.js +56 -0
  45. package/dist/dom/quick-panel.d.ts +12 -0
  46. package/dist/dom/quick-panel.js +102 -0
  47. package/dist/dom/selection.d.ts +29 -0
  48. package/dist/dom/selection.js +53 -0
  49. package/dist/dom/symbol-panel.d.ts +9 -0
  50. package/dist/dom/symbol-panel.js +146 -0
  51. package/dist/dom/theme.d.ts +6 -0
  52. package/dist/dom/theme.js +68 -0
  53. package/dist/dom/toolbar-catalog.d.ts +10 -0
  54. package/dist/dom/toolbar-catalog.js +35 -0
  55. package/dist/dom/toolbar.d.ts +13 -2
  56. package/dist/dom/toolbar.js +295 -28
  57. package/dist/dom.d.ts +9 -1
  58. package/dist/dom.js +682 -151
  59. package/dist/editor-labels.d.ts +0 -1
  60. package/dist/editor-labels.js +0 -1
  61. package/dist/enter-policy.d.ts +0 -1
  62. package/dist/enter-policy.js +0 -1
  63. package/dist/fences.d.ts +0 -1
  64. package/dist/fences.js +0 -1
  65. package/dist/i18n.d.ts +0 -1
  66. package/dist/i18n.js +0 -1
  67. package/dist/index.d.ts +4 -1
  68. package/dist/index.js +4 -1
  69. package/dist/latex-insertion.d.ts +12 -0
  70. package/dist/latex-insertion.js +26 -0
  71. package/dist/latex-paste-panel.d.ts +3 -0
  72. package/dist/latex-paste-panel.js +18 -0
  73. package/dist/latex.d.ts +0 -1
  74. package/dist/latex.js +11 -1
  75. package/dist/lines.d.ts +0 -1
  76. package/dist/lines.js +0 -1
  77. package/dist/locales/en.js +39 -1
  78. package/dist/locales/en.json +39 -1
  79. package/dist/locales/ko.js +39 -1
  80. package/dist/locales/ko.json +39 -1
  81. package/dist/math-editor-toolbar.d.ts +4 -2
  82. package/dist/math-editor-toolbar.js +3 -4
  83. package/dist/math-editor.d.ts +7 -2
  84. package/dist/math-editor.js +661 -308
  85. package/dist/math-layout.d.ts +12 -0
  86. package/dist/math-layout.js +59 -0
  87. package/dist/math-spacing.d.ts +13 -0
  88. package/dist/math-spacing.js +87 -0
  89. package/dist/matrix-range.d.ts +40 -0
  90. package/dist/matrix-range.js +242 -0
  91. package/dist/matrix.d.ts +0 -1
  92. package/dist/matrix.js +0 -1
  93. package/dist/model.d.ts +4 -1
  94. package/dist/model.js +49 -5
  95. package/dist/outputs.d.ts +0 -1
  96. package/dist/outputs.js +0 -1
  97. package/dist/preferences.d.ts +24 -0
  98. package/dist/preferences.js +66 -0
  99. package/dist/presentation-controls.d.ts +10 -0
  100. package/dist/presentation-controls.js +10 -0
  101. package/dist/presentation.d.ts +13 -0
  102. package/dist/presentation.js +86 -0
  103. package/dist/quick-panel.d.ts +2 -0
  104. package/dist/quick-panel.js +17 -0
  105. package/dist/range.d.ts +2 -1
  106. package/dist/range.js +11 -1
  107. package/dist/react.d.ts +0 -1
  108. package/dist/react.js +0 -1
  109. package/dist/root-transform.d.ts +19 -0
  110. package/dist/root-transform.js +69 -0
  111. package/dist/session.d.ts +31 -2
  112. package/dist/session.js +108 -7
  113. package/dist/solid.d.ts +0 -1
  114. package/dist/solid.js +0 -1
  115. package/dist/suggestions.d.ts +13 -1
  116. package/dist/suggestions.js +153 -12
  117. package/dist/svelte.d.ts +0 -1
  118. package/dist/svelte.js +0 -1
  119. package/dist/symbol-browser.d.ts +0 -1
  120. package/dist/symbol-browser.js +0 -1
  121. package/dist/symbols.d.ts +0 -1
  122. package/dist/symbols.js +0 -1
  123. package/dist/templates.d.ts +0 -1
  124. package/dist/templates.js +0 -1
  125. package/dist/tokens.d.ts +0 -1
  126. package/dist/tokens.js +0 -1
  127. package/dist/vertical-navigation.d.ts +15 -0
  128. package/dist/vertical-navigation.js +145 -0
  129. package/dist/vue.d.ts +0 -1
  130. package/dist/vue.js +0 -1
  131. package/dist/web-component.d.ts +0 -1
  132. package/dist/web-component.js +9 -2
  133. package/package.json +11 -3
  134. package/src/fonts/KaTeX_Math-Italic.woff2 +0 -0
  135. package/src/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  136. package/src/fonts/README.md +18 -0
  137. package/src/shapes/README.md +21 -0
  138. package/src/shapes/parenthesis-bottom.svg +1 -0
  139. package/src/shapes/parenthesis-top.svg +1 -0
  140. package/src/shapes/parenthesis.svg +1 -0
  141. package/src/shapes/radical.svg +1 -0
  142. package/src/style.css +949 -173
  143. package/dist/core.d.ts.map +0 -1
  144. package/dist/core.js.map +0 -1
  145. package/dist/document-codec.d.ts.map +0 -1
  146. package/dist/document-codec.js.map +0 -1
  147. package/dist/dom/elements.d.ts.map +0 -1
  148. package/dist/dom/elements.js.map +0 -1
  149. package/dist/dom/menu-position.d.ts.map +0 -1
  150. package/dist/dom/menu-position.js.map +0 -1
  151. package/dist/dom/toolbar.d.ts.map +0 -1
  152. package/dist/dom/toolbar.js.map +0 -1
  153. package/dist/dom.d.ts.map +0 -1
  154. package/dist/dom.js.map +0 -1
  155. package/dist/editor-labels.d.ts.map +0 -1
  156. package/dist/editor-labels.js.map +0 -1
  157. package/dist/enter-policy.d.ts.map +0 -1
  158. package/dist/enter-policy.js.map +0 -1
  159. package/dist/fences.d.ts.map +0 -1
  160. package/dist/fences.js.map +0 -1
  161. package/dist/i18n.d.ts.map +0 -1
  162. package/dist/i18n.js.map +0 -1
  163. package/dist/index.d.ts.map +0 -1
  164. package/dist/index.js.map +0 -1
  165. package/dist/latex.d.ts.map +0 -1
  166. package/dist/latex.js.map +0 -1
  167. package/dist/lines.d.ts.map +0 -1
  168. package/dist/lines.js.map +0 -1
  169. package/dist/math-editor-toolbar.d.ts.map +0 -1
  170. package/dist/math-editor-toolbar.js.map +0 -1
  171. package/dist/math-editor.d.ts.map +0 -1
  172. package/dist/math-editor.js.map +0 -1
  173. package/dist/matrix.d.ts.map +0 -1
  174. package/dist/matrix.js.map +0 -1
  175. package/dist/model.d.ts.map +0 -1
  176. package/dist/model.js.map +0 -1
  177. package/dist/outputs.d.ts.map +0 -1
  178. package/dist/outputs.js.map +0 -1
  179. package/dist/range.d.ts.map +0 -1
  180. package/dist/range.js.map +0 -1
  181. package/dist/react.d.ts.map +0 -1
  182. package/dist/react.js.map +0 -1
  183. package/dist/session.d.ts.map +0 -1
  184. package/dist/session.js.map +0 -1
  185. package/dist/solid.d.ts.map +0 -1
  186. package/dist/solid.js.map +0 -1
  187. package/dist/suggestions.d.ts.map +0 -1
  188. package/dist/suggestions.js.map +0 -1
  189. package/dist/svelte.d.ts.map +0 -1
  190. package/dist/svelte.js.map +0 -1
  191. package/dist/symbol-browser.d.ts.map +0 -1
  192. package/dist/symbol-browser.js.map +0 -1
  193. package/dist/symbols.d.ts.map +0 -1
  194. package/dist/symbols.js.map +0 -1
  195. package/dist/templates.d.ts.map +0 -1
  196. package/dist/templates.js.map +0 -1
  197. package/dist/tokens.d.ts.map +0 -1
  198. package/dist/tokens.js.map +0 -1
  199. package/dist/vue.d.ts.map +0 -1
  200. package/dist/vue.js.map +0 -1
  201. package/dist/web-component.d.ts.map +0 -1
  202. package/dist/web-component.js.map +0 -1
@@ -0,0 +1,10 @@
1
+ import type { MathState, StructureKind } from './model.js';
2
+ import type { MathPresentationChange } from './presentation.js';
3
+ import type { MathLocale } from './i18n.js';
4
+ export declare function PresentationControls(props: {
5
+ state: MathState;
6
+ locale: MathLocale;
7
+ disabled: boolean;
8
+ kinds?: readonly StructureKind[];
9
+ onChange(id: string, change: MathPresentationChange): void;
10
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef } from 'react';
3
+ import { drawPresentationControls } from './dom/presentation-controls.js';
4
+ export function PresentationControls(props) {
5
+ const host = useRef(null);
6
+ useLayoutEffect(() => {
7
+ drawPresentationControls(host.current, props.state, props.locale, props.onChange, props.disabled, props.kinds);
8
+ });
9
+ return _jsx("div", { ref: host });
10
+ }
@@ -0,0 +1,13 @@
1
+ import { type MathState, type MathStructure, type StructureKind } from './model.js';
2
+ export type MathPresentationChange = {
3
+ fence: string;
4
+ } | {
5
+ mathStyle: 'default' | 'display' | 'text';
6
+ } | {
7
+ limits: 'default' | 'above-below' | 'side';
8
+ };
9
+ export declare const limitKinds: readonly StructureKind[];
10
+ /** Nearest containing structure with presentation settings. Nested content is never flattened. */
11
+ export declare function activePresentation(state: MathState): MathStructure | undefined;
12
+ /** Change metadata only. Preserve node/slot IDs, contents and the model caret. */
13
+ export declare function setMathPresentation(state: MathState, id: string, change: MathPresentationChange): MathState;
@@ -0,0 +1,86 @@
1
+ import { documentRows, isGrid, } from './model.js';
2
+ import { fencePairs, fences } from './fences.js';
3
+ export const limitKinds = [
4
+ 'sum',
5
+ 'product',
6
+ 'integral',
7
+ 'doubleIntegral',
8
+ 'tripleIntegral',
9
+ 'contourIntegral',
10
+ 'limit',
11
+ 'limsup',
12
+ 'liminf',
13
+ ];
14
+ const editable = (node) => !!fences(node) || ['fraction', 'binomial'].includes(node.type) || limitKinds.includes(node.type);
15
+ /** Nearest containing structure with presentation settings. Nested content is never flattened. */
16
+ export function activePresentation(state) {
17
+ const visit = (row) => {
18
+ for (const node of row.children) {
19
+ if (node.type === 'text') {
20
+ if (node.id === state.caret.id)
21
+ return { contains: true };
22
+ }
23
+ else
24
+ for (const slot of node.slots) {
25
+ const child = visit(slot);
26
+ if (child.contains)
27
+ return { contains: true, target: child.target ?? (editable(node) ? node : undefined) };
28
+ }
29
+ }
30
+ return { contains: false };
31
+ };
32
+ for (const row of documentRows(state.document)) {
33
+ const result = visit(row);
34
+ if (result.contains)
35
+ return result.target;
36
+ }
37
+ }
38
+ /** Change metadata only. Preserve node/slot IDs, contents and the model caret. */
39
+ export function setMathPresentation(state, id, change) {
40
+ const document = structuredClone(state.document);
41
+ const find = (row) => {
42
+ for (const node of row.children)
43
+ if (node.type !== 'text') {
44
+ if (node.id === id)
45
+ return node;
46
+ for (const slot of node.slots) {
47
+ const found = find(slot);
48
+ if (found)
49
+ return found;
50
+ }
51
+ }
52
+ };
53
+ const node = documentRows(document).map(find).find(Boolean);
54
+ if (!node || isGrid(node))
55
+ return state;
56
+ if ('fence' in change) {
57
+ if (!fences(node) || !Object.hasOwn(fencePairs, change.fence))
58
+ return state;
59
+ node.type = change.fence;
60
+ delete node.left;
61
+ delete node.right;
62
+ }
63
+ else if ('mathStyle' in change) {
64
+ if (!['fraction', 'binomial'].includes(node.type) ||
65
+ !['default', 'display', 'text'].includes(change.mathStyle))
66
+ return state;
67
+ if (change.mathStyle === 'default')
68
+ delete node.mathStyle;
69
+ else
70
+ node.mathStyle = change.mathStyle;
71
+ }
72
+ else if ('limits' in change) {
73
+ if (!limitKinds.includes(node.type) ||
74
+ !['default', 'above-below', 'side'].includes(change.limits))
75
+ return state;
76
+ if (change.limits === 'default')
77
+ delete node.limits;
78
+ else
79
+ node.limits = change.limits === 'above-below';
80
+ }
81
+ else
82
+ return state;
83
+ return JSON.stringify(document) === JSON.stringify(state.document)
84
+ ? state
85
+ : { document, caret: { ...state.caret } };
86
+ }
@@ -0,0 +1,2 @@
1
+ import { mountQuickPanel } from './dom/quick-panel.js';
2
+ export declare function QuickPanel(props: Parameters<typeof mountQuickPanel>[1]): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef } from 'react';
3
+ import { mountQuickPanel } from './dom/quick-panel.js';
4
+ export function QuickPanel(props) {
5
+ const host = useRef(null);
6
+ const current = useRef(props);
7
+ current.current = props;
8
+ useLayoutEffect(() => {
9
+ const panel = mountQuickPanel(host.current, {
10
+ ...props,
11
+ onInsert: (item) => current.current.onInsert(item),
12
+ onClose: () => current.current.onClose(),
13
+ });
14
+ return () => panel.destroy();
15
+ }, [props.locale, props.preferences, props.allowedTemplates?.join(',')]);
16
+ return _jsx("div", { ref: host });
17
+ }
package/dist/range.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface MathFragment {
11
11
  version: 1;
12
12
  rows: MathRow[];
13
13
  }
14
+ /** Plain arrows leave a model selection at its document-ordered edge. */
15
+ export declare function collapseKeyboardRange(document: MathDocument, range: MathRange | undefined, key: string): MathPoint | undefined;
14
16
  export declare function resolveRange(document: MathDocument, range: MathRange): {
15
17
  row: MathRow;
16
18
  start: number;
@@ -29,4 +31,3 @@ export type WrappingKind = (typeof wrappingKinds)[number];
29
31
  export declare function wrapRange(state: MathState, range: MathRange, kind: WrappingKind): MathState;
30
32
  /** Keyboard ranges share drag-selection normalization. Cross a structure as one atom. */
31
33
  export declare function extendKeyboardRange(state: MathState, range: MathRange | undefined, key: string): MathRange;
32
- //# sourceMappingURL=range.d.ts.map
package/dist/range.js CHANGED
@@ -33,6 +33,17 @@ function ordered(document, range) {
33
33
  ? { a: b, b: a, start: range.focus, end: range.anchor }
34
34
  : { a, b, start: range.anchor, end: range.focus };
35
35
  }
36
+ /** Plain arrows leave a model selection at its document-ordered edge. */
37
+ export function collapseKeyboardRange(document, range, key) {
38
+ if (!range || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(key))
39
+ return;
40
+ const bounds = ordered(document, range);
41
+ if (!bounds)
42
+ return;
43
+ const point = key === 'ArrowLeft' || key === 'ArrowUp' ? bounds.start : bounds.end;
44
+ const node = textNodes(document).find((node) => node.id === point.id);
45
+ return { id: point.id, offset: Math.max(0, Math.min(point.offset, node.text.length)) };
46
+ }
36
47
  function offset(p, depth, point, end) {
37
48
  const at = p[depth], base = at.row.children.slice(0, at.index).reduce((n, node) => n + size(node), 0);
38
49
  return (base +
@@ -428,4 +439,3 @@ export function extendKeyboardRange(state, range, key) {
428
439
  const target = direction < 0 ? nodes.at(-1) : nodes[0];
429
440
  return { anchor, focus: { id: target.id, offset: direction < 0 ? target.text.length : 0 } };
430
441
  }
431
- //# sourceMappingURL=range.js.map
package/dist/react.d.ts CHANGED
@@ -8,4 +8,3 @@ export declare function MathEditorSurface(props: DOMMathEditorOptions & {
8
8
  ref: import("react").RefObject<HTMLSpanElement | null>;
9
9
  className: string | undefined;
10
10
  }, HTMLSpanElement>;
11
- //# sourceMappingURL=react.d.ts.map
package/dist/react.js CHANGED
@@ -17,4 +17,3 @@ export function MathEditorSurface(props) {
17
17
  });
18
18
  return createElement('span', { ref: host, className: props.className });
19
19
  }
20
- //# sourceMappingURL=react.js.map
@@ -0,0 +1,19 @@
1
+ import { type MathState, type MathStructure } from './model.js';
2
+ /** Use the nearest enclosing radical, including from one of its nested slots. */
3
+ export declare function activeRoot(state: MathState): MathStructure | undefined;
4
+ /** An explicit non-square index cannot be discarded by a presentation change. */
5
+ export declare function canUseSquareRoot(root: MathStructure): boolean;
6
+ /** Preserve the radicand's entire tree and IDs, and focus the newly editable index. */
7
+ export declare function transformRoot(state: MathState, targetId: string, kind: 'root' | 'indexedRoot'): MathState;
8
+ /** Footer context is independent of the typed query and suggestion dismissal. */
9
+ export declare function rootEditingContext(state: MathState): {
10
+ id: string;
11
+ kind: "root" | "indexedRoot";
12
+ target: "root" | "indexedRoot";
13
+ canTransform: boolean;
14
+ indexCaret: {
15
+ id: string;
16
+ start: number;
17
+ end: number;
18
+ } | undefined;
19
+ } | undefined;
@@ -0,0 +1,69 @@
1
+ import { documentRows, isLiteralText, row, textNodes, } from './model.js';
2
+ /** Use the nearest enclosing radical, including from one of its nested slots. */
3
+ export function activeRoot(state) {
4
+ if (isLiteralText(state.document, state.caret.id))
5
+ return;
6
+ const visit = (line, parent) => {
7
+ for (const node of line.children) {
8
+ if (node.type === 'text') {
9
+ if (node.id === state.caret.id)
10
+ return parent;
11
+ }
12
+ else {
13
+ for (const slot of node.slots) {
14
+ const found = visit(slot, ['root', 'indexedRoot'].includes(node.type) ? node : parent);
15
+ if (found)
16
+ return found;
17
+ }
18
+ }
19
+ }
20
+ };
21
+ for (const line of documentRows(state.document)) {
22
+ const found = visit(line);
23
+ if (found)
24
+ return found;
25
+ }
26
+ }
27
+ /** An explicit non-square index cannot be discarded by a presentation change. */
28
+ export function canUseSquareRoot(root) {
29
+ if (root.type !== 'indexedRoot')
30
+ return false;
31
+ const index = root.slots[0];
32
+ if (index.children.some((node) => node.type !== 'text'))
33
+ return false;
34
+ const value = index.children
35
+ .map((node) => (node.type === 'text' ? node.text : ''))
36
+ .join('')
37
+ .trim();
38
+ return value === '' || value === '2';
39
+ }
40
+ /** Preserve the radicand's entire tree and IDs, and focus the newly editable index. */
41
+ export function transformRoot(state, targetId, kind) {
42
+ const current = activeRoot(state);
43
+ if (!current || current.id !== targetId || current.type === kind)
44
+ return state;
45
+ if (kind === 'root' && !canUseSquareRoot(current))
46
+ return state;
47
+ const next = { ...state, document: structuredClone(state.document) };
48
+ const root = activeRoot(next);
49
+ const radicand = root.slots[root.type === 'root' ? 0 : 1];
50
+ root.type = kind;
51
+ // Start at 2 to retain the formula's meaning; select it so typing n replaces it.
52
+ root.slots = kind === 'indexedRoot' ? [row('2'), radicand] : [radicand];
53
+ const first = textNodes({ version: 1, root: root.slots[0] })[0];
54
+ return { ...next, caret: { id: first.id, start: 0, end: kind === 'indexedRoot' ? 1 : 0 } };
55
+ }
56
+ /** Footer context is independent of the typed query and suggestion dismissal. */
57
+ export function rootEditingContext(state) {
58
+ const root = activeRoot(state);
59
+ if (!root)
60
+ return;
61
+ const index = root.type === 'indexedRoot' ? textNodes({ version: 1, root: root.slots[0] })[0] : undefined;
62
+ return {
63
+ id: root.id,
64
+ kind: root.type,
65
+ target: root.type === 'root' ? 'indexedRoot' : 'root',
66
+ canTransform: root.type === 'root' || canUseSquareRoot(root),
67
+ indexCaret: index ? { id: index.id, start: 0, end: index.text.length } : undefined,
68
+ };
69
+ }
package/dist/session.d.ts CHANGED
@@ -1,14 +1,24 @@
1
+ import { type MathPresentationChange } from './presentation.js';
2
+ import { type MathPreferences } from './preferences.js';
3
+ import { type LatexInsertionResult } from './latex-insertion.js';
1
4
  import { type LatexParseResult } from './latex.js';
2
5
  import { type MathDocument, type MathState, type MathCaret, type StructureKind } from './model.js';
3
6
  import { type MathRange, type MathFragment } from './range.js';
4
7
  import type { MathLocale } from './i18n.js';
8
+ import { type MathMatrixRange, type MathMatrixFragment } from './matrix-range.js';
5
9
  export type MathEditorMode = 'block' | 'inline';
6
10
  export interface MathSessionOptions {
11
+ /** UI-only preference store. Supply one store to share it across editors. */
12
+ preferences?: MathPreferences;
7
13
  document?: MathDocument;
8
14
  mode?: MathEditorMode;
9
15
  locale?: MathLocale;
10
16
  }
11
17
  export type MathCommand = {
18
+ type: 'presentation';
19
+ id: string;
20
+ change: MathPresentationChange;
21
+ } | {
12
22
  type: 'text';
13
23
  value: string;
14
24
  } | {
@@ -23,10 +33,18 @@ export type MathCommand = {
23
33
  type: 'undo';
24
34
  } | {
25
35
  type: 'redo';
36
+ } | {
37
+ type: 'matrix-select';
38
+ all?: boolean;
39
+ } | {
40
+ type: 'matrix-transpose';
41
+ } | {
42
+ type: 'matrix-clear';
26
43
  };
27
44
  export interface MathSessionSnapshot {
28
45
  state: MathState;
29
46
  range?: MathRange;
47
+ matrixRange?: MathMatrixRange;
30
48
  locale: MathLocale;
31
49
  mode: MathEditorMode;
32
50
  canUndo: boolean;
@@ -35,14 +53,26 @@ export interface MathSessionSnapshot {
35
53
  }
36
54
  /** Framework-free session. Snapshots are copies; consumers cannot mutate history. */
37
55
  export declare function createMathSession(options?: MathSessionOptions): {
56
+ preferences: {
57
+ getSnapshot: () => import("./preferences.js").MathPreferencesSnapshot;
58
+ record(id: string): void;
59
+ toggleFavorite(id: string): void;
60
+ clearRecent(): void;
61
+ subscribe(listener: () => void): () => void;
62
+ };
38
63
  getSnapshot: () => MathSessionSnapshot;
39
64
  subscribe(listener: (snapshot: MathSessionSnapshot, documentChanged: boolean) => void): () => void;
40
65
  apply: (next: MathState) => boolean;
41
66
  select(caret: MathCaret): void;
42
67
  selectRange(next?: MathRange): void;
43
- configure(next: Omit<MathSessionOptions, "document">): void;
68
+ selectMatrixRange(next?: MathMatrixRange): boolean;
69
+ copyMatrix(): MathMatrixFragment | undefined;
70
+ pasteMatrix(fragment: MathMatrixFragment): boolean;
71
+ configure(next: Omit<MathSessionOptions, "document" | "preferences">): void;
44
72
  /** Atomic replacement in the existing history. Syntax/policy failures emit no changes. */
45
73
  importLatex(source: string): LatexParseResult;
74
+ /** Explicit LaTeX paste replaces only the current range and creates one Undo entry. */
75
+ pasteLatex(source: string): LatexInsertionResult;
46
76
  load(document: MathDocument): void;
47
77
  copy(): MathFragment | undefined;
48
78
  paste(fragment: MathFragment): boolean;
@@ -50,4 +80,3 @@ export declare function createMathSession(options?: MathSessionOptions): {
50
80
  destroy(): void;
51
81
  };
52
82
  export type MathSession = ReturnType<typeof createMathSession>;
53
- //# sourceMappingURL=session.d.ts.map
package/dist/session.js CHANGED
@@ -1,12 +1,17 @@
1
+ import { setMathPresentation } from './presentation.js';
2
+ import { createMathPreferences } from './preferences.js';
3
+ import { insertLatex } from './latex-insertion.js';
1
4
  import { parseLatex } from './latex.js';
2
5
  import { commit, createHistory, initialState, insertStructure, toLatex, row, undo, redo, } from './model.js';
3
6
  import { copyRange, pasteFragment, wrapRange, wrappingKinds, } from './range.js';
4
7
  import { insertTemplate } from './templates.js';
5
8
  import { splitLine } from './lines.js';
9
+ import { activeMatrix } from './matrix.js';
10
+ import { clearMatrixRange, copyMatrixRange, focusMatrixCell, matrixFragment, pasteMatrixFragment, resolveMatrixRange, transposeMatrix, } from './matrix-range.js';
6
11
  /** Framework-free session. Snapshots are copies; consumers cannot mutate history. */
7
12
  export function createMathSession(options = {}) {
8
13
  let history = createHistory(initialState(options.document ? structuredClone(options.document) : undefined));
9
- let mode = options.mode ?? 'block', locale = options.locale ?? 'ko', range;
14
+ let mode = options.mode ?? 'block', locale = options.locale ?? 'ko', range, matrixRange;
10
15
  const listeners = new Set();
11
16
  const valid = (document) => mode !== 'inline' || !document.additionalLines?.length;
12
17
  if (!valid(history.present.document))
@@ -14,6 +19,7 @@ export function createMathSession(options = {}) {
14
19
  const getSnapshot = () => ({
15
20
  state: structuredClone(history.present),
16
21
  range: range && structuredClone(range),
22
+ matrixRange: matrixRange && structuredClone(matrixRange),
17
23
  locale,
18
24
  mode,
19
25
  canUndo: !!history.past.length,
@@ -32,12 +38,16 @@ export function createMathSession(options = {}) {
32
38
  history = changed
33
39
  ? commit(history, next)
34
40
  : { ...history, present: { ...history.present, caret: next.caret } };
35
- if (changed)
41
+ if (changed) {
36
42
  range = undefined;
43
+ matrixRange = undefined;
44
+ }
37
45
  emit(changed);
38
46
  return true;
39
47
  };
48
+ const preferences = options.preferences ?? createMathPreferences();
40
49
  return {
50
+ preferences,
41
51
  getSnapshot,
42
52
  subscribe(listener) {
43
53
  listeners.add(listener);
@@ -52,12 +62,34 @@ export function createMathSession(options = {}) {
52
62
  present: { ...history.present, caret: { ...caret } },
53
63
  };
54
64
  range = undefined;
65
+ matrixRange = undefined;
55
66
  emit(false);
56
67
  },
57
68
  selectRange(next) {
69
+ matrixRange = undefined;
58
70
  range = next && structuredClone(next);
59
71
  emit(false);
60
72
  },
73
+ selectMatrixRange(next) {
74
+ if (next && !resolveMatrixRange(history.present.document, next))
75
+ return false;
76
+ matrixRange = next && structuredClone(next);
77
+ range = undefined;
78
+ if (next)
79
+ history = {
80
+ ...history,
81
+ present: focusMatrixCell(history.present, next.matrixId, next.focus),
82
+ };
83
+ emit(false);
84
+ return true;
85
+ },
86
+ copyMatrix() {
87
+ return matrixRange ? copyMatrixRange(history.present.document, matrixRange) : undefined;
88
+ },
89
+ pasteMatrix(fragment) {
90
+ const next = pasteMatrixFragment(history.present, fragment, matrixRange);
91
+ return next !== history.present && apply(next);
92
+ },
61
93
  configure(next) {
62
94
  const targetMode = next.mode ?? mode;
63
95
  if (targetMode === 'inline' && history.present.document.additionalLines?.length)
@@ -73,17 +105,44 @@ export function createMathSession(options = {}) {
73
105
  apply(initialState(result.document));
74
106
  return result;
75
107
  },
108
+ /** Explicit LaTeX paste replaces only the current range and creates one Undo entry. */
109
+ pasteLatex(source) {
110
+ if (matrixRange)
111
+ return {
112
+ ok: false,
113
+ source,
114
+ diagnostics: [
115
+ {
116
+ code: 'unsupported-structure',
117
+ message: 'Select one insertion point before pasting LaTeX.',
118
+ start: 0,
119
+ end: source.length,
120
+ },
121
+ ],
122
+ };
123
+ const result = insertLatex(history.present, source, range, { multiline: mode !== 'inline' });
124
+ if (result.ok)
125
+ apply(result.state);
126
+ return result;
127
+ },
76
128
  load(document) {
77
129
  if (!valid(document))
78
130
  throw new Error('Inline mode requires a single top-level row.');
79
131
  history = createHistory(initialState(structuredClone(document)));
80
132
  range = undefined;
133
+ matrixRange = undefined;
81
134
  emit(true);
82
135
  },
83
136
  copy() {
137
+ if (matrixRange) {
138
+ const fragment = copyMatrixRange(history.present.document, matrixRange);
139
+ return fragment && matrixFragment(fragment);
140
+ }
84
141
  return range ? copyRange(history.present.document, range) : undefined;
85
142
  },
86
143
  paste(fragment) {
144
+ if (matrixRange)
145
+ return false;
87
146
  return apply(pasteFragment(history.present, fragment, range));
88
147
  },
89
148
  execute(command) {
@@ -96,19 +155,62 @@ export function createMathSession(options = {}) {
96
155
  const changed = next.present.document !== state.document;
97
156
  history = next;
98
157
  range = undefined;
158
+ matrixRange = undefined;
99
159
  emit(changed);
100
160
  return changed;
101
161
  }
102
- if (command.type === 'text')
103
- return apply(pasteFragment(state, {
162
+ if (command.type === 'presentation') {
163
+ if (range || matrixRange)
164
+ return false;
165
+ const next = setMathPresentation(state, command.id, command.change);
166
+ return next !== state && apply(next);
167
+ }
168
+ if (command.type === 'matrix-select') {
169
+ const context = activeMatrix(state);
170
+ if (!context)
171
+ return false;
172
+ matrixRange = {
173
+ matrixId: context.matrix.id,
174
+ anchor: command.all ? 0 : context.cell,
175
+ focus: command.all ? context.matrix.slots.length - 1 : context.cell,
176
+ };
177
+ range = undefined;
178
+ history = {
179
+ ...history,
180
+ present: focusMatrixCell(state, matrixRange.matrixId, matrixRange.focus),
181
+ };
182
+ emit(false);
183
+ return true;
184
+ }
185
+ if (command.type === 'matrix-transpose') {
186
+ const next = transposeMatrix(state, matrixRange?.matrixId);
187
+ return next !== state && apply(next);
188
+ }
189
+ if (command.type === 'matrix-clear')
190
+ return !!matrixRange && apply(clearMatrixRange(state, matrixRange));
191
+ if (matrixRange)
192
+ return (command.type === 'text' &&
193
+ !command.value.includes('\n') &&
194
+ !command.value.includes('\r') &&
195
+ apply(clearMatrixRange(state, matrixRange, command.value)));
196
+ if (command.type === 'text') {
197
+ const inserted = apply(pasteFragment(state, {
104
198
  version: 1,
105
199
  rows: command.value
106
200
  .replace(/\r\n?/g, '\n')
107
201
  .split('\n')
108
202
  .map((value) => row(value)),
109
203
  }, range));
110
- if (command.type === 'template')
111
- return apply(insertTemplate(state, command.id, range));
204
+ if (inserted)
205
+ preferences.record(`symbol-${command.value}`);
206
+ return inserted;
207
+ }
208
+ if (command.type === 'template') {
209
+ const inserted = apply(insertTemplate(state, command.id, range));
210
+ if (inserted)
211
+ preferences.record(`template-${command.id}`);
212
+ return inserted;
213
+ }
112
214
  if (command.type === 'newline')
113
215
  return mode === 'inline' ? false : apply(splitLine(state));
114
216
  if (range)
@@ -121,4 +223,3 @@ export function createMathSession(options = {}) {
121
223
  },
122
224
  };
123
225
  }
124
- //# sourceMappingURL=session.js.map
package/dist/solid.d.ts CHANGED
@@ -9,4 +9,3 @@ declare module 'solid-js' {
9
9
  }
10
10
  /** Solid directive: <div use:mathEditor={options()} />. */
11
11
  export declare function mathEditor(host: HTMLElement, options: Accessor<DOMMathEditorOptions>): void;
12
- //# sourceMappingURL=solid.d.ts.map
package/dist/solid.js CHANGED
@@ -6,4 +6,3 @@ export function mathEditor(host, options) {
6
6
  createEffect(() => editor.update(options()));
7
7
  onCleanup(() => editor.destroy());
8
8
  }
9
- //# sourceMappingURL=solid.js.map
@@ -1,13 +1,21 @@
1
1
  import { type MathLocale, type MathMessageParameters } from './i18n.js';
2
2
  import { type MathState, type StructureKind } from './model.js';
3
3
  export interface MathSuggestion {
4
+ /** Convert this enclosing structure without consuming the typed query. */
5
+ transformRootId?: string;
6
+ /** Change the current fence without inserting a wrapper or consuming text. */
7
+ transformFenceId?: string;
4
8
  /** Preserve the query as an operand instead of replacing a command name. */
5
9
  wrapOperand?: boolean;
6
10
  operatorName?: string;
7
11
  mathStyle?: 'display' | 'text';
8
12
  templateId?: string;
9
13
  identity?: boolean;
14
+ /** Fill each matrix cell with an editable zero. */
15
+ zero?: boolean;
16
+ /** Row count; matrices are square unless matrixColumns is supplied. */
10
17
  matrixSize?: number;
18
+ matrixColumns?: number;
11
19
  id: string;
12
20
  /** Catalog entries hold message keys; findSuggestions returns localized display text. */
13
21
  label: string;
@@ -27,5 +35,9 @@ export declare function findSuggestions(text: string, caret: number, locale?: Ma
27
35
  query: string;
28
36
  candidates: MathSuggestion[];
29
37
  };
38
+ /** Combine text search with changes available for the current formula structure. */
39
+ export declare function findStateSuggestions(state: MathState, locale?: MathLocale, caret?: number): {
40
+ query: string;
41
+ candidates: MathSuggestion[];
42
+ };
30
43
  export declare function acceptSuggestion(state: MathState, query: string, candidate: MathSuggestion): MathState;
31
- //# sourceMappingURL=suggestions.d.ts.map