@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,12 @@
1
+ import { type MathDocument } from './model.js';
2
+ export type MathLayoutStyle = 'display' | 'text' | 'script' | 'scriptscript';
3
+ /** Resolve mathematical style from model slots, independently of DOM ancestry.
4
+ * These values control operator presentation only; they are not saved in JSON.
5
+ */
6
+ export declare function mathLayout(document: MathDocument, mode?: 'block' | 'inline'): {
7
+ rows: Map<string, MathLayoutStyle>;
8
+ operators: Map<string, {
9
+ style: MathLayoutStyle;
10
+ placement: "side" | "stacked";
11
+ }>;
12
+ };
@@ -0,0 +1,59 @@
1
+ import { documentRows, isGrid, } from './model.js';
2
+ const scriptStyle = (style) => style === 'display' || style === 'text' ? 'script' : 'scriptscript';
3
+ const large = new Set([
4
+ 'sum',
5
+ 'product',
6
+ 'integral',
7
+ 'doubleIntegral',
8
+ 'tripleIntegral',
9
+ 'contourIntegral',
10
+ ]);
11
+ const limits = new Set(['limit', 'limsup', 'liminf']);
12
+ /** Resolve mathematical style from model slots, independently of DOM ancestry.
13
+ * These values control operator presentation only; they are not saved in JSON.
14
+ */
15
+ export function mathLayout(document, mode = 'block') {
16
+ const rows = new Map();
17
+ const operators = new Map();
18
+ const visit = (row, style) => {
19
+ rows.set(row.id, style);
20
+ for (const node of row.children) {
21
+ if (node.type === 'text')
22
+ continue;
23
+ const own = !isGrid(node) && node.mathStyle ? node.mathStyle : style;
24
+ if (large.has(node.type) || limits.has(node.type)) {
25
+ const integral = node.type.toLowerCase().includes('integral');
26
+ const explicit = !isGrid(node) ? node.limits : undefined;
27
+ operators.set(node.id, {
28
+ style: own,
29
+ placement: explicit === true || (explicit !== false && own === 'display' && !integral)
30
+ ? 'stacked'
31
+ : 'side',
32
+ });
33
+ }
34
+ node.slots.forEach((slot, index) => visit(slot, slotStyle(node, index, own)));
35
+ }
36
+ };
37
+ documentRows(document).forEach((row) => visit(row, mode === 'inline' ? 'text' : 'display'));
38
+ return { rows, operators };
39
+ }
40
+ function slotStyle(node, index, style) {
41
+ if (node.type === 'fraction' || node.type === 'binomial')
42
+ return style === 'display' ? 'text' : scriptStyle(style);
43
+ if (['superscript', 'subscript', 'scripts'].includes(node.type))
44
+ return index === 0 ? style : scriptStyle(style);
45
+ if (node.type === 'indexedRoot' && index === 0)
46
+ return 'scriptscript';
47
+ if (large.has(node.type))
48
+ return index < 2 ? scriptStyle(style) : style;
49
+ if (limits.has(node.type))
50
+ return index === 0 ? scriptStyle(style) : style;
51
+ if (['overset', 'underset', 'overbrace', 'underbrace', 'xrightarrow', 'xleftarrow'].includes(node.type))
52
+ return node.type.startsWith('x') || index === 0 ? scriptStyle(style) : style;
53
+ // AMS aligned cells explicitly enter display style, even inside a fraction.
54
+ if (node.type === 'aligned')
55
+ return 'display';
56
+ if (node.type === 'matrix' || node.type === 'cases')
57
+ return 'text';
58
+ return style;
59
+ }
@@ -0,0 +1,13 @@
1
+ import type { MathRow } from './model.js';
2
+ export interface RunSpacing {
3
+ before: number;
4
+ after: number;
5
+ tightBefore?: number;
6
+ tightAfter?: number;
7
+ }
8
+ /** Text-run spacing in math units (18 mu = 1 em).
9
+ * Empty caret boundaries do not participate. Named functions and fractions
10
+ * share run spacing. Other structures still own their outer spacing.
11
+ * Literal text rows must not use these values.
12
+ */
13
+ export declare function mathRunSpacing(row: MathRow): Map<string, RunSpacing>;
@@ -0,0 +1,87 @@
1
+ import { tokenizeMathText } from './tokens.js';
2
+ const binary = new Set(Array.from('+−-±∓×·÷∗∪∩∖⊕⊗∧∨'));
3
+ const relation = new Set(Array.from('=<>≤≥⩽⩾≠≈≃≅∼≡∝∈∉∋⊂⊃⊆⊇⊈⊥∥←→↔↑↓↕⇐⇒⇔⇑⇓⇕↦'));
4
+ function atom(text) {
5
+ if (binary.has(text))
6
+ return 'binary';
7
+ if (relation.has(text) || /^[\u2190-\u21ff\u27f0-\u27ff\u2900-\u297f]$/u.test(text))
8
+ return 'relation';
9
+ if ('([{'.includes(text) && text.length === 1)
10
+ return 'open';
11
+ if (')]}'.includes(text) && text.length === 1)
12
+ return 'close';
13
+ if (text === ',' || text === ';')
14
+ return 'punctuation';
15
+ return 'ordinary';
16
+ }
17
+ /** Text-run spacing in math units (18 mu = 1 em).
18
+ * Empty caret boundaries do not participate. Named functions and fractions
19
+ * share run spacing. Other structures still own their outer spacing.
20
+ * Literal text rows must not use these values.
21
+ */
22
+ export function mathRunSpacing(row) {
23
+ const entries = row.children.flatMap((node) => node.type === 'text'
24
+ ? tokenizeMathText(node.text)
25
+ .filter((token) => token.text && token.kind !== 'space')
26
+ .map((token) => ({ key: `${node.id}:${token.start}`, atom: atom(token.text) }))
27
+ : [
28
+ {
29
+ key: ['operatorName', 'fraction'].includes(node.type) ? node.id : '',
30
+ atom: (node.type === 'operatorName' ? 'operator' : 'ordinary'),
31
+ },
32
+ ]);
33
+ // A leading/trailing binary sign is unary. The same is true after an open
34
+ // fence, relation or operator, and before a closing fence or punctuation.
35
+ for (let index = 0; index < entries.length; index++) {
36
+ const entry = entries[index];
37
+ if (entry.atom !== 'binary')
38
+ continue;
39
+ const left = entries[index - 1]?.atom;
40
+ const right = entries[index + 1]?.atom;
41
+ if (!left ||
42
+ !right ||
43
+ ['binary', 'relation', 'open', 'punctuation', 'operator'].includes(left) ||
44
+ ['relation', 'close', 'punctuation'].includes(right))
45
+ entry.atom = 'ordinary';
46
+ }
47
+ const result = new Map();
48
+ entries.forEach((entry) => {
49
+ if (entry.key)
50
+ result.set(entry.key, { before: 0, after: 0 });
51
+ });
52
+ for (let index = 1; index < entries.length; index++) {
53
+ const left = entries[index - 1];
54
+ const right = entries[index];
55
+ let gap = 0;
56
+ if (left.atom !== 'open') {
57
+ if (right.atom === 'relation' &&
58
+ ['ordinary', 'close', 'punctuation', 'operator'].includes(left.atom))
59
+ gap = 5;
60
+ else if (left.atom === 'relation' && ['ordinary', 'open', 'operator'].includes(right.atom))
61
+ gap = 5;
62
+ else if (right.atom === 'binary' && ['ordinary', 'close'].includes(left.atom))
63
+ gap = 4;
64
+ else if (left.atom === 'binary' && ['ordinary', 'open', 'operator'].includes(right.atom))
65
+ gap = 4;
66
+ else if (left.atom === 'punctuation')
67
+ gap = 3;
68
+ else if ((right.atom === 'operator' && ['ordinary', 'close', 'operator'].includes(left.atom)) ||
69
+ (left.atom === 'operator' && right.atom === 'ordinary'))
70
+ gap = 3;
71
+ }
72
+ const tight = gap === 3 &&
73
+ (left.atom === 'operator' || right.atom === 'operator') &&
74
+ left.atom !== 'punctuation';
75
+ if (right.key) {
76
+ result.get(right.key).before = gap;
77
+ if (tight)
78
+ result.get(right.key).tightBefore = gap;
79
+ }
80
+ else if (left.key) {
81
+ result.get(left.key).after = gap;
82
+ if (tight)
83
+ result.get(left.key).tightAfter = gap;
84
+ }
85
+ }
86
+ return result;
87
+ }
@@ -0,0 +1,40 @@
1
+ import { type MathDocument, type MathMatrix, type MathRow, type MathState, type MatrixEnvironment } from './model.js';
2
+ import { type MathFragment } from './range.js';
3
+ /** Transient row-major cell addresses; never persisted in the formula document. */
4
+ export interface MathMatrixRange {
5
+ matrixId: string;
6
+ anchor: number;
7
+ focus: number;
8
+ }
9
+ export interface MathMatrixFragment {
10
+ version: 1;
11
+ columns: number;
12
+ cells: MathRow[];
13
+ environment: MatrixEnvironment;
14
+ }
15
+ export declare const MATH_MATRIX_CLIPBOARD_TYPE = "application/x-barocss-math-matrix+json";
16
+ export declare function findMatrix(document: MathDocument, id: string): MathMatrix | undefined;
17
+ export declare function resolveMatrixRange(document: MathDocument, range: MathMatrixRange): {
18
+ matrix: MathMatrix;
19
+ top: number;
20
+ left: number;
21
+ rows: number;
22
+ columns: number;
23
+ indices: number[];
24
+ } | undefined;
25
+ /** A cross-cell drag selects the nearest matrix shared by both endpoints. */
26
+ export declare function matrixRangeBetween(document: MathDocument, anchorId: string, focusId: string): MathMatrixRange | undefined;
27
+ export declare function focusMatrixCell(state: MathState, matrixId: string, cell: number): MathState;
28
+ export declare function extendMatrixRange(document: MathDocument, range: MathMatrixRange, key: string): MathMatrixRange;
29
+ export declare function copyMatrixRange(document: MathDocument, range: MathMatrixRange): MathMatrixFragment | undefined;
30
+ /** Regular fragment fallback lets a cell rectangle be pasted outside a matrix too. */
31
+ export declare function matrixFragment(fragment: MathMatrixFragment): MathFragment;
32
+ export declare function parseMatrixFragment(source: string): MathMatrixFragment | undefined;
33
+ /** Spreadsheet text is cell text, never evaluated as HTML or implicit LaTeX. */
34
+ export declare function matrixFromTSV(text: string): MathMatrixFragment | undefined;
35
+ /** Clear cell contents, retaining the matrix geometry and all unselected cells. */
36
+ export declare function clearMatrixRange(state: MathState, range: MathMatrixRange, replacement?: string): MathState;
37
+ /** Paste atomically. A single cell is an anchor; larger rectangles require matching dimensions. */
38
+ export declare function pasteMatrixFragment(state: MathState, fragment: MathMatrixFragment, range?: MathMatrixRange): MathState;
39
+ /** Transpose the entire matrix; moving rows preserves every cell ID and the nested caret. */
40
+ export declare function transposeMatrix(state: MathState, matrixId?: string | undefined): MathState;
@@ -0,0 +1,242 @@
1
+ import { documentRows, row, textNode, textNodes, } from './model.js';
2
+ import { activeMatrix, MATRIX_MAX_SIZE } from './matrix.js';
3
+ import { parseFragment } from './range.js';
4
+ export const MATH_MATRIX_CLIPBOARD_TYPE = 'application/x-barocss-math-matrix+json';
5
+ export function findMatrix(document, id) {
6
+ const visit = (r) => {
7
+ for (const node of r.children) {
8
+ if (node.type === 'matrix' && node.id === id)
9
+ return node;
10
+ if (node.type !== 'text')
11
+ for (const slot of node.slots) {
12
+ const found = visit(slot);
13
+ if (found)
14
+ return found;
15
+ }
16
+ }
17
+ };
18
+ for (const line of documentRows(document)) {
19
+ const found = visit(line);
20
+ if (found)
21
+ return found;
22
+ }
23
+ }
24
+ export function resolveMatrixRange(document, range) {
25
+ const matrix = findMatrix(document, range.matrixId);
26
+ if (!matrix ||
27
+ ![range.anchor, range.focus].every((cell) => Number.isInteger(cell) && cell >= 0 && cell < matrix.slots.length))
28
+ return;
29
+ const top = Math.min(Math.floor(range.anchor / matrix.columns), Math.floor(range.focus / matrix.columns));
30
+ const bottom = Math.max(Math.floor(range.anchor / matrix.columns), Math.floor(range.focus / matrix.columns));
31
+ const left = Math.min(range.anchor % matrix.columns, range.focus % matrix.columns);
32
+ const right = Math.max(range.anchor % matrix.columns, range.focus % matrix.columns);
33
+ const indices = [];
34
+ for (let r = top; r <= bottom; r++)
35
+ for (let c = left; c <= right; c++)
36
+ indices.push(r * matrix.columns + c);
37
+ return { matrix, top, left, rows: bottom - top + 1, columns: right - left + 1, indices };
38
+ }
39
+ /** A cross-cell drag selects the nearest matrix shared by both endpoints. */
40
+ export function matrixRangeBetween(document, anchorId, focusId) {
41
+ const parents = (id) => {
42
+ const result = [];
43
+ const visit = (r) => {
44
+ for (const node of r.children) {
45
+ if (node.type === 'text') {
46
+ if (node.id === id)
47
+ return true;
48
+ }
49
+ else
50
+ for (const [cell, slot] of node.slots.entries()) {
51
+ if (visit(slot)) {
52
+ if (node.type === 'matrix')
53
+ result.push({ matrixId: node.id, cell });
54
+ return true;
55
+ }
56
+ }
57
+ }
58
+ return false;
59
+ };
60
+ documentRows(document).some(visit);
61
+ return result;
62
+ };
63
+ const focus = parents(focusId);
64
+ for (const anchor of parents(anchorId)) {
65
+ const target = focus.find((item) => item.matrixId === anchor.matrixId);
66
+ if (target)
67
+ return target.cell === anchor.cell
68
+ ? undefined
69
+ : {
70
+ matrixId: anchor.matrixId,
71
+ anchor: anchor.cell,
72
+ focus: target.cell,
73
+ };
74
+ }
75
+ }
76
+ export function focusMatrixCell(state, matrixId, cell) {
77
+ const slot = findMatrix(state.document, matrixId)?.slots[cell];
78
+ if (!slot)
79
+ return state;
80
+ const node = textNodes({ version: 1, root: slot })[0];
81
+ return { ...state, caret: { id: node.id, start: 0, end: 0 } };
82
+ }
83
+ export function extendMatrixRange(document, range, key) {
84
+ const bounds = resolveMatrixRange(document, range);
85
+ if (!bounds)
86
+ return range;
87
+ const matrix = bounds.matrix;
88
+ let r = Math.floor(range.focus / matrix.columns), c = range.focus % matrix.columns;
89
+ if (key === 'ArrowUp')
90
+ r--;
91
+ if (key === 'ArrowDown')
92
+ r++;
93
+ if (key === 'ArrowLeft')
94
+ c--;
95
+ if (key === 'ArrowRight')
96
+ c++;
97
+ r = Math.max(0, Math.min(matrix.slots.length / matrix.columns - 1, r));
98
+ c = Math.max(0, Math.min(matrix.columns - 1, c));
99
+ return { ...range, focus: r * matrix.columns + c };
100
+ }
101
+ export function copyMatrixRange(document, range) {
102
+ const bounds = resolveMatrixRange(document, range);
103
+ if (!bounds)
104
+ return;
105
+ return {
106
+ version: 1,
107
+ columns: bounds.columns,
108
+ environment: bounds.matrix.environment,
109
+ cells: bounds.indices.map((index) => structuredClone(bounds.matrix.slots[index])),
110
+ };
111
+ }
112
+ /** Regular fragment fallback lets a cell rectangle be pasted outside a matrix too. */
113
+ export function matrixFragment(fragment) {
114
+ const root = row();
115
+ root.children.push({
116
+ type: 'matrix',
117
+ id: row().id,
118
+ columns: fragment.columns,
119
+ environment: fragment.environment,
120
+ slots: structuredClone(fragment.cells),
121
+ }, textNode());
122
+ return { version: 1, rows: [root] };
123
+ }
124
+ export function parseMatrixFragment(source) {
125
+ if (source.length > 200000)
126
+ return;
127
+ try {
128
+ const value = JSON.parse(source);
129
+ if (value?.version !== 1 ||
130
+ !Array.isArray(value.cells) ||
131
+ !Number.isInteger(value.columns) ||
132
+ value.columns < 1 ||
133
+ value.columns > MATRIX_MAX_SIZE ||
134
+ !value.cells.length ||
135
+ value.cells.length > MATRIX_MAX_SIZE * value.columns ||
136
+ value.cells.length % value.columns)
137
+ return;
138
+ // Reuse the same bounded presentation-tree validation as normal math clipboard data.
139
+ if (!parseFragment(JSON.stringify(matrixFragment(value))))
140
+ return;
141
+ return {
142
+ version: 1,
143
+ columns: value.columns,
144
+ environment: value.environment,
145
+ cells: value.cells,
146
+ };
147
+ }
148
+ catch {
149
+ return;
150
+ }
151
+ }
152
+ /** Spreadsheet text is cell text, never evaluated as HTML or implicit LaTeX. */
153
+ export function matrixFromTSV(text) {
154
+ if (text.length > 200000 || !text.includes('\t'))
155
+ return;
156
+ const lines = text
157
+ .replace(/\r\n?/g, '\n')
158
+ .replace(/\n$/, '')
159
+ .split('\n')
160
+ .map((line) => line.split('\t'));
161
+ const columns = lines[0].length;
162
+ if (lines.length > MATRIX_MAX_SIZE ||
163
+ columns > MATRIX_MAX_SIZE ||
164
+ lines.some((line) => line.length !== columns))
165
+ return;
166
+ return {
167
+ version: 1,
168
+ columns,
169
+ environment: 'bmatrix',
170
+ cells: lines.flat().map((text) => row(text)),
171
+ };
172
+ }
173
+ const freshCell = (cell) => ({
174
+ id: row().id,
175
+ children: cell.children.map((node) => node.type === 'text'
176
+ ? textNode(node.text)
177
+ : { ...node, id: row().id, slots: node.slots.map(freshCell) }),
178
+ });
179
+ /** Clear cell contents, retaining the matrix geometry and all unselected cells. */
180
+ export function clearMatrixRange(state, range, replacement = '') {
181
+ const bounds = resolveMatrixRange(state.document, range);
182
+ if (!bounds)
183
+ return state;
184
+ const next = { ...state, document: structuredClone(state.document) };
185
+ const matrix = findMatrix(next.document, range.matrixId);
186
+ bounds.indices.forEach((index, at) => {
187
+ matrix.slots[index] = row(at === 0 ? replacement : '');
188
+ });
189
+ const focused = focusMatrixCell(next, matrix.id, bounds.indices[0]);
190
+ focused.caret.start = focused.caret.end = replacement.length;
191
+ return focused;
192
+ }
193
+ /** Paste atomically. A single cell is an anchor; larger rectangles require matching dimensions. */
194
+ export function pasteMatrixFragment(state, fragment, range) {
195
+ const safe = parseMatrixFragment(JSON.stringify(fragment));
196
+ const context = activeMatrix(state);
197
+ const bounds = range && resolveMatrixRange(state.document, range);
198
+ if (!safe || (range && !bounds) || (!bounds && !context))
199
+ return state;
200
+ const target = bounds ? bounds.matrix : context.matrix;
201
+ const top = bounds ? bounds.top : context.row, left = bounds ? bounds.left : context.column;
202
+ let rows = safe.cells.length / safe.columns, columns = safe.columns;
203
+ const fill = !!bounds && safe.cells.length === 1;
204
+ if (bounds && bounds.indices.length > 1) {
205
+ if (!fill && (rows !== bounds.rows || columns !== bounds.columns))
206
+ return state;
207
+ rows = bounds.rows;
208
+ columns = bounds.columns;
209
+ }
210
+ const totalRows = Math.max(target.slots.length / target.columns, top + rows);
211
+ const totalColumns = Math.max(target.columns, left + columns);
212
+ if (totalRows > MATRIX_MAX_SIZE || totalColumns > MATRIX_MAX_SIZE)
213
+ return state;
214
+ const next = { ...state, document: structuredClone(state.document) };
215
+ const matrix = findMatrix(next.document, target.id);
216
+ const slots = [];
217
+ for (let r = 0; r < totalRows; r++)
218
+ for (let c = 0; c < totalColumns; c++) {
219
+ const inside = r >= top && r < top + rows && c >= left && c < left + columns;
220
+ slots.push(inside
221
+ ? freshCell(safe.cells[fill ? 0 : (r - top) * safe.columns + c - left])
222
+ : r < target.slots.length / target.columns && c < target.columns
223
+ ? matrix.slots[r * target.columns + c]
224
+ : row());
225
+ }
226
+ matrix.columns = totalColumns;
227
+ matrix.slots = slots;
228
+ return focusMatrixCell(next, matrix.id, top * totalColumns + left);
229
+ }
230
+ /** Transpose the entire matrix; moving rows preserves every cell ID and the nested caret. */
231
+ export function transposeMatrix(state, matrixId = activeMatrix(state)?.matrix.id) {
232
+ const current = matrixId && findMatrix(state.document, matrixId);
233
+ if (!current || current.slots.length === 1)
234
+ return state;
235
+ const next = { ...state, document: structuredClone(state.document) };
236
+ const matrix = findMatrix(next.document, current.id);
237
+ const rows = matrix.slots.length / matrix.columns, columns = matrix.columns;
238
+ const slots = matrix.slots;
239
+ matrix.slots = Array.from({ length: slots.length }, (_, i) => slots[(i % rows) * columns + Math.floor(i / rows)]);
240
+ matrix.columns = rows;
241
+ return next;
242
+ }
package/dist/matrix.d.ts CHANGED
@@ -26,4 +26,3 @@ export declare function activeMatrix(state: MathState): {
26
26
  rows: number;
27
27
  } | undefined;
