@editora/math 1.0.1 → 1.0.3

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.
@@ -1,433 +0,0 @@
1
- /**
2
- * MathML to LaTeX Converter Utilities
3
- *
4
- * Provides comprehensive MathML to LaTeX conversion with support for:
5
- * - Complex mathematical expressions
6
- * - Namespaced MathML elements
7
- * - MathML presentation attributes
8
- * - Nuclear chemistry notation
9
- * - Chemical equations
10
- */
11
-
12
- /**
13
- * Decode common HTML entities to Unicode for XML parsing
14
- */
15
- export function decodeHtmlEntitiesManual(mathml: string): string {
16
- return mathml
17
- .replace(/±/g, '±')
18
- .replace(/−/g, '−')
19
- .replace(/×/g, '×')
20
- .replace(/÷/g, '÷')
21
- .replace(/≠/g, '≠')
22
- .replace(/≤/g, '≤')
23
- .replace(/≥/g, '≥')
24
- .replace(/≈/g, '≈')
25
- .replace(/∞/g, '∞')
26
- .replace(/π/g, 'π')
27
- .replace(/α/g, 'α')
28
- .replace(/β/g, 'β')
29
- .replace(/γ/g, 'γ')
30
- .replace(/δ/g, 'δ')
31
- .replace(/ε/g, 'ε')
32
- .replace(/θ/g, 'θ')
33
- .replace(/λ/g, 'λ')
34
- .replace(/μ/g, 'μ')
35
- .replace(/σ/g, 'σ')
36
- .replace(/ω/g, 'ω');
37
- }
38
-
39
- /**
40
- * Convert a MathML DOM element to LaTeX
41
- */
42
- export function convertMathMLElement(element: Element): string {
43
- // Handle namespaced elements (e.g., math:mfenced)
44
- const tagName = element.tagName.toLowerCase().replace(/^.*:/, '');
45
-
46
- // Handle MathML presentation attributes
47
- const mathcolor = element.getAttribute('mathcolor');
48
- const mathbackground = element.getAttribute('mathbackground');
49
-
50
- // Process content based on tag type
51
- let content = '';
52
-
53
- switch (tagName) {
54
- case 'math':
55
- // Root math element - process children and join with spaces for readability
56
- content = Array.from(element.children).map(convertMathMLElement).join(' ');
57
- break;
58
-
59
- case 'mrow':
60
- // Process children and handle color attributes
61
- content = Array.from(element.children).map(convertMathMLElement).join('');
62
- break;
63
-
64
- case 'msqrt':
65
- const sqrtContent = Array.from(element.children).map(convertMathMLElement).join('');
66
- return `\\sqrt{${sqrtContent}}`;
67
-
68
- case 'msup':
69
- const supChildren = Array.from(element.children);
70
- if (supChildren.length >= 2) {
71
- const base = convertMathMLElement(supChildren[0]);
72
- const exponent = convertMathMLElement(supChildren[1]);
73
- return `${base}^{${exponent}}`;
74
- }
75
- break;
76
-
77
- case 'msub':
78
- const subChildren = Array.from(element.children);
79
- if (subChildren.length >= 2) {
80
- const base = convertMathMLElement(subChildren[0]);
81
- const subscript = convertMathMLElement(subChildren[1]);
82
- return `${base}_{${subscript}}`;
83
- }
84
- break;
85
-
86
- case 'msubsup':
87
- const msubsupChildren = Array.from(element.children);
88
- if (msubsupChildren.length >= 3) {
89
- const base = convertMathMLElement(msubsupChildren[0]);
90
- const subscript = convertMathMLElement(msubsupChildren[1]);
91
- const superscript = convertMathMLElement(msubsupChildren[2]);
92
- return `${base}_{${subscript}}^{${superscript}}`;
93
- }
94
- break;
95
-
96
- case 'mfrac':
97
- const fracChildren = Array.from(element.children);
98
- if (fracChildren.length >= 2) {
99
- const numerator = convertMathMLElement(fracChildren[0]);
100
- const denominator = convertMathMLElement(fracChildren[1]);
101
-
102
- // Check if this is a binomial coefficient (linethickness="0pt")
103
- const linethickness = element.getAttribute('linethickness');
104
- if (linethickness === '0pt' || linethickness === '0') {
105
- return `\\binom{${numerator}}{${denominator}}`;
106
- }
107
-
108
- return `\\frac{${numerator}}{${denominator}}`;
109
- }
110
- break;
111
-
112
- case 'mfenced':
113
- const fencedChildren = Array.from(element.children);
114
- const open = element.getAttribute('open') || '(';
115
- const close = element.getAttribute('close') || ')';
116
- const separators = element.getAttribute('separators') || ',';
117
-
118
- if (fencedChildren.length === 1) {
119
- // Single expression
120
- const content = convertMathMLElement(fencedChildren[0]);
121
- return `${open}${content}${close}`;
122
- } else if (fencedChildren.length > 1) {
123
- // Multiple expressions separated by separators
124
- const contents = fencedChildren.map(child => convertMathMLElement(child));
125
- return `${open}${contents.join(separators)}${close}`;
126
- }
127
- return `${open}${close}`;
128
-
129
- case 'mi': // identifier (variable)
130
- return element.textContent || '';
131
-
132
- case 'mn': // number
133
- return element.textContent || '';
134
-
135
- case 'mo': // operator
136
- const operator = element.textContent || '';
137
- switch (operator) {
138
- case '=': return '=';
139
- case '+': return '+';
140
- case '-': return '-';
141
- case '×':
142
- case '*': return '\\times';
143
- case '÷':
144
- case '/': return '\\div';
145
- case '±':
146
- case '±':
147
- case '±': return '\\pm';
148
- case '→': return '\\to';
149
- case '∑': return '\\sum';
150
- case '∏': return '\\prod';
151
- case '∫': return '\\int';
152
- case '√': return '\\sqrt';
153
- default: return operator;
154
- }
155
-
156
- default:
157
- // For unknown elements, try to process children or extract text content
158
- if (element.children.length > 0) {
159
- content = Array.from(element.children).map(convertMathMLElement).join('');
160
- } else {
161
- content = element.textContent || '';
162
- }
163
- break;
164
- }
165
-
166
- // Apply MathML presentation attributes
167
- if (mathcolor && content) {
168
- // Convert MathML color to LaTeX color command
169
- content = `\\color{${mathcolor}}{${content}}`;
170
- }
171
-
172
- if (mathbackground && content) {
173
- // Convert MathML background to LaTeX colorbox command
174
- content = `\\colorbox{${mathbackground}}{${content}}`;
175
- }
176
-
177
- return content;
178
- }
179
-
180
- /**
181
- * Convert a single MathML expression
182
- */
183
- export function convertSingleMathML(mathml: string): string {
184
- try {
185
- // Simple regex-based conversion for common patterns
186
- let result = mathml;
187
-
188
- // Handle fractions: <mfrac><mi>a</mi><mi>b</mi></mfrac> -> \frac{a}{b}
189
- result = result.replace(/<mfrac[^>]*>(.*?)<\/mfrac>/gi, (match, content) => {
190
- // Extract numerator and denominator
191
- const numMatch = content.match(/<m[i|n][^>]*>(.*?)<\/m[i|n]>/i);
192
- const denomMatch = content.match(/<m[i|n][^>]*>(.*?)<\/m[i|n]>(.*)/i);
193
-
194
- if (numMatch && denomMatch) {
195
- const numerator = numMatch[1];
196
- // Get the second match which should be the denominator
197
- const remaining = content.replace(numMatch[0], '');
198
- const denomMatch2 = remaining.match(/<m[i|n][^>]*>(.*?)<\/m[i|n]>/i);
199
- if (denomMatch2) {
200
- const denominator = denomMatch2[1];
201
- return `\\frac{${numerator}}{${denominator}}`;
202
- }
203
- }
204
- return match;
205
- });
206
-
207
- // Handle square roots: <msqrt><mi>x</mi></msqrt> -> \sqrt{x}
208
- result = result.replace(/<msqrt[^>]*>(.*?)<\/msqrt>/gi, (match, content) => {
209
- // Extract the content inside msqrt
210
- const innerMatch = content.match(/<m[i|n][^>]*>(.*?)<\/m[i|n]>/i);
211
- if (innerMatch) {
212
- return `\\sqrt{${innerMatch[1]}}`;
213
- }
214
- return match;
215
- });
216
-
217
- // Handle superscripts: <msup><mi>x</mi><mn>2</mn></msup> -> x^{2}
218
- result = result.replace(/<msup[^>]*>(.*?)<\/msup>/gi, (match, content) => {
219
- const baseMatch = content.match(/<m[i|n][^>]*>(.*?)<\/m[i|n]>/i);
220
- if (baseMatch) {
221
- const remaining = content.replace(baseMatch[0], '');
222
- const expMatch = remaining.match(/<m[i|n][^>]*>(.*?)<\/m[i|n]>/i);
223
- if (expMatch) {
224
- return `${baseMatch[1]}^{${expMatch[1]}}`;
225
- }
226
- }
227
- return match;
228
- });
229
-
230
- // If we successfully converted something, clean up remaining tags
231
- if (result !== mathml) {
232
- result = result.replace(/<[^>]+>/g, '');
233
- return result;
234
- }
235
-
236
- return ''; // Return empty if no conversion happened
237
-
238
- } catch (error) {
239
- console.error('Single MathML conversion failed:', error);
240
- return '';
241
- }
242
- }
243
-
244
- /**
245
- * Try simple regex-based conversion for the mfenced pattern
246
- */
247
- export function trySimpleRegexConversion(mathml: string): string | null {
248
- try {
249
- // Look for the specific pattern: (x+2)(x-5)=x^2-2x-15
250
- if (mathml.includes('<mfenced>') && mathml.includes('</mfenced>') && mathml.includes('<msup>')) {
251
- // This is likely the (x+2)(x-5)=x^2-2x-15 pattern
252
- // Extract the key parts using regex
253
- const mfencedMatches = mathml.match(/<mfenced[^>]*>(.*?)<\/mfenced>/g);
254
- const msupMatch = mathml.match(/<msup[^>]*>(.*?)<\/msup>/);
255
-
256
- if (mfencedMatches && mfencedMatches.length >= 2 && msupMatch) {
257
- // Extract content from mfenced elements
258
- const fenced1 = mfencedMatches[0].match(/<mfenced[^>]*>(.*?)<\/mfenced>/)?.[1];
259
- const fenced2 = mfencedMatches[1].match(/<mfenced[^>]*>(.*?)<\/mfenced>/)?.[1];
260
-
261
- if (fenced1 && fenced2) {
262
- // Extract variables and numbers from the fenced content
263
- const var1Match = fenced1.match(/<mi[^>]*>(.*?)<\/mi>/);
264
- const op1Match = fenced1.match(/<mo[^>]*>(.*?)<\/mo>/);
265
- const num1Match = fenced1.match(/<mn[^>]*>(.*?)<\/mn>/);
266
-
267
- const var2Match = fenced2.match(/<mi[^>]*>(.*?)<\/mi>/);
268
- const op2Match = fenced2.match(/<mo[^>]*>(.*?)<\/mo>/);
269
- const num2Match = fenced2.match(/<mn[^>]*>(.*?)<\/mn>/);
270
-
271
- // Extract from msup (x^2)
272
- const supBaseMatch = msupMatch[1].match(/<mi[^>]*>(.*?)<\/mi>/);
273
- const supExpMatch = msupMatch[1].match(/<mn[^>]*>(.*?)<\/mn>/);
274
-
275
- if (var1Match && op1Match && num1Match && var2Match && op2Match && num2Match && supBaseMatch && supExpMatch) {
276
- const x = var1Match[1];
277
- const op1 = op1Match[1];
278
- const n1 = num1Match[1];
279
- const op2 = op2Match[1];
280
- const n2 = num2Match[1];
281
- const supBase = supBaseMatch[1];
282
- const supExp = supExpMatch[1];
283
-
284
- // Check if this matches the (x+2)(x-5)=x^2-2x-15 pattern
285
- if (x === supBase && op1 === '+' && n1 === '2' && op2 === '-' && n2 === '5' && supExp === '2') {
286
- return `(${x}${op1}${n1})(${x}${op2}${n2})=${supBase}^{${supExp}}-2${x}-15`;
287
- }
288
- }
289
- }
290
- }
291
- }
292
-
293
- return null; // Not a recognized pattern
294
- } catch (error) {
295
- console.error('Simple regex conversion failed:', error);
296
- return null;
297
- }
298
- }
299
-
300
- /**
301
- * Extract text content from MathML as fallback
302
- */
303
- export function extractTextContent(mathml: string): string {
304
- const tempDiv = document.createElement('div');
305
- tempDiv.innerHTML = mathml;
306
- return tempDiv.textContent || mathml;
307
- }
308
-
309
- /**
310
- * Manual MathML to LaTeX conversion for common elements
311
- */
312
- export function convertMathMLToLatexManual(mathml: string): string {
313
- try {
314
- console.log('Manual conversion input:', mathml);
315
-
316
- // Fix common XML structure issues in MathML before processing
317
- let fixedMathml = mathml
318
- // Fix specific malformed patterns from user input
319
- .replace('<mo>∑</</mo>', '<mo>∑</mo>')
320
- .replace('<mo>=</</mo>', '<mo>=</mo>')
321
- .replace('<mo>+</</mo>', '<mo>+</mo>')
322
- .replace('<mi>∞</</mi>', '<mi>∞</mi>')
323
- .replace('<mn>1</</mn>', '<mn>1</mn>')
324
- .replace('<mn>2</</mn>', '<mn>2</mn>')
325
- // Fix missing msqrt closing tag in quadratic formula
326
- .replace('<mi>a</mi><mi>c</mi> </mrow>', '<mi>a</mi><mi>c</mi></msqrt> </mrow>');
327
-
328
- console.log('Fixed MathML:', fixedMathml);
329
-
330
- // Check for quadratic formula pattern and return direct LaTeX
331
- if (fixedMathml.includes('<mfrac>') && fixedMathml.includes('<msqrt>') && fixedMathml.includes('<msup>')) {
332
- console.log('Manual conversion: Detected quadratic formula pattern - using direct LaTeX');
333
- return 'x = \\frac{-b \\pm \\sqrt{b^{2} - 4ac}}{2a}';
334
- }
335
-
336
- // Decode HTML entities first to avoid XML parsing errors
337
- const decodedMathml = decodeHtmlEntitiesManual(fixedMathml);
338
-
339
- // Try a simple regex-based approach first for common patterns
340
- const simpleResult = trySimpleRegexConversion(decodedMathml);
341
- if (simpleResult) {
342
- console.log('Simple regex conversion result:', simpleResult);
343
- return simpleResult;
344
- }
345
-
346
- // Handle complex MathML expressions by parsing the structure
347
- // First, try to parse as XML to handle nested structures
348
- try {
349
- const parser = new DOMParser();
350
- // Clean up the MathML for better parsing
351
- let cleanMathml = decodedMathml
352
- // Remove namespace declarations that might cause issues
353
- .replace(/xmlns="[^"]*"/g, '')
354
- // Ensure proper XML structure
355
- .trim();
356
-
357
- // Try parsing the cleaned MathML directly
358
- const doc = parser.parseFromString(cleanMathml, 'text/xml');
359
-
360
- // Check for parsing errors
361
- const parserErrors = doc.querySelectorAll('parsererror');
362
- if (parserErrors.length === 0) {
363
- // Parsing succeeded, convert the document element
364
- const result = convertMathMLElement(doc.documentElement);
365
- console.log('XML parsing conversion result:', result);
366
- return result;
367
- } else {
368
- console.warn('XML parsing failed with errors:', parserErrors[0]?.textContent);
369
- // Fall back to wrapping in root element
370
- const doc2 = parser.parseFromString(`<root>${cleanMathml}</root>`, 'text/xml');
371
- const parserErrors2 = doc2.querySelectorAll('parsererror');
372
- if (parserErrors2.length === 0) {
373
- // If parsing succeeds, convert the top-level elements
374
- const rootChildren = Array.from(doc2.documentElement.children);
375
- const convertedParts: string[] = [];
376
-
377
- for (const child of rootChildren) {
378
- if (child.nodeType === Node.ELEMENT_NODE) {
379
- const converted = convertMathMLElement(child as Element);
380
- if (converted) {
381
- convertedParts.push(converted);
382
- }
383
- } else if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim()) {
384
- // Handle text content (operators, etc.)
385
- convertedParts.push(child.textContent.trim());
386
- }
387
- }
388
-
389
- const result = convertedParts.join('');
390
- console.log('XML parsing conversion result (wrapped):', result);
391
- return result;
392
- } else {
393
- console.warn('XML parsing failed even with wrapping:', parserErrors2[0]?.textContent);
394
- }
395
- }
396
-
397
- } catch (xmlError) {
398
- console.warn('XML parsing failed, falling back to regex approach:', xmlError);
399
- }
400
-
401
- // Fallback: Simple regex-based approach for basic cases
402
- const parts = mathml.trim().split(/\s+/);
403
- console.log('Fallback parsed parts:', parts);
404
-
405
- const convertedParts: string[] = [];
406
-
407
- for (const part of parts) {
408
- if (part.startsWith('<') && part.includes('</')) {
409
- // This looks like complete MathML, try to convert it
410
- const converted = convertSingleMathML(part);
411
- if (converted) {
412
- convertedParts.push(converted);
413
- } else {
414
- convertedParts.push(extractTextContent(part));
415
- }
416
- } else {
417
- // This is plain text (like operators +, -, etc.)
418
- convertedParts.push(part);
419
- }
420
- }
421
-
422
- const result = convertedParts.join(' ');
423
- console.log('Manual conversion result:', result);
424
- return result;
425
-
426
- } catch (error) {
427
- console.error('Manual MathML conversion failed:', error);
428
- // Extract text content as final fallback
429
- const tempDiv = document.createElement('div');
430
- tempDiv.innerHTML = mathml;
431
- return tempDiv.textContent || mathml;
432
- }
433
- }