@barocss/math-editor 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/ADAPTERS.md +31 -2
  2. package/API-JAVASCRIPT.md +157 -0
  3. package/API-REACT.md +202 -0
  4. package/API-SESSION.md +163 -0
  5. package/API-SOLID.md +90 -0
  6. package/API-SVELTE.md +83 -0
  7. package/API-VUE.md +104 -0
  8. package/API-WEB-COMPONENT.md +128 -0
  9. package/CHANGELOG.md +21 -0
  10. package/EMBEDDING.md +7 -0
  11. package/IMPLEMENTATION.md +132 -1
  12. package/JSON-MODEL.md +441 -0
  13. package/LATEX-GUIDE.md +254 -0
  14. package/LATEX-MODEL.md +290 -0
  15. package/LATEX-SCOPE.md +260 -4
  16. package/LOCALIZATION.md +7 -1
  17. package/README.md +97 -4
  18. package/RELEASING.md +1 -1
  19. package/ROADMAP.md +111 -16
  20. package/SUPPORT.md +134 -13
  21. package/SYMBOLS.md +33 -1
  22. package/VALIDATION.md +175 -0
  23. package/dist/core.d.ts +2 -0
  24. package/dist/core.d.ts.map +1 -1
  25. package/dist/core.js +2 -0
  26. package/dist/core.js.map +1 -1
  27. package/dist/document-codec.d.ts +4 -0
  28. package/dist/document-codec.d.ts.map +1 -0
  29. package/dist/document-codec.js +37 -0
  30. package/dist/document-codec.js.map +1 -0
  31. package/dist/dom/menu-position.d.ts +3 -0
  32. package/dist/dom/menu-position.d.ts.map +1 -0
  33. package/dist/dom/menu-position.js +44 -0
  34. package/dist/dom/menu-position.js.map +1 -0
  35. package/dist/dom/toolbar.d.ts +2 -0
  36. package/dist/dom/toolbar.d.ts.map +1 -1
  37. package/dist/dom/toolbar.js +16 -1
  38. package/dist/dom/toolbar.js.map +1 -1
  39. package/dist/dom.d.ts +1 -0
  40. package/dist/dom.d.ts.map +1 -1
  41. package/dist/dom.js +153 -24
  42. package/dist/dom.js.map +1 -1
  43. package/dist/editor-labels.d.ts.map +1 -1
  44. package/dist/editor-labels.js +42 -0
  45. package/dist/editor-labels.js.map +1 -1
  46. package/dist/fences.d.ts +11 -0
  47. package/dist/fences.d.ts.map +1 -0
  48. package/dist/fences.js +21 -0
  49. package/dist/fences.js.map +1 -0
  50. package/dist/index.d.ts +3 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/latex.d.ts +26 -0
  55. package/dist/latex.d.ts.map +1 -0
  56. package/dist/latex.js +689 -0
  57. package/dist/latex.js.map +1 -0
  58. package/dist/locales/en.js +109 -2
  59. package/dist/locales/en.json +109 -2
  60. package/dist/locales/ko.js +121 -3
  61. package/dist/locales/ko.json +121 -3
  62. package/dist/math-editor-toolbar.d.ts +5 -2
  63. package/dist/math-editor-toolbar.d.ts.map +1 -1
  64. package/dist/math-editor-toolbar.js +10 -6
  65. package/dist/math-editor-toolbar.js.map +1 -1
  66. package/dist/math-editor.d.ts +24 -4
  67. package/dist/math-editor.d.ts.map +1 -1
  68. package/dist/math-editor.js +236 -40
  69. package/dist/math-editor.js.map +1 -1
  70. package/dist/model.d.ts +10 -2
  71. package/dist/model.d.ts.map +1 -1
  72. package/dist/model.js +262 -9
  73. package/dist/model.js.map +1 -1
  74. package/dist/range.d.ts +3 -1
  75. package/dist/range.d.ts.map +1 -1
  76. package/dist/range.js +162 -6
  77. package/dist/range.js.map +1 -1
  78. package/dist/react.d.ts +1 -1
  79. package/dist/react.d.ts.map +1 -1
  80. package/dist/session.d.ts +3 -0
  81. package/dist/session.d.ts.map +1 -1
  82. package/dist/session.js +9 -1
  83. package/dist/session.js.map +1 -1
  84. package/dist/suggestions.d.ts +4 -0
  85. package/dist/suggestions.d.ts.map +1 -1
  86. package/dist/suggestions.js +161 -22
  87. package/dist/suggestions.js.map +1 -1
  88. package/dist/symbols.d.ts +1 -1
  89. package/dist/symbols.d.ts.map +1 -1
  90. package/dist/symbols.js +56 -5
  91. package/dist/symbols.js.map +1 -1
  92. package/package.json +3 -2
  93. package/src/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  94. package/src/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  95. package/src/fonts/KaTeX_Main-Bold.woff2 +0 -0
  96. package/src/fonts/KaTeX_Main-Regular.woff2 +0 -0
  97. package/src/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  98. package/src/fonts/LICENSE-KaTeX.txt +21 -0
  99. package/src/style.css +868 -45