28
28
  export declare function insertIdentityMatrix(state: MathState, size: number): MathState;
29
- //# sourceMappingURL=matrix.d.ts.map
package/dist/matrix.js CHANGED
@@ -122,4 +122,3 @@ export function insertIdentityMatrix(state, size) {
122
122
  });
123
123
  return next;
124
124
  }
125
- //# sourceMappingURL=matrix.js.map
package/dist/model.d.ts CHANGED
@@ -64,6 +64,10 @@ export declare const namedFunctions: readonly ["sin", "cos", "tan", "cot", "sec"
64
64
  /** Wrap the selection, or the preceding simple operand; never swallow a preceding + or =. */
65
65
  export declare function insertStructure(state: MathState, kind: StructureKind, consumeOperand?: boolean, mathStyle?: 'display' | 'text'): MathState;
66
66
  export declare function moveCaret(state: MathState, direction: -1 | 1): MathState;
67
+ /** Remove the wrapper of an empty slot while retaining its other slots' content.
68
+ * Grid cells keep their shape; row/column deletion has its own explicit commands.
69
+ */
70
+ export declare function unwrapEmptySlot(state: MathState): MathState;
67
71
  /** At the right edge of a structure, unwrap it instead of silently deleting its contents. */
68
72
  export declare function unwrapPrevious(state: MathState): MathState;
69
73
  export declare function toLatex(document: MathDocument): string;
@@ -79,4 +83,3 @@ export declare function redo(history: MathHistory): MathHistory;
79
83
  /** Grids can be selected from either outside edge or their first/last direct text slot. */
80
84
  export declare function gridDeletionTarget(state: MathState, direction: -1 | 1): MathGrid | undefined;
81
85
  export declare function removeGrid(state: MathState, gridId: string): MathState;
82
- //# sourceMappingURL=model.d.ts.map
package/dist/model.js CHANGED
@@ -233,6 +233,47 @@ export function moveCaret(state, direction) {
233
233
  const offset = direction < 0 ? next.text.length : 0;
234
234
  return { ...state, caret: { id: next.id, start: offset, end: offset } };
235
235
  }
236
+ /** Remove the wrapper of an empty slot while retaining its other slots' content.
237
+ * Grid cells keep their shape; row/column deletion has its own explicit commands.
238
+ */
239
+ export function unwrapEmptySlot(state) {
240
+ if (state.caret.start !== 0 || state.caret.end !== 0)
241
+ return state;
242
+ const visit = (line) => {
243
+ for (let index = 0; index < line.children.length; index++) {
244
+ const node = line.children[index];
245
+ if (node.type === 'text')
246
+ continue;
247
+ const emptySlot = node.slots.findIndex((slot) => slot.children.length === 1 &&
248
+ slot.children[0].id === state.caret.id &&
249
+ slot.children[0].type === 'text' &&
250
+ slot.children[0].text === '');
251
+ if (emptySlot >= 0) {
252
+ if (isGrid(node))
253
+ return state;
254
+ // Preserve the remaining script or radicand instead of flattening it.
255
+ if ((node.type === 'indexedRoot' && emptySlot === 0) ||
256
+ (node.type === 'scripts' && emptySlot > 0))
257
+ return unwrapPrevious(state);
258
+ const after = line.children[index + 1];
259
+ if (after?.type !== 'text')
260
+ return state;
261
+ return unwrapPrevious({ ...state, caret: { id: after.id, start: 0, end: 0 } });
262
+ }
263
+ for (const slot of node.slots) {
264
+ const next = visit(slot);
265
+ if (next)
266
+ return next;
267
+ }
268
+ }
269
+ };
270
+ for (const line of documentRows(state.document)) {
271
+ const next = visit(line);
272
+ if (next)
273
+ return next;
274
+ }
275
+ return state;
276
+ }
236
277
  /** At the right edge of a structure, unwrap it instead of silently deleting its contents. */
237
278
  export function unwrapPrevious(state) {
238
279
  if (state.caret.start !== 0 || state.caret.end !== 0)
@@ -308,12 +349,12 @@ const escapeText = (text) => Array.from(text)
308
349
  '{': '\\{',
309
350
  '}': '\\}',
310
351
  _: '\\_',
311
- '^': '\\textasciicircum{}',
352
+ '^': '\\char"005E{}',
312
353
  '%': '\\%',
313
354
  $: '\\$',
314
355
  '#': '\\#',
315
356
  '&': '\\&',
316
- '~': '\\textasciitilde{}',
357
+ '~': '\\char"007E{}',
317
358
  ' ': '\\ ',
318
359
  }[c] ??
319
360
  c)
