@barocss/math-editor 0.2.1 → 0.4.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.
- package/ADAPTERS.md +25 -14
- package/API-JAVASCRIPT.md +1 -1
- package/API-REACT.md +5 -1
- package/API-SESSION.md +168 -6
- package/API-SOLID.md +1 -1
- package/API-SVELTE.md +1 -1
- package/API-VUE.md +1 -1
- package/API-WEB-COMPONENT.md +1 -1
- package/CHANGELOG.md +45 -0
- package/EMBEDDING.md +4 -0
- package/IMPLEMENTATION.md +27 -2
- package/JSON-MODEL.md +1 -0
- package/LATEX-GUIDE.md +8 -1
- package/LATEX-SCOPE.md +2 -2
- package/LICENSE +1 -3
- package/README.md +145 -37
- package/ROADMAP.md +93 -11
- package/STYLING.md +157 -0
- package/SUPPORT.md +24 -12
- package/VALIDATION.md +113 -4
- package/dist/core.d.ts +5 -1
- package/dist/core.js +5 -1
- package/dist/document-codec.d.ts +0 -1
- package/dist/document-codec.js +0 -1
- package/dist/dom/caret-geometry.d.ts +3 -0
- package/dist/dom/caret-geometry.js +41 -0
- package/dist/dom/elements.d.ts +1 -2
- package/dist/dom/elements.js +2 -3
- package/dist/dom/latex-panel.d.ts +10 -0
- package/dist/dom/latex-panel.js +59 -0
- package/dist/dom/matrix-selection.d.ts +11 -0
- package/dist/dom/matrix-selection.js +30 -0
- package/dist/dom/menu-position.d.ts +14 -2
- package/dist/dom/menu-position.js +84 -15
- package/dist/dom/presentation-controls.d.ts +4 -0
- package/dist/dom/presentation-controls.js +56 -0
- package/dist/dom/quick-panel.d.ts +12 -0
- package/dist/dom/quick-panel.js +102 -0
- package/dist/dom/selection.d.ts +29 -0
- package/dist/dom/selection.js +53 -0
- package/dist/dom/symbol-panel.d.ts +9 -0
- package/dist/dom/symbol-panel.js +146 -0
- package/dist/dom/theme.d.ts +6 -0
- package/dist/dom/theme.js +68 -0
- package/dist/dom/toolbar-catalog.d.ts +10 -0
- package/dist/dom/toolbar-catalog.js +35 -0
- package/dist/dom/toolbar.d.ts +13 -2
- package/dist/dom/toolbar.js +295 -28
- package/dist/dom.d.ts +7 -1
- package/dist/dom.js +510 -128
- package/dist/editor-labels.d.ts +0 -1
- package/dist/editor-labels.js +0 -1
- package/dist/enter-policy.d.ts +0 -1
- package/dist/enter-policy.js +0 -1
- package/dist/fences.d.ts +0 -1
- package/dist/fences.js +0 -1
- package/dist/i18n.d.ts +0 -1
- package/dist/i18n.js +0 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/latex-insertion.d.ts +12 -0
- package/dist/latex-insertion.js +26 -0
- package/dist/latex-paste-panel.d.ts +3 -0
- package/dist/latex-paste-panel.js +18 -0
- package/dist/latex.d.ts +0 -1
- package/dist/latex.js +0 -1
- package/dist/lines.d.ts +0 -1
- package/dist/lines.js +0 -1
- package/dist/locales/en.js +27 -1
- package/dist/locales/en.json +27 -1
- package/dist/locales/ko.js +27 -1
- package/dist/locales/ko.json +27 -1
- package/dist/math-editor-toolbar.d.ts +4 -2
- package/dist/math-editor-toolbar.js +3 -4
- package/dist/math-editor.d.ts +5 -2
- package/dist/math-editor.js +285 -46
- package/dist/matrix-range.d.ts +40 -0
- package/dist/matrix-range.js +242 -0
- package/dist/matrix.d.ts +0 -1
- package/dist/matrix.js +0 -1
- package/dist/model.d.ts +0 -1
- package/dist/model.js +0 -1
- package/dist/outputs.d.ts +0 -1
- package/dist/outputs.js +0 -1
- package/dist/preferences.d.ts +24 -0
- package/dist/preferences.js +66 -0
- package/dist/presentation-controls.d.ts +10 -0
- package/dist/presentation-controls.js +10 -0
- package/dist/presentation.d.ts +13 -0
- package/dist/presentation.js +86 -0
- package/dist/quick-panel.d.ts +2 -0
- package/dist/quick-panel.js +17 -0
- package/dist/range.d.ts +2 -1
- package/dist/range.js +11 -1
- package/dist/react.d.ts +0 -1
- package/dist/react.js +0 -1
- package/dist/session.d.ts +31 -2
- package/dist/session.js +108 -7
- package/dist/solid.d.ts +0 -1
- package/dist/solid.js +0 -1
- package/dist/suggestions.d.ts +4 -1
- package/dist/suggestions.js +100 -12
- package/dist/svelte.d.ts +0 -1
- package/dist/svelte.js +0 -1
- package/dist/symbol-browser.d.ts +0 -1
- package/dist/symbol-browser.js +0 -1
- package/dist/symbols.d.ts +0 -1
- package/dist/symbols.js +0 -1
- package/dist/templates.d.ts +0 -1
- package/dist/templates.js +0 -1
- package/dist/tokens.d.ts +0 -1
- package/dist/tokens.js +0 -1
- package/dist/vertical-navigation.d.ts +15 -0
- package/dist/vertical-navigation.js +145 -0
- package/dist/vue.d.ts +0 -1
- package/dist/vue.js +0 -1
- package/dist/web-component.d.ts +0 -1
- package/dist/web-component.js +0 -1
- package/package.json +1 -1
- package/src/style.css +254 -129
- package/dist/core.d.ts.map +0 -1
- package/dist/core.js.map +0 -1
- package/dist/document-codec.d.ts.map +0 -1
- package/dist/document-codec.js.map +0 -1
- package/dist/dom/elements.d.ts.map +0 -1
- package/dist/dom/elements.js.map +0 -1
- package/dist/dom/menu-position.d.ts.map +0 -1
- package/dist/dom/menu-position.js.map +0 -1
- package/dist/dom/toolbar.d.ts.map +0 -1
- package/dist/dom/toolbar.js.map +0 -1
- package/dist/dom.d.ts.map +0 -1
- package/dist/dom.js.map +0 -1
- package/dist/editor-labels.d.ts.map +0 -1
- package/dist/editor-labels.js.map +0 -1
- package/dist/enter-policy.d.ts.map +0 -1
- package/dist/enter-policy.js.map +0 -1
- package/dist/fences.d.ts.map +0 -1
- package/dist/fences.js.map +0 -1
- package/dist/i18n.d.ts.map +0 -1
- package/dist/i18n.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/latex.d.ts.map +0 -1
- package/dist/latex.js.map +0 -1
- package/dist/lines.d.ts.map +0 -1
- package/dist/lines.js.map +0 -1
- package/dist/math-editor-toolbar.d.ts.map +0 -1
- package/dist/math-editor-toolbar.js.map +0 -1
- package/dist/math-editor.d.ts.map +0 -1
- package/dist/math-editor.js.map +0 -1
- package/dist/matrix.d.ts.map +0 -1
- package/dist/matrix.js.map +0 -1
- package/dist/model.d.ts.map +0 -1
- package/dist/model.js.map +0 -1
- package/dist/outputs.d.ts.map +0 -1
- package/dist/outputs.js.map +0 -1
- package/dist/range.d.ts.map +0 -1
- package/dist/range.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/solid.d.ts.map +0 -1
- package/dist/solid.js.map +0 -1
- package/dist/suggestions.d.ts.map +0 -1
- package/dist/suggestions.js.map +0 -1
- package/dist/svelte.d.ts.map +0 -1
- package/dist/svelte.js.map +0 -1
- package/dist/symbol-browser.d.ts.map +0 -1
- package/dist/symbol-browser.js.map +0 -1
- package/dist/symbols.d.ts.map +0 -1
- package/dist/symbols.js.map +0 -1
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js.map +0 -1
- package/dist/tokens.d.ts.map +0 -1
- package/dist/tokens.js.map +0 -1
- package/dist/vue.d.ts.map +0 -1
- package/dist/vue.js.map +0 -1
- package/dist/web-component.d.ts.map +0 -1
- package/dist/web-component.js.map +0 -1
|
@@ -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
package/dist/matrix.js
CHANGED
package/dist/model.d.ts
CHANGED
|
@@ -79,4 +79,3 @@ export declare function redo(history: MathHistory): MathHistory;
|
|
|
79
79
|
/** Grids can be selected from either outside edge or their first/last direct text slot. */
|
|
80
80
|
export declare function gridDeletionTarget(state: MathState, direction: -1 | 1): MathGrid | undefined;
|
|
81
81
|
export declare function removeGrid(state: MathState, gridId: string): MathState;
|
|
82
|
-
//# sourceMappingURL=model.d.ts.map
|
package/dist/model.js
CHANGED
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
|
@@ -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
|
+
}
|
|
@@ -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,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
package/dist/react.js
CHANGED