@barocss/math-editor 0.4.0 → 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 (49) hide show
  1. package/API-SESSION.md +10 -0
  2. package/API-WEB-COMPONENT.md +2 -0
  3. package/CHANGELOG.md +61 -0
  4. package/EDITING-SCENARIOS.md +174 -0
  5. package/IMPLEMENTATION.md +95 -0
  6. package/LATEX-GUIDE.md +22 -0
  7. package/LATEX-MODEL.md +12 -0
  8. package/LATEX-SCOPE.md +10 -1
  9. package/README.md +73 -19
  10. package/RELEASING.md +81 -10
  11. package/RENDERING-TESTS.md +79 -0
  12. package/ROADMAP.md +132 -5
  13. package/VALIDATION.md +331 -0
  14. package/dist/context-tools.d.ts +21 -0
  15. package/dist/context-tools.js +37 -0
  16. package/dist/dom/context-keyboard.d.ts +2 -0
  17. package/dist/dom/context-keyboard.js +22 -0
  18. package/dist/dom/menu-position.d.ts +3 -0
  19. package/dist/dom/menu-position.js +45 -1
  20. package/dist/dom.d.ts +2 -0
  21. package/dist/dom.js +187 -38
  22. package/dist/latex.js +11 -0
  23. package/dist/locales/en.js +13 -1
  24. package/dist/locales/en.json +13 -1
  25. package/dist/locales/ko.js +13 -1
  26. package/dist/locales/ko.json +13 -1
  27. package/dist/math-editor.d.ts +3 -1
  28. package/dist/math-editor.js +394 -280
  29. package/dist/math-layout.d.ts +12 -0
  30. package/dist/math-layout.js +59 -0
  31. package/dist/math-spacing.d.ts +13 -0
  32. package/dist/math-spacing.js +87 -0
  33. package/dist/model.d.ts +4 -0
  34. package/dist/model.js +49 -4
  35. package/dist/root-transform.d.ts +19 -0
  36. package/dist/root-transform.js +69 -0
  37. package/dist/suggestions.d.ts +9 -0
  38. package/dist/suggestions.js +53 -0
  39. package/dist/web-component.js +9 -1
  40. package/package.json +11 -3
  41. package/src/fonts/KaTeX_Math-Italic.woff2 +0 -0
  42. package/src/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  43. package/src/fonts/README.md +18 -0
  44. package/src/shapes/README.md +21 -0
  45. package/src/shapes/parenthesis-bottom.svg +1 -0
  46. package/src/shapes/parenthesis-top.svg +1 -0
  47. package/src/shapes/parenthesis.svg +1 -0
  48. package/src/shapes/radical.svg +1 -0
  49. package/src/style.css +695 -44
@@ -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
+ }
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;
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':
@@ -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
+ }
@@ -1,6 +1,10 @@
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;
@@ -31,4 +35,9 @@ export declare function findSuggestions(text: string, caret: number, locale?: Ma
31
35
  query: string;
32
36
  candidates: MathSuggestion[];
33
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
+ };
34
43
  export declare function acceptSuggestion(state: MathState, query: string, candidate: MathSuggestion): MathState;
@@ -1,3 +1,6 @@
1
+ import { transformRoot } from './root-transform.js';
2
+ import { structureEditingContext, changeContextFence } from './context-tools.js';
3
+ import { fencePairs } from './fences.js';
1
4
  import { mathLocaleAliases, translate, } from './i18n.js';
2
5
  import { insertTemplate, mathTemplates } from './templates.js';
3
6
  import { activeMatrix, insertIdentityMatrix, insertMatrix, MATRIX_MAX_SIZE } from './matrix.js';
@@ -284,7 +287,57 @@ export function findSuggestions(text, caret, locale = 'ko') {
284
287
  })),
285
288
  };
286
289
  }