@@ -364,8 +405,12 @@ export function toLatex(document) {
364
405
  return `\\${n.mathStyle === 'display' ? 'dbinom' : n.mathStyle === 'text' ? 'tbinom' : 'binom'}{${a}}{${b}}`;
365
406
  case 'fraction':
366
407
  return `\\${n.mathStyle === 'display' ? 'dfrac' : n.mathStyle === 'text' ? 'tfrac' : 'frac'}{${a}}{${b}}`;
367
- case 'indexedRoot':
368
- return `\\sqrt[${a}]{${b}}`;
408
+ case 'indexedRoot': {
409
+ // Group complex optional arguments: nested brackets and leading
410
+ // script groups otherwise terminate KaTeX's root-index scan early.
411
+ const index = /[{}\[\]]/.test(a) ? `{${a}}` : a;
412
+ return `\\sqrt[${index}]{${b}}`;
413
+ }
369
414
  case 'vec':
370
415
  case 'hat':
371
416
  case 'tilde':
@@ -512,4 +557,3 @@ export function removeGrid(state, gridId) {
512
557
  at.row.children.splice(at.index, 2);
513
558
  return { document, caret: { id: before.id, start: offset, end: offset } };
514
559
  }
515
- //# sourceMappingURL=model.js.map
package/dist/outputs.d.ts CHANGED
@@ -10,4 +10,3 @@ export interface MathPreviewOptions {
10
10
  export declare function mountMathLatex(host: HTMLElement, session: MathSession): MathOutput;
11
11
  /** Optional renderer adapter. The core package has no KaTeX, SVG or image-export dependency. */
12
12
  export declare function mountMathPreview(host: HTMLElement, session: MathSession, options: MathPreviewOptions): MathOutput;
13
- //# sourceMappingURL=outputs.d.ts.map
package/dist/outputs.js CHANGED
@@ -58,4 +58,3 @@ export function mountMathPreview(host, session, options) {
58
58
  },
59
59
  };
60
60
  }