package/dist/latex.js ADDED
@@ -0,0 +1,689 @@
1
+ import { fenceCharacters, fencePairs } from './fences.js';
2
+ import { row, namedFunctions, textNode, documentRows, } from './model.js';
3
+ import { mathSymbols } from './symbols.js';
4
+ import { parseMathDocument } from './document-codec.js';
5
+ const commands = new Map(mathSymbols
6
+ .filter((item) => /^\\[a-zA-Z]+$/.test(item[4]))
7
+ .map((item) => [item[4].slice(1), item[0]]));
8
+ for (const [alias, canonical] of Object.entries({
9
+ neq: 'ne',
10
+ leq: 'le',
11
+ geq: 'ge',
12
+ rightarrow: 'to',
13
+ gets: 'leftarrow',
14
+ land: 'wedge',
15
+ lor: 'vee',
16
+ lnot: 'neg',
17
+ }))
18
+ commands.set(alias, commands.get(canonical));
19
+ const matrices = ['matrix', 'pmatrix', 'bmatrix', 'Bmatrix', 'vmatrix', 'Vmatrix'];
20
+ /** A bounded parser for the editor's presentation grammar, not a TeX macro interpreter. */
21
+ class LatexParser {
22
+ source;
23
+ at = 0;
24
+ end;
25
+ depth = 0;
26
+ scopeStop = () => false;
27
+ constructor(source) {
28
+ this.source = source;
29
+ this.end = source.length;
30
+ }
31
+ fail(code, message, start = this.at) {
32
+ throw {
33
+ code,
34
+ message,
35
+ start,
36
+ end: Math.min(this.source.length, Math.max(start + 1, this.at)),
37
+ };
38
+ }
39
+ starts(value) {
40
+ return this.source.startsWith(value, this.at);
41
+ }
42
+ isCommand(name) {
43
+ return (this.starts(`\\${name}`) && !/[a-zA-Z]/.test(this.source[this.at + name.length + 1] ?? ''));
44
+ }
45
+ skip() {
46
+ while (this.at < this.end) {
47
+ if (/\s/.test(this.source[this.at]))
48
+ this.at++;
49
+ else if (this.source[this.at] === '%') {
50
+ while (this.at < this.end && this.source[this.at] !== '\n')
51
+ this.at++;
52
+ }
53
+ else
54
+ break;
55
+ }
56
+ }
57
+ command() {
58
+ const start = this.at++;
59
+ const word = this.source.slice(this.at, this.end).match(/^[a-zA-Z]+/)?.[0];
60
+ if (word) {
61
+ this.at += word.length;
62
+ return word;
63
+ }
64
+ if (this.at >= this.end)
65
+ this.fail('syntax', 'Incomplete command.', start);
66
+ return this.source[this.at++];
67
+ }
68
+ structure(type, slots) {
69
+ return { type, id: row().id, slots };
70
+ }
71
+ append(target, atom) {
72
+ for (const node of atom.children) {
73
+ const previous = target.children.at(-1);
74
+ if (node.type === 'text' && previous?.type === 'text')
75
+ previous.text += node.text;
76
+ else
77
+ target.children.push(node);
78
+ }
79
+ }
80
+ wrap(node) {
81
+ const result = row();
82
+ result.children.push(node, textNode());
83
+ return result;
84
+ }
85
+ expression(stop) {
86
+ const result = row();
87
+ const previousStop = this.scopeStop;
88
+ this.scopeStop = stop;
89
+ try {
90
+ while (true) {
91
+ this.skip();
92
+ if (this.at >= this.end || stop())
93
+ return result;
94
+ const atom = this.scriptedAtom();
95
+ this.append(result, atom);
96
+ }
97
+ }
98
+ finally {
99
+ this.scopeStop = previousStop;
100
+ }
101
+ }
102
+ /** Recognize a plain parenthesized base before a script, without treating an
103
+ * unmatched literal parenthesis elsewhere as a failed import. */
104
+ scriptedParenthesisEnd() {
105
+ if (!this.starts('('))
106
+ return;
107
+ let parentheses = 0, braces = 0;
108
+ for (let i = this.at; i < this.end; i++) {
109
+ const char = this.source[i];
110
+ if (char === '\\') {
111
+ i++;
112
+ continue;
113
+ }
114
+ if (char === '%') {
115
+ while (i < this.end && this.source[i] !== '\n')
116
+ i++;
117
+ continue;
118
+ }
119
+ if (char === '{') {
120
+ braces++;
121
+ continue;
122
+ }
123
+ if (char === '}') {
124
+ if (--braces < 0)
125
+ return;
126
+ continue;
127
+ }
128
+ if (braces)
129
+ continue;
130
+ if (char === '(') {
131
+ if (++parentheses > 40)
132
+ this.fail('limit', 'Parenthesis nesting limit exceeded.');
133
+ }
134
+ else if (char === ')' && --parentheses === 0)
135
+ return /^\s*[_^]/.test(this.source.slice(i + 1, this.end)) ? i : undefined;
136
+ }
137
+ }
138
+ scriptedAtom() {
139
+ const closing = this.scriptedParenthesisEnd();
140
+ let atom;
141
+ if (closing !== undefined) {
142
+ if (++this.depth > 40)
143
+ this.fail('limit', 'Nesting exceeds 40 levels.');
144
+ try {
145
+ this.at++;
146
+ const content = this.expression(() => this.at === closing);
147
+ this.at++;
148
+ atom = this.wrap(this.structure('parentheses', [content]));
149
+ }
150
+ finally {
151
+ this.depth--;
152
+ }
153
+ }
154
+ else
155
+ atom = this.atom();
156
+ this.skip();
157
+ if (this.starts('^{\\circ}')) {
158
+ this.at += 8;
159
+ this.append(atom, row('°'));
160
+ return atom;
161
+ }
162
+ if (this.starts('^') || this.starts('_')) {
163
+ const marker = this.source[this.at++];
164
+ const script = this.argument();
165
+ this.skip();
166
+ if (this.starts('^') || this.starts('_')) {
167
+ const second = this.source[this.at++];
168
+ if (second === marker)
169
+ this.fail('syntax', 'Repeated script needs explicit grouping.');
170
+ const other = this.argument();
171
+ atom = this.wrap(this.structure('scripts', marker === '_' ? [atom, script, other] : [atom, other, script]));
172
+ this.skip();
173
+ if (this.starts('^') || this.starts('_'))
174
+ this.fail('syntax', 'Repeated script needs explicit grouping.');
175
+ }
176
+ else {
177
+ atom = this.wrap(this.structure(marker === '^' ? 'superscript' : 'subscript', [atom, script]));
178
+ }
179
+ }
180
+ return atom;
181
+ }
182
+ literalArgument() {
183
+ this.skip();
184
+ if (!this.starts('{'))
185
+ this.fail('syntax', 'Expected a braced text argument.');
186
+ this.at++;
187
+ let value = '', depth = 1;
188
+ while (this.at < this.end) {
189
+ const char = this.source[this.at];
190
+ if (char === '\\') {
191
+ const start = this.at;
192
+ const command = this.command();
193
+ if (['{', '}', '_', '%', '$', '#', '&', ' '].includes(command))
194
+ value += command;
195
+ else if (['textbackslash', 'textasciicircum', 'textasciitilde'].includes(command)) {
196
+ if (!this.starts('{}'))
197
+ this.fail('syntax', 'Expected an empty argument.', start);
198
+ this.at += 2;
199
+ value += { textbackslash: '\\', textasciicircum: '^', textasciitilde: '~' }[command];
200
+ }
201
+ else
202
+ this.fail('unsupported-command', 'Unsupported command inside literal text.', start);
203
+ }
204
+ else if (char === '{') {
205
+ this.at++;
206
+ if (++depth > 40)
207
+ this.fail('limit', 'Text nesting limit exceeded.');
208
+ }
209
+ else if (char === '}') {
210
+ this.at++;
211
+ if (--depth === 0)
212
+ return row(value);
213
+ }
214
+ else if (char === '%') {
215
+ while (this.at < this.end && this.source[this.at] !== '\n')
216
+ this.at++;
217
+ }
218
+ else if (/[$&#_^~]/.test(char))
219
+ this.fail('unsupported-structure', 'Escape special characters inside literal text.');
220
+ else {
221
+ value += char;
222
+ this.at++;
223
+ }
224
+ }
225
+ this.fail('syntax', 'Missing closing text brace.');
226
+ }
227
+ argument() {
228
+ this.skip();
229
+ if (this.at >= this.end)
230
+ this.fail('syntax', 'Expected an argument.');
231
+ return this.atom();
232
+ }
233
+ group() {
234
+ this.at++;
235
+ const result = this.expression(() => this.starts('}'));
236
+ if (!this.starts('}'))
237
+ this.fail('syntax', 'Missing closing brace.');
238
+ this.at++;
239
+ return result;
240
+ }
241
+ environmentName() {
242
+ this.skip();
243
+ const match = this.source.slice(this.at, this.end).match(/^\{([a-zA-Z*]+)\}/);
244
+ if (!match)
245
+ this.fail('syntax', 'Expected an environment name.');
246
+ this.at += match[0].length;
247
+ return match[1];
248
+ }
249
+ delimiter() {
250
+ this.skip();
251
+ if (this.at >= this.end)
252
+ this.fail('syntax', 'Missing delimiter.');
253
+ if (!this.starts('\\'))
254
+ return this.source[this.at++];
255
+ const start = this.at;
256
+ const name = this.command();
257
+ const delimiters = {
258
+ '{': '{',
259
+ '}': '}',
260
+ lbrace: '{',
261
+ rbrace: '}',
262
+ langle: '⟨',
263
+ rangle: '⟩',
264
+ lparen: '(',
265
+ rparen: ')',
266
+ lbrack: '[',
267
+ rbrack: ']',
268
+ lvert: '|',
269
+ rvert: '|',
270
+ vert: '|',
271
+ lVert: '‖',
272
+ rVert: '‖',
273
+ Vert: '‖',
274
+ '|': '‖',
275
+ };
276
+ if (!Object.hasOwn(delimiters, name))
277
+ this.fail('unsupported-structure', 'Unsupported delimiter.', start);
278
+ return delimiters[name];
279
+ }
280
+ grid(name) {
281
+ const rows = [[]];
282
+ while (true) {
283
+ rows
284
+ .at(-1)
285
+ .push(this.expression(() => this.starts('&') || this.starts('\\\\') || this.isCommand('end')));
286
+ if (this.starts('&')) {
287
+ this.at++;
288
+ continue;
289
+ }
290
+ if (this.starts('\\\\')) {
291
+ this.at += 2;
292
+ rows.push([]);
293
+ if (rows.length > 200)
294
+ this.fail('limit', 'Too many rows.');
295
+ continue;
296
+ }
297
+ if (!this.isCommand('end'))
298
+ this.fail('syntax', `Missing \\end{${name}}.`);
299
+ this.command();
300
+ if (this.environmentName() !== name)
301
+ this.fail('syntax', 'Mismatched environment.');
302
+ break;
303
+ }
304
+ // A cases row may contain just an expression (for example a system of equations).
305
+ // Keep the model's two-column shape by adding an editable empty condition.
306
+ if (name === 'cases') {
307
+ if (rows.some((cells) => cells.length > 2))
308
+ this.fail('unsupported-structure', 'Cases supports at most two cells per row.');
309
+ for (const cells of rows)
310
+ if (cells.length === 1)
311
+ cells.push(row());
312
+ }
313
+ const columns = rows[0].length;
314
+ if (rows.some((cells) => cells.length !== columns))
315
+ this.fail('unsupported-structure', 'All rows must have the same number of cells.');
316
+ if (name === 'gathered') {
317
+ if (columns !== 1)
318
+ this.fail('unsupported-structure', 'Gathered rows cannot contain alignment cells.');
319
+ return rows.map((cells) => cells[0]);
320
+ }
321
+ if (columns > 20 || rows.length > 20)
322
+ this.fail('limit', 'Grids support up to 20 rows and columns.');
323
+ if ((name === 'aligned' || name === 'cases') && columns !== 2)
324
+ this.fail('unsupported-structure', 'Aligned and cases require two cells per row.');
325
+ const node = matrices.includes(name)
326
+ ? {
327
+ type: 'matrix',
328
+ id: row().id,
329
+ environment: name,
330
+ columns,
331
+ slots: rows.flat(),
332
+ }
333
+ : { type: name, id: row().id, columns, slots: rows.flat() };
334
+ return [this.wrap(node)];
335
+ }
336
+ atom() {
337
+ if (++this.depth > 40)
338
+ this.fail('limit', 'Nesting exceeds 40 levels.');
339
+ try {
340
+ return this.readAtom();
341
+ }
342
+ finally {
343
+ this.depth--;
344
+ }
345
+ }
346
+ readAtom() {
347
+ this.skip();
348
+ const start = this.at;
349
+ if (this.at >= this.end)
350
+ this.fail('syntax', 'Expected an expression.');
351
+ // The degree catalog entry has an empty base; recognize its exact exported spelling.
352
+ if (this.starts('{}^{\\circ}')) {
353
+ this.at += 10;
354
+ return row('°');
355
+ }
356
+ if (this.starts('{'))
357
+ return this.group();
358
+ const char = this.source[this.at];
359
+ if (/[}\^_&$#~]/.test(char))
360
+ this.fail('syntax', `Unexpected ${char}.`);
361
+ if (char !== '\\') {
362
+ const value = String.fromCodePoint(this.source.codePointAt(this.at));
363
+ this.at += value.length;
364
+ return row(value);
365
+ }
366
+ const command = this.command();
367
+ if (namedFunctions.includes(command))
368
+ return this.wrap(this.structure('operatorName', [row(command)]));
369
+ if (command === 'text' || command === 'operatorname')
370
+ return this.wrap(this.structure(command === 'text' ? 'textGroup' : 'operatorName', [this.literalArgument()]));
371
+ // The declaration lasts to this group's/cell's boundary; export a scoped command.
372
+ if (command === 'rm')
373
+ return this.wrap(this.structure('roman', [this.expression(this.scopeStop)]));
374
+ if (command === 'mathbf' || command === 'mathcal')
375
+ return this.wrap(this.structure(command === 'mathbf' ? 'bold' : 'calligraphic', [this.argument()]));
376
+ if (command === 'mathrm')
377
+ return this.wrap(this.structure('roman', [this.argument()]));
378
+ const spacingCommands = {
379
+ ',': 'thinSpace',
380
+ ':': 'mediumSpace',
381
+ ';': 'thickSpace',
382
+ '!': 'negativeThinSpace',
383
+ };
384
+ if (Object.hasOwn(spacingCommands, command))
385
+ return this.wrap(this.structure(spacingCommands[command], []));
386
+ if (command === 'quad' || command === 'qquad')
387
+ return this.wrap(this.structure(command, []));
388
+ if (commands.has(command))
389
+ return row(commands.get(command));
390
+ if (['{', '}', '_', '%', '$', '#', '&', ' '].includes(command))
391
+ return row(command);
392
+ if (command === 'backslash')
393
+ return row('\\');
394
+ if (command === 'textasciicircum' || command === 'textasciitilde') {
395
+ this.skip();
396
+ if (!this.starts('{}'))
397
+ this.fail('syntax', 'Expected an empty argument.', start);
398
+ this.at += 2;
399
+ return row(command === 'textasciicircum' ? '^' : '~');
400
+ }
401
+ if (command === 'mathbb') {
402
+ this.skip();
403
+ const match = this.source
404
+ .slice(this.at, this.end)
405
+ .match(/^(?:\{\s*([NZQRC])\s*\}|([NZQRC]))/);
406
+ if (!match)
407
+ return this.wrap(this.structure('blackboard', [this.argument()]));
408
+ this.at += match[0].length;
409
+ return row({ N: 'ℕ', Z: 'ℤ', Q: 'ℚ', R: 'ℝ', C: 'ℂ' }[(match[1] ?? match[2])]);
410
+ }
411
+ if (['vec', 'hat', 'overline', 'tilde', 'bar', 'dot', 'ddot', 'widehat', 'widetilde'].includes(command))
412
+ return this.wrap(this.structure(command, [this.argument()]));
413
+ if (['binom', 'dbinom', 'tbinom', 'dfrac', 'tfrac'].includes(command)) {
414
+ const node = this.structure(command.endsWith('binom') ? 'binomial' : 'fraction', [
415
+ this.argument(),
416
+ this.argument(),
417
+ ]);
418
+ if (command !== 'binom')
419
+ Object.assign(node, { mathStyle: command.startsWith('d') ? 'display' : 'text' });
420
+ return this.wrap(node);
421
+ }
422
+ if (command === 'overbrace' || command === 'underbrace') {
423
+ const body = this.argument();
424
+ this.skip();
425
+ const marker = command === 'overbrace' ? '^' : '_';
426
+ let annotation = row();
427
+ if (this.starts(marker)) {
428
+ this.at++;
429
+ annotation = this.argument();
430
+ this.skip();
431
+ if (this.starts(marker))
432
+ this.fail('syntax', 'Repeated brace annotation.');
433
+ }
434
+ return this.wrap(this.structure(command, [annotation, body]));
435
+ }
436
+ if (command === 'xrightarrow' || command === 'xleftarrow') {
437
+ this.skip();
438
+ let lower = row();
439
+ if (this.starts('[')) {
440
+ this.at++;
441
+ lower = this.expression(() => this.starts(']'));
442
+ if (!this.starts(']'))
443
+ this.fail('syntax', 'Missing closing arrow label bracket.', start);
444
+ this.at++;
445
+ }
446
+ return this.wrap(this.structure(command, [this.argument(), lower]));
447
+ }
448
+ if (command === 'overset' || command === 'underset')
449
+ return this.wrap(this.structure(command, [this.argument(), this.argument()]));
450
+ if (command === 'lVert') {
451
+ const body = this.expression(() => this.isCommand('rVert'));
452
+ if (!this.isCommand('rVert'))
453
+ this.fail('syntax', 'Missing rVert.', start);
454
+ this.command();
455
+ return this.wrap(this.structure('norm', [body]));
456
+ }
457
+ if (command === 'langle') {
458
+ const body = this.expression(() => this.isCommand('rangle'));
459
+ if (!this.isCommand('rangle'))
460
+ this.fail('syntax', 'Missing rangle.', start);
461
+ this.command();
462
+ return this.wrap(this.structure('angle', [body]));
463
+ }
464
+ if (command === 'frac')
465
+ return this.wrap(this.structure('fraction', [this.argument(), this.argument()]));
466
+ if (command === 'sqrt') {
467
+ this.skip();
468
+ if (this.starts('[')) {
469
+ this.at++;
470
+ const index = this.expression(() => this.starts(']'));
471
+ if (!this.starts(']'))
472
+ this.fail('syntax', 'Missing closing root index bracket.', start);
473
+ this.at++;
474
+ return this.wrap(this.structure('indexedRoot', [index, this.argument()]));
475
+ }
476
+ return this.wrap(this.structure('root', [this.argument()]));
477
+ }
478
+ if (command === 'left') {
479
+ this.skip();
480
+ const open = this.delimiter();
481
+ if (!fenceCharacters.includes(open))
482
+ this.fail('unsupported-structure', 'Unsupported delimiter.', start);
483
+ const content = this.expression(() => this.isCommand('right'));
484
+ if (!this.isCommand('right'))
485
+ this.fail('syntax', 'Missing right delimiter.', start);
486
+ this.command();
487
+ const close = this.delimiter();
488
+ if (!fenceCharacters.includes(close))
489
+ this.fail('unsupported-structure', 'Unsupported delimiter.', start);
490
+ const kind = Object.keys(fencePairs).find((key) => fencePairs[key][0] === open && fencePairs[key][1] === close);
491
+ const node = this.structure((kind ?? 'fenced'), [content]);
492
+ if (!kind)
493
+ Object.assign(node, { left: open, right: close });
494
+ return this.wrap(node);
495
+ }
496
+ let explicitLimits;
497
+ if (['lim', 'limsup', 'liminf', 'sum', 'prod', 'int', 'iint', 'iiint', 'oint'].includes(command)) {
498
+ this.skip();
499
+ if (this.isCommand('limits') || this.isCommand('nolimits')) {
500
+ explicitLimits = this.command() === 'limits';
501
+ this.skip();
502
+ }
503
+ }
504
+ if (['lim', 'limsup', 'liminf'].includes(command)) {
505
+ this.skip();
506
+ let condition = row();
507
+ if (this.starts('_')) {
508
+ this.at++;
509
+ condition = this.argument();
510
+ this.skip();
511
+ }
512
+ if (this.starts('_') || this.starts('^'))
513
+ this.fail('unsupported-structure', 'A limit accepts one lower condition.', start);
514
+ // Preserve the same explicit-body convention as the other large operators.
515
+ const body = this.at >= this.end ||
516
+ this.starts('}') ||
517
+ this.starts('&') ||
518
+ this.starts('\\\\') ||
519
+ this.isCommand('end') ||
520
+ this.isCommand('right')
521
+ ? row()
522
+ : this.scriptedAtom();
523
+ const limit = this.structure(command === 'lim' ? 'limit' : command, [
524
+ condition,
525
+ body,
526
+ ]);
527
+ if (explicitLimits !== undefined)
528
+ Object.assign(limit, { limits: explicitLimits });
529
+ return this.wrap(limit);
530
+ }
531
+ if (['sum', 'prod', 'int', 'iint', 'iiint', 'oint'].includes(command)) {
532
+ let lower = row(), upper = row();
533
+ const seen = new Set();
534
+ this.skip();
535
+ while (this.starts('_') || this.starts('^')) {
536
+ const marker = this.source[this.at++];
537
+ if (seen.has(marker))
538
+ this.fail('syntax', 'Repeated operator limit.', start);
539
+ seen.add(marker);
540
+ const value = this.argument();
541
+ if (marker === '_')
542
+ lower = value;
543
+ else
544
+ upper = value;
545
+ this.skip();
546
+ }
547
+ // An exported operator always has an explicit body. For ordinary input, consume one atom.
548
+ const body = this.at >= this.end ||
549
+ this.starts('}') ||
550
+ this.starts('&') ||
551
+ this.isCommand('end') ||
552
+ this.isCommand('right')
553
+ ? row()
554
+ : this.scriptedAtom();
555
+ const operator = this.structure({
556
+ sum: 'sum',
557
+ prod: 'product',
558
+ int: 'integral',
559
+ iint: 'doubleIntegral',
560
+ iiint: 'tripleIntegral',
561
+ oint: 'contourIntegral',
562
+ }[command], [lower, upper, body]);
563
+ if (explicitLimits !== undefined)
564
+ Object.assign(operator, { limits: explicitLimits });
565
+ return this.wrap(operator);
566
+ }
567
+ if (command === 'begin') {
568
+ const name = this.environmentName();
569
+ if (![...matrices, 'aligned', 'cases'].includes(name))
570
+ this.fail('unsupported-environment', `Unsupported nested environment: ${name}.`, start);
571
+ return this.grid(name)[0];
572
+ }
573
+ this.fail('unsupported-command', `Unsupported command: \\${command}.`, start);
574
+ }
575
+ parse() {
576
+ this.skip();
577
+ const delimiter = ['$$', '$', '\\(', '\\['].find((value) => this.starts(value));
578
+ if (delimiter) {
579
+ const closing = { $$: '$$', $: '$', '\\(': '\\)', '\\[': '\\]' }[delimiter];
580
+ this.at += delimiter.length;
581
+ while (/\s/.test(this.source[this.end - 1] ?? '') && this.end > this.at)
582
+ this.end--;
583
+ if (!this.source.slice(0, this.end).endsWith(closing))
584
+ this.fail('syntax', 'Missing math delimiter.');
585
+ const closingAt = this.end - closing.length;
586
+ let inComment = false;
587
+ for (let offset = this.at; offset < closingAt; offset++) {
588
+ const char = this.source[offset];
589
+ if (char === '\n' || char === '\r')
590
+ inComment = false;
591
+ else if (!inComment && char === '\\')
592
+ offset++;
593
+ else if (!inComment && char === '%')
594
+ inComment = true;
595
+ }
596
+ if (inComment)
597
+ this.fail('syntax', 'The closing math delimiter is inside a comment.', closingAt);
598
+ this.end = closingAt;
599
+ }
600
+ this.skip();
601
+ let lines;
602
+ const beginning = this.at;
603
+ let environment;
604
+ if (this.isCommand('begin')) {
605
+ this.command();
606
+ environment = this.environmentName();
607
+ }
608
+ if (environment === 'equation')
609
+ this.fail('unsupported-environment', 'Numbered equations are not supported; use equation* for formula-only import.', beginning);
610
+ if (environment === 'equation*') {
611
+ lines = [this.expression(() => this.isCommand('end'))];
612
+ if (!this.isCommand('end'))
613
+ this.fail('syntax', 'Missing equation* end.');
614
+ this.command();
615
+ if (this.environmentName() !== 'equation*')
616
+ this.fail('syntax', 'Mismatched equation* end.');
617
+ }
618
+ else if (environment === 'gathered')
619
+ lines = this.grid('gathered');
620
+ else {
621
+ this.at = beginning;
622
+ lines = [this.expression(() => false)];
623
+ }
624
+ this.skip();
625
+ if (this.at !== this.end)
626
+ this.fail('unsupported-structure', 'Unexpected content after the expression.');
627
+ return {
628
+ version: 1,
629
+ root: lines[0],
630
+ ...(lines.length > 1 ? { additionalLines: lines.slice(1) } : {}),
631
+ };
632
+ }
633
+ }
634
+ /** Parse without changing editor state. Failures retain the complete original source. */
635
+ export function parseLatex(source, options = {}) {
636
+ if (source.length > 10000)
637
+ return {
638
+ ok: false,
639
+ source,
640
+ diagnostics: [
641
+ {
642
+ code: 'limit',
643
+ message: 'Input exceeds 10,000 characters.',
644
+ start: 10000,
645
+ end: source.length,
646
+ },
647
+ ],
648
+ };
649
+ try {
650
+ const document = new LatexParser(source).parse();
651
+ if (!parseMathDocument(JSON.stringify(document)))
652
+ return {
653
+ ok: false,
654
+ source,
655
+ diagnostics: [
656
+ {
657
+ code: 'limit',
658
+ message: 'The resulting document exceeds the persistence limits.',
659
+ start: 0,
660
+ end: source.length,
661
+ },
662
+ ],
663
+ };
664
+ const excluded = new Set(options.excludedStructures ?? []);
665
+ const allowed = (value) => value.children.every((node) => node.type === 'text' || (!excluded.has(node.type) && node.slots.every(allowed)));
666
+ if ((options.multiline === false && document.additionalLines?.length) ||
667
+ !documentRows(document).every(allowed)) {
668
+ return {
669
+ ok: false,
670
+ source,
671
+ diagnostics: [
672
+ {
673
+ code: 'unsupported-structure',
674
+ message: 'This formula is not allowed by the host editing policy.',
675
+ start: 0,
676
+ end: source.length,
677
+ },
678
+ ],
679
+ };
680
+ }
681
+ return { ok: true, document, source, diagnostics: [] };
682
+ }
683
+ catch (error) {
684
+ if (error && typeof error === 'object' && 'code' in error && 'start' in error)
685
+ return { ok: false, source, diagnostics: [error] };
686
+ throw error;
687
+ }
688
+ }
689
+ //# sourceMappingURL=latex.js.map