290
+ /** Combine text search with changes available for the current formula structure. */
291
+ export function findStateSuggestions(state, locale = 'ko', caret = state.caret.start) {
292
+ const text = textNodes(state.document).find((node) => node.id === state.caret.id)?.text ?? '';
293
+ const result = findSuggestions(text, caret, locale);
294
+ const context = structureEditingContext(state);
295
+ const root = context?.root;
296
+ const kind = root?.canTransform ? root.target : undefined;
297
+ if (root && kind) {
298
+ const item = mathStructures.find((entry) => entry.kind === kind);
299
+ result.candidates.push({
300
+ ...item,
301
+ id: `transform-${kind}`,
302
+ transformRootId: root.id,
303
+ label: translate(locale, 'suggestion.transform', {
304
+ kind: translate(locale, `context.${kind}`),
305
+ }),
306
+ detail: translate(locale, 'suggestion.transformRootDetail'),
307
+ });
308
+ }
309
+ if (context?.fence) {
310
+ const fence = context.fence;
311
+ const changes = Object.keys(fencePairs)
312
+ .filter((kind) => kind !== fence.type)
313
+ .map((kind) => {
314
+ const item = mathStructures.find((entry) => entry.kind === kind);
315
+ return {
316
+ ...item,
317
+ id: `transform-fence-${kind}`,
318
+ transformFenceId: fence.id,
319
+ label: translate(locale, 'suggestion.transformFence', {
320
+ kind: translate(locale, item.label),
321
+ }),
322
+ detail: translate(locale, 'suggestion.transformFenceDetail'),
323
+ };
324
+ });
325
+ // Explicit symbol/command matches stay first. Changes precede new wrapping actions.
326
+ const wrapping = result.candidates.findIndex((item) => item.wrapOperand);
327
+ result.candidates.splice(wrapping < 0 ? result.candidates.length : wrapping, 0, ...changes);
328
+ }
329
+ return result;
330
+ }
287
331
  export function acceptSuggestion(state, query, candidate) {
332
+ if (candidate.transformFenceId)
333
+ return candidate.kind
334
+ ? changeContextFence(state, candidate.transformFenceId, candidate.kind)
335
+ : state;
336
+ if (candidate.transformRootId) {
337
+ return candidate.kind === 'root' || candidate.kind === 'indexedRoot'
338
+ ? transformRoot(state, candidate.transformRootId, candidate.kind)
339
+ : state;
340
+ }
288
341
  const node = textNodes(state.document).find((n) => n.id === state.caret.id);
289
342
  const start = state.caret.start - query.length;
290
343
  if (!node || start < 0 || node.text.slice(start, state.caret.start) !== query)
@@ -6,7 +6,14 @@ export function defineMathEditor(tagName = 'barocss-math-editor') {
6
6
  if (existing)
7
7
  return existing;
8
8
  class EditorElement extends HTMLElement {
9
- static observedAttributes = ['locale', 'mode', 'toolbar', 'line-numbers', 'enter-behavior'];
9
+ static observedAttributes = [
10
+ 'locale',
11
+ 'mode',
12
+ 'toolbar',
13
+ 'line-numbers',
14
+ 'enter-behavior',
15
+ 'context-tools',
16
+ ];
10
17
  session = createMathSession();
11
18
  editor;
12
19
  get value() {
@@ -26,6 +33,7 @@ export function defineMathEditor(tagName = 'barocss-math-editor') {
26
33
  onCancel: () => this.dispatchEvent(new CustomEvent('math-cancel', { bubbles: true, composed: true })),
27
34
  onCommit: (snapshot) => this.dispatchEvent(new CustomEvent('math-commit', { detail: snapshot, bubbles: true, composed: true })),
28
35
  showLineNumbers: this.getAttribute('line-numbers') !== 'false',
36
+ contextTools: this.getAttribute('context-tools') !== 'false',
29
37
  onChange: (document, latex) => this.dispatchEvent(new CustomEvent('math-change', {
30
38
  detail: { document, latex },
31
39
  bubbles: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barocss/math-editor",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -46,7 +46,8 @@
46
46
  "src/style.css",
47
47
  "*.md",
48
48
  "LICENSE",
49
- "src/fonts"
49
+ "src/fonts",
50
+ "src/shapes"
50
51
  ],
51
52
  "peerDependencies": {
52
53
  "react": ">=18",
@@ -120,6 +121,13 @@
120
121
  "test": "vitest run",
121
122
  "test:run": "vitest run",
122
123
  "format": "prettier --write src test \"examples/*.ts\" scripts",
123
- "format:check": "prettier --check src test \"examples/*.ts\" scripts"
124
+ "format:check": "prettier --check src test \"examples/*.ts\" scripts",
125
+ "test:rendering": "node test/rendering/run.mjs",
126
+ "test:rendering:syntax": "node test/rendering/run.mjs --suite=syntax",
127
+ "test:rendering:combinations": "node test/rendering/run.mjs --suite=combinations",
128
+ "test:rendering:inventory": "vitest run test/rendering-coverage.test.ts",
129
+ "test:rendering:update": "node test/rendering/update.mjs",
130
+ "test:rendering:report": "node test/rendering/coverage-report.mjs",
131
+ "test:editing": "node test/editing/run.mjs"
124
132
  }
125
133
  }
@@ -0,0 +1,18 @@
1
+ # Bundled mathematical glyphs
2
+
3
+ The WOFF2 files in this directory come from KaTeX 0.16.28. Their MIT notice is
4
+ preserved in [LICENSE-KaTeX.txt](LICENSE-KaTeX.txt).
5
+
6
+ The editor uses Main Regular for upright mathematics, Math Italic for variables,
7
+ Size1 Regular for inline operators, and Size2 Regular for display operators. Bold, calligraphic and blackboard faces
8
+ support the corresponding alphabet structures. Unsupported characters retain
9
+ system-font fallbacks.
10
+
11
+ The over/underbrace masks in `../style.css` also derive from the `leftbrace`,
12
+ `midbrace` and `rightbrace` paths in KaTeX 0.16.28 `src/svgGeometry.js`, under the
13
+ same MIT license. Three clipped slices preserve the ends and center when the
14
+ brace stretches. These assets do not add a KaTeX JavaScript runtime dependency.
15
+
16
+ Root and parenthesis masks also use KaTeX contours. See
17
+ [../shapes/README.md](../shapes/README.md) for the source glyphs, bounds and
18
+ conversion details.
@@ -0,0 +1,21 @@
1
+ # Mathematical outlines
2
+
3
+ These static SVG masks are derived from KaTeX 0.16.28, under its MIT license.
4
+ The copyright and license notice is preserved in
5
+ [../fonts/LICENSE-KaTeX.txt](../fonts/LICENSE-KaTeX.txt).
6
+
7
+ | Asset | Source | Conversion |
8
+ | ------------------------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `radical.svg` | `src/svgGeometry.js`, `sqrtMain` | Set `extraVinculum` and top padding to zero; crop to the 850 × 1000 radical region. The editable radicand slot draws the horizontal rule. |
10
+ | `parenthesis.svg` | Main Regular, U+0028 | Extract the glyph outline, invert the font Y axis and crop to its ink bounds: (94, −250)–(333, 750). |
11
+ | `parenthesis-top.svg` | Size4 Regular, U+239B | Same conversion, bounds (291, −655)–(843, 1154). |
12
+ | `parenthesis-bottom.svg` | Size4 Regular, U+239D | Same conversion, bounds (291, −644)–(843, 1165). |
13
+
14
+ Font contours were extracted from the distributed TTF files with fontTools
15
+ `SVGPathPen` and `TransformPen`. The masks retain the existing editor layout
16
+ boxes; they do not reproduce KaTeX's full delimiter-size selection algorithm.
17
+ Tall parentheses use bounded end caps and a straight extender. Right parentheses
18
+ mirror the corresponding left outline.
19
+
20
+ These files are packaged with `style.css`. They do not require KaTeX JavaScript,
21
+ fontTools, network requests to a CDN, or code generation in a consuming app.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 552 1809" preserveAspectRatio="none"><path d="M5 1Q7 0 75 0Q118 0 120 1Q124 2 125.0 8.0Q126 14 126 46Q126 290 143 500Q174 908 272.0 1223.0Q370 1538 542 1782Q552 1796 552 1800Q552 1803 546 1809H529Q510 1809 509 1808Q501 1800 494 1791Q130 1349 36 629Q4 384 0 72Q0 21 0.5 12.0Q1 3 5 1Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 552 1809" preserveAspectRatio="none"><path d="M509 1Q510 0 528 0H546Q552 6 552 9Q552 13 542 27Q339 316 241.5 692.0Q144 1068 129 1574Q126 1730 126 1763Q126 1795 125.0 1801.0Q124 1807 120 1808Q118 1809 75 1809Q8 1809 6 1808Q1 1806 0.5 1797.0Q0 1788 0 1737Q3 1498 24 1283Q104 491 494 18Q501 9 509 1Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 239 1000" preserveAspectRatio="none"><path d="M239 991Q239 1000 221 1000H208Q201 994 180 976Q0 812 0 500Q0 378 29 277Q75 120 180 24Q187 18 196.0 10.5Q205 3 208 0H221Q233 0 236.0 3.0Q239 6 239 9Q239 13 228 24Q73 188 73.0 500.0Q73 812 228 976Q239 987 239 991Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1000" preserveAspectRatio="none"><path d="M95,622 c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14 c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54 c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10 s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429 c69,-144,104.5,-217.7,106.5,-221 l0.0 -0 c5.3,-9.3,12,-14,20,-14 H400000v40H845.2724 s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7 c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z M834 0h400000v40h-400000z"/></svg>