61
- //# sourceMappingURL=outputs.js.map
@@ -0,0 +1,24 @@
1
+ import type { MathMessageParameters } from './i18n.js';
2
+ export interface MathQuickItem {
3
+ id: string;
4
+ label: string;
5
+ labelParameters?: MathMessageParameters;
6
+ glyph: string;
7
+ template?: string;
8
+ aliases?: readonly string[];
9
+ }
10
+ export declare const mathQuickItems: readonly MathQuickItem[];
11
+ export interface MathPreferencesSnapshot {
12
+ version: 1;
13
+ recent: string[];
14
+ favorites: string[];
15
+ }
16
+ /** UI preferences are separate from formula JSON and Undo. The host owns persistence. */
17
+ export declare function createMathPreferences(initial?: unknown): {
18
+ getSnapshot: () => MathPreferencesSnapshot;
19
+ record(id: string): void;
20
+ toggleFavorite(id: string): void;
21
+ clearRecent(): void;
22
+ subscribe(listener: () => void): () => void;
23
+ };
24
+ export type MathPreferences = ReturnType<typeof createMathPreferences>;
@@ -0,0 +1,66 @@
1
+ import { mathSymbols } from './symbols.js';
2
+ import { mathTemplates } from './templates.js';
3
+ export const mathQuickItems = [
4
+ ...mathSymbols.map(([glyph, label, aliases, triggers, latex]) => ({
5
+ id: `symbol-${glyph}`,
6
+ glyph,
7
+ label,
8
+ aliases: [aliases, triggers, latex],
9
+ })),
10
+ ...mathTemplates.map((item) => ({
11
+ id: `template-${item.id}`,
12
+ label: item.label,
13
+ labelParameters: item.labelParameters,
14
+ aliases: item.aliases,
15
+ glyph: item.size ? '▦' : 'ƒ',
16
+ template: item.id,
17
+ })),
18
+ ];
19
+ /** UI preferences are separate from formula JSON and Undo. The host owns persistence. */
20
+ export function createMathPreferences(initial) {
21
+ const ids = new Set(mathQuickItems.map((item) => item.id));
22
+ const clean = (value, limit) => Array.isArray(value)
23
+ ? [
24
+ ...new Set(value.filter((id) => typeof id === 'string' && ids.has(id))),
25
+ ].slice(0, limit)
26
+ : [];
27
+ const saved = initial && typeof initial === 'object' ? initial : {};
28
+ let recent = saved.version === 1 ? clean(saved.recent, 12) : [];
29
+ let favorites = saved.version === 1 ? clean(saved.favorites, 100) : [];
30
+ const listeners = new Set();
31
+ const emit = () => {
32
+ for (const listener of listeners)
33
+ listener();
34
+ };
35
+ return {
36
+ getSnapshot: () => ({
37
+ version: 1,
38
+ recent: [...recent],
39
+ favorites: [...favorites],
40
+ }),
41
+ record(id) {
42
+ if (!ids.has(id))
43
+ return;
44
+ recent = [id, ...recent.filter((value) => value !== id)].slice(0, 12);
45
+ emit();
46
+ },
47
+ toggleFavorite(id) {
48
+ if (!ids.has(id))
49
+ return;
50
+ favorites = favorites.includes(id)
51
+ ? favorites.filter((value) => value !== id)
52
+ : [...favorites, id].slice(0, 100);
53
+ emit();
54
+ },
55
+ clearRecent() {
56
+ recent = [];
57
+ emit();
58
+ },
59
+ subscribe(listener) {
60
+ listeners.add(listener);
61
+ return () => {
62
+ listeners.delete(listener);
63
+ };
64
+ },
65
+ };
66
+ }