├── dist └── .keep ├── lib └── .keep ├── .gitignore ├── publish.sh ├── .npmignore ├── config ├── rollup.config.es.js ├── rollup.config.umd.js ├── rollup.config.browser.es.js ├── rollup.config.cjs.js ├── rollup.config.iife.js ├── rollup.config.browser.cjs.js ├── rollup.config.browser.umd.js └── rollup.config.js ├── test ├── test-root.js ├── test-scripts-sub-sup.js ├── test_helper.js ├── test-fractal.js ├── test-brackets.js ├── test-multiply-scripts.js ├── test-matrix.js ├── test-fence.js ├── test-base.js └── test-scripts-under-over.js ├── src ├── node-tool.js ├── html-parser.js ├── brackets.js ├── math-symbol.js └── mathml2latex.js ├── README.md ├── LICENSE └── package.json /dist/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | lib/*.js 3 | node_modules 4 | .ignore 5 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # login npm first 4 | npm run build 5 | npm publish 6 | 7 | # logout 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | config 4 | src 5 | package-lock.json 6 | .git 7 | .gitignore 8 | .ignore 9 | publish.sh 10 | -------------------------------------------------------------------------------- /config/rollup.config.es.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.config' 2 | 3 | export default config({ 4 | output: { 5 | file: 'lib/mathml2latex.es.js', 6 | format: 'es' 7 | }, 8 | browser: false 9 | }) 10 | -------------------------------------------------------------------------------- /config/rollup.config.umd.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.config' 2 | 3 | export default config({ 4 | output: { 5 | file: 'lib/mathml2latex.umd.js', 6 | format: 'umd', 7 | name: 'MathML2LaTeX' 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /config/rollup.config.browser.es.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.config' 2 | 3 | export default config({ 4 | output: { 5 | file: 'lib/mathml2latex.browser.es.js', 6 | format: 'es' 7 | }, 8 | browser: true 9 | }) 10 | -------------------------------------------------------------------------------- /config/rollup.config.cjs.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.config' 2 | 3 | export default config({ 4 | output: { 5 | file: 'lib/mathml2latex.cjs.js', 6 | format: 'cjs', 7 | exports: 'auto', 8 | }, 9 | browser: false 10 | }) 11 | -------------------------------------------------------------------------------- /config/rollup.config.iife.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.config' 2 | 3 | export default config({ 4 | output: { 5 | file: 'dist/mathml2latex.js', 6 | format: 'iife', 7 | name: 'MathML2LaTeX' 8 | }, 9 | browser: true 10 | }) 11 | -------------------------------------------------------------------------------- /config/rollup.config.browser.cjs.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.config' 2 | 3 | export default config({ 4 | output: { 5 | file: 'lib/mathml2latex.browser.cjs.js', 6 | format: 'cjs', 7 | exports: 'auto', 8 | }, 9 | browser: true 10 | }) 11 | -------------------------------------------------------------------------------- /config/rollup.config.browser.umd.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.config' 2 | 3 | export default config({ 4 | output: { 5 | file: 'lib/mathml2latex.browser.umd.js', 6 | format: 'umd', 7 | name: 'MathML2LaTeX' 8 | }, 9 | browser: true 10 | }) 11 | -------------------------------------------------------------------------------- /test/test-root.js: -------------------------------------------------------------------------------- 1 | const {test, convert} = require('./test_helper'); 2 | 3 | 4 | test("root", convert({ 5 | from: ' 27 3 ', 6 | to: '\\sqrt[3]{27}' 7 | })); 8 | 9 | test("sqrt-1", convert({ 10 | from: ' 4 ', 11 | to: '\\sqrt{4}' 12 | })); 13 | 14 | test("sqrt-2", convert({ 15 | from: ' 4 n ', 16 | to: '\\sqrt{4n}' 17 | })); 18 | -------------------------------------------------------------------------------- /test/test-scripts-sub-sup.js: -------------------------------------------------------------------------------- 1 | const {test, convert} = require('./test_helper'); 2 | 3 | test("msub", convert({ 4 | from: 'a22', 5 | to: 'a_{22}' 6 | })); 7 | 8 | test("msup", convert({ 9 | from: 'an', 10 | to: 'a^{n}' 11 | })); 12 | 13 | test("msubsup", convert({ 14 | from: ` 15 | 16 | a 17 | i 18 | 22 19 | 20 | `, 21 | to: 'a_{i}^{22}' 22 | })); 23 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs' 2 | import replace from '@rollup/plugin-replace' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | 5 | export default function (config) { 6 | return { 7 | input: 'src/mathml2latex.js', 8 | output: config.output, 9 | external: ['domino'], 10 | plugins: [ 11 | commonjs(), 12 | replace({ 'process.browser': JSON.stringify(!!config.browser), preventAssignment: true }), 13 | resolve() 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/test_helper.js: -------------------------------------------------------------------------------- 1 | 2 | // https://github.com/substack/tape 3 | 4 | 5 | const test = require('tape'); 6 | const MathML2LaTeX = require('../lib/mathml2latex.cjs.js'); 7 | 8 | function convert(options) { 9 | return (t) => { 10 | const {from, to} = options; 11 | t.plan(1); 12 | // Do the convert 13 | const mathmlHtml = mathml(from); 14 | const convertedLatex = MathML2LaTeX.convert(mathmlHtml); 15 | t.equal(convertedLatex, to); 16 | } 17 | } 18 | 19 | function mathml(innerHtml){ 20 | return `${innerHtml}`; 21 | } 22 | 23 | module.exports = { 24 | test: test, 25 | convert: convert 26 | } 27 | -------------------------------------------------------------------------------- /src/node-tool.js: -------------------------------------------------------------------------------- 1 | 2 | import HTMLParser from './html-parser.js'; 3 | 4 | const NodeTool = { 5 | parseMath: function(html) { 6 | const parser = new HTMLParser(); 7 | const doc = parser.parseFromString(html, 'text/html'); 8 | return doc.querySelector('math'); 9 | }, 10 | getChildren: function(node) { 11 | return node.children; 12 | }, 13 | getNodeName: function(node) { 14 | return node.tagName.toLowerCase(); 15 | }, 16 | getNodeText: function(node) { 17 | return node.textContent; 18 | }, 19 | getAttr: function(node, attrName, defaultValue) { 20 | const value = node.getAttribute(attrName); 21 | if ( value === null) { 22 | return defaultValue; 23 | } else { 24 | return value; 25 | } 26 | }, 27 | getPrevNode: function(node) { 28 | return node.previousElementSibling; 29 | }, 30 | getNextNode: function(node) { 31 | return node.nextElementSibling; 32 | } 33 | } 34 | 35 | export default NodeTool; 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # mathml2latex 3 | 4 | It's a javascript library to convert mathML to latex 5 | 6 | ## Build 7 | 8 | Clone this repo and run `npm run build`, It'll generate belowing files: 9 | 10 | ```text 11 | lib 12 | ├── mathml2latex.browser.cjs.js 13 | ├── mathml2latex.browser.es.js 14 | ├── mathml2latex.browser.umd.js 15 | ├── mathml2latex.cjs.js 16 | ├── mathml2latex.es.js 17 | └── mathml2latex.umd.js 18 | dist 19 | └── mathml2latex.js 20 | ``` 21 | 22 | ## usage 23 | 24 | ### load library 25 | 26 | In browser environment, you need to build the library first, then load it: 27 | 28 | ```html 29 | 30 | ``` 31 | 32 | 33 | Using with npm 34 | 35 | ```shell 36 | npm install mathml2latex 37 | ``` 38 | 39 | ```javascript 40 | const MathMl2LaTeX = require('mathml2latex') 41 | ``` 42 | 43 | ### convert mathml html 44 | 45 | ```javascript 46 | const mathmlHtml = 'ab'; 47 | const latex = MathML2LaTeX.convert(mathmlHtml); // => \frac{a}{b} 48 | ``` 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mika 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test-fractal.js: -------------------------------------------------------------------------------- 1 | const {test, convert} = require('./test_helper'); 2 | 3 | test("mfrac", convert({ 4 | from: '4n', 5 | to: '\\frac{4}{n}' 6 | })); 7 | 8 | test("mfrac linethickness is none", convert({ 9 | from: ` 10 | 11 | 12 | ab 13 | 14 | 15 | `, 16 | to: '{}_{b}^{a}' 17 | })); 18 | 19 | test("binomial coefficients-0", convert({ 20 | from: ` 21 | 22 | ( 23 | 24 | ab 25 | 26 | ) 27 | 28 | `, 29 | to: '\\binom{a}{b}' 30 | })); 31 | 32 | test("binomial coefficients-0px", convert({ 33 | from: ` 34 | 35 | ( 36 | 37 | ab 38 | 39 | ) 40 | 41 | `, 42 | to: '\\binom{a}{b}' 43 | })); 44 | 45 | test("bevelled fraction", convert({ 46 | from: ` 47 | 48 | ab 49 | 50 | `, 51 | to: '{}^{a}/_{b}' 52 | })); 53 | -------------------------------------------------------------------------------- /test/test-brackets.js: -------------------------------------------------------------------------------- 1 | const {test, convert} = require('./test_helper'); 2 | 3 | test("vertical bar", convert({ 4 | from: ` 5 | | 6 | a 7 | | 8 | `, 9 | to: '\\left|a\\right|' 10 | })); 11 | 12 | test('vertical double bar', convert({ 13 | from: ` 14 | 15 | a 16 | 17 | `, 18 | to: '\\left\\|a\\right\\|' 19 | })); 20 | 21 | test('vertical bar multiply', convert({ 22 | from: ` 23 | |a| 24 | + 25 | |b| 26 | = 27 | 4 28 | `, 29 | to: '\\left|a\\right| + \\left|b\\right| = 4' 30 | })); 31 | 32 | test('special brackets', convert({ 33 | from: ` 34 | 35 | a 36 | 37 | `, 38 | to: '\\left\\langle a \\right\\rangle' 39 | })); 40 | 41 | 42 | test('brackets-with-stretchy-false', convert({ 43 | from: ` 44 | { 45 | a 46 | } 47 | `, 48 | to: '\\{a\\}' 49 | })); 50 | 51 | // test a delimiter on only one side of expression is required 52 | // \left.\frac{x^3}{3}\right|_0^1 53 | -------------------------------------------------------------------------------- /test/test-multiply-scripts.js: -------------------------------------------------------------------------------- 1 | const {test, convert} = require('./test_helper'); 2 | 3 | test("multiscripts-simple", convert({ 4 | from: ` 5 | 6 | G 7 | a 8 | b 9 | 10 | `, 11 | to: 'G_{a}^{b}' 12 | })); 13 | 14 | test("multiscripts-without-prescripts", convert({ 15 | from: ` 16 | 17 | G 18 | 19 | i 20 | 21 | 22 | k 23 | l 24 | 25 | 26 | n 27 | 28 | `, 29 | to: 'G_{i k \\:}^{\\: l n}' 30 | })); 31 | 32 | test('mmultiscripts-with-prescripts', convert({ 33 | from: ` 34 | 35 | G 36 | 37 | i 38 | 39 | 40 | k 41 | l 42 | 43 | 44 | 45 | 46 | a 47 | 48 | b 49 | c 50 | 51 | `, 52 | to: '_{\\: b}^{a c}G_{i k}^{\\: l}' 53 | })); 54 | -------------------------------------------------------------------------------- /test/test-matrix.js: -------------------------------------------------------------------------------- 1 | const {test, convert} = require('./test_helper'); 2 | 3 | const tableRows = ` 4 | 5 | a 6 | b 7 | c 8 | 9 | 10 | d 11 | e 12 | f 13 | 14 | `; 15 | 16 | 17 | test("matrix without brackets", convert({ 18 | from: `${tableRows}`, 19 | to: '\\begin{matrix} a & b & c \\\\ d & e & f \\\\ \\end{matrix}' 20 | })); 21 | 22 | 23 | // Brackets 24 | // 25 | // ( ) Round brackets or parentheses 26 | // [ ] Square brackets or brackets 27 | // { } Curly brackets or braces 28 | // ⟨ ⟩ Angle brackets or chevrons 29 | 30 | 31 | test("matrix with Round brackets (parentheses)", convert({ 32 | from: `(${tableRows}) `, 33 | to: '\\left(\\begin{matrix} a & b & c \\\\ d & e & f \\\\ \\end{matrix}\\right)' 34 | })); 35 | 36 | 37 | // issue 17 38 | test("mtd has more than one child", convert({ 39 | from: ` 40 | 41 | 42 | 43 | 1 44 | 45 | 2 46 | 47 | 48 | 49 | 50 | 3 51 | 52 | 4 53 | 54 | 55 | 56 | `, 57 | to: '\\begin{matrix} 1 \\cdot 2 \\\\ 3 \\cdot 4 \\\\ \\end{matrix}' 58 | })); 59 | -------------------------------------------------------------------------------- /test/test-fence.js: -------------------------------------------------------------------------------- 1 | const {test, convert} = require('./test_helper'); 2 | 3 | test('mfenced-default', convert({ 4 | from: ` 5 | 6 | 1 7 | 2 8 | 3 9 | 10 | `, 11 | to: '\\left(1,2,3\\right)' 12 | })); 13 | 14 | test('mfenced-empty-separators', convert({ 15 | from: ` 16 | 17 | 1 18 | 2 19 | 3 20 | 21 | `, 22 | to: '\\left(123\\right)' 23 | })); 24 | 25 | test('mfenced-custom-separators', convert({ 26 | from: ` 27 | 28 | 1 29 | 2 30 | 3 31 | 32 | `, 33 | to: '\\left(1.2.3\\right)' 34 | })); 35 | 36 | test('mfenced-multiply-separators', convert({ 37 | from: ` 38 | 39 | 1 40 | 2 41 | 3 42 | 4 43 | 5 44 | 45 | `, 46 | to: '\\left(1.2,3_4_5\\right)' 47 | })); 48 | 49 | test('mfenced-empty-open', convert({ 50 | from: ` 51 | 52 | 1 53 | 2 54 | 3 55 | 56 | `, 57 | to: '1,2,3\\right)' 58 | })); 59 | 60 | test('mfenced-custom-open', convert({ 61 | from: ` 62 | 63 | 1 64 | 2 65 | 3 66 | 67 | `, 68 | to: '\\left\\{1,2,3\\right)' 69 | })); 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mathml2latex", 3 | "version": "1.1.3", 4 | "description": "Convert mathml to latex.", 5 | "author": "Mika", 6 | "license": "MIT", 7 | "keywords": [ 8 | "converter", 9 | "mathml", 10 | "latex" 11 | ], 12 | "homepage": "https://github.com/mika-cn/mathml2latex", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/mika-cn/mathml2latex.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/mika-cn/mathml2latex/issues" 19 | }, 20 | "main": "lib/mathml2latex.cjs.js", 21 | "module": "lib/mathml2latex.es.js", 22 | "browser": { 23 | "domino": false, 24 | "./lib/mathml2latex.cjs.js": "./lib/mathml2latex.browser.cjs.js", 25 | "./lib/mathml2latex.es.js": "./lib/mathml2latex.browser.es.js", 26 | "./lib/mathml2latex.umd.js": "./lib/mathml2latex.browser.umd.js" 27 | }, 28 | "dependencies": { 29 | "domino": "^2.1.6" 30 | }, 31 | "devDependencies": { 32 | "@rollup/plugin-commonjs": "^19.0.0", 33 | "@rollup/plugin-node-resolve": "13.0.0", 34 | "@rollup/plugin-replace": "^2.4.2", 35 | "rollup": "^2.52.3", 36 | "tape": "^4.11.0" 37 | }, 38 | "files": [ 39 | "lib", 40 | "dist" 41 | ], 42 | "scripts": { 43 | "build": "npm run build-cjs && npm run build-es && npm run build-umd && npm run build-iife", 44 | "build-cjs": "rollup -c config/rollup.config.cjs.js && rollup -c config/rollup.config.browser.cjs.js", 45 | "build-es": "rollup -c config/rollup.config.es.js && rollup -c config/rollup.config.browser.es.js", 46 | "build-umd": "rollup -c config/rollup.config.umd.js && rollup -c config/rollup.config.browser.umd.js", 47 | "build-iife": "rollup -c config/rollup.config.iife.js", 48 | "test": "rollup -c config/rollup.config.cjs.js && tape test/test-*.js" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/html-parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Set up window for Node.js 3 | */ 4 | 5 | const root = (typeof window !== 'undefined' ? window : {}) 6 | 7 | /* 8 | * Parsing HTML strings 9 | */ 10 | 11 | function canParseHTMLNatively () { 12 | const Parser = root.DOMParser 13 | let canParse = false 14 | 15 | // Adapted from https://gist.github.com/1129031 16 | // Firefox/Opera/IE throw errors on unsupported types 17 | try { 18 | // WebKit returns null on unsupported types 19 | if (new Parser().parseFromString('', 'text/html')) { 20 | canParse = true 21 | } 22 | } catch (e) {} 23 | 24 | return canParse 25 | } 26 | 27 | function createHTMLParser () { 28 | const Parser = function () {} 29 | 30 | if (process.browser) { 31 | if (shouldUseActiveX()) { 32 | Parser.prototype.parseFromString = function (string) { 33 | const doc = new window.ActiveXObject('htmlfile') 34 | doc.designMode = 'on' // disable on-page scripts 35 | doc.open() 36 | doc.write(string) 37 | doc.close() 38 | return doc 39 | } 40 | } else { 41 | Parser.prototype.parseFromString = function (string) { 42 | const doc = document.implementation.createHTMLDocument('') 43 | doc.open() 44 | doc.write(string) 45 | doc.close() 46 | return doc 47 | } 48 | } 49 | } else { 50 | const domino = require('domino') 51 | Parser.prototype.parseFromString = function (string) { 52 | return domino.createDocument(string) 53 | } 54 | } 55 | return Parser 56 | } 57 | 58 | function shouldUseActiveX () { 59 | let useActiveX = false 60 | try { 61 | document.implementation.createHTMLDocument('').open() 62 | } catch (e) { 63 | if (window.ActiveXObject) useActiveX = true 64 | } 65 | return useActiveX 66 | } 67 | 68 | export default canParseHTMLNatively() ? root.DOMParser : createHTMLParser() 69 | -------------------------------------------------------------------------------- /src/brackets.js: -------------------------------------------------------------------------------- 1 | 2 | const Brackets = { 3 | left: ['(', '[', '{', '|', '‖', '⟨', '⌊', '⌈', '⌜'], 4 | right: [')', ']', '}', '|', '‖', '⟩', '⌋', '⌉', '⌝'], 5 | isPair: function(l, r){ 6 | const idx = this.left.indexOf(l); 7 | return r === this.right[idx]; 8 | }, 9 | contains: function(it) { 10 | return this.isLeft(it) || this.isRight(it); 11 | }, 12 | isLeft: function(it) { 13 | return this.left.indexOf(it) > -1 14 | }, 15 | isRight: function(it) { 16 | return this.right.indexOf(it) > -1; 17 | }, 18 | parseLeft: function(it, stretchy = true) { 19 | if(this.left.indexOf(it) < 0){ return it} 20 | let r = ''; 21 | switch(it){ 22 | case '(': 23 | case '[': 24 | case '|': r = `\\left${it}`; 25 | break; 26 | case '‖': r = '\\left\\|'; 27 | break; 28 | case '{': r = '\\left\\{'; 29 | break; 30 | case '⟨': r = '\\left\\langle '; 31 | break; 32 | case '⌊': r = '\\left\\lfloor '; 33 | break; 34 | case '⌈': r = '\\left\\lceil '; 35 | break; 36 | case '⌜': r = '\\left\\ulcorner '; 37 | break; 38 | } 39 | return (stretchy ? r : r.replace('\\left', '')); 40 | }, 41 | 42 | parseRight: function(it, stretchy = true) { 43 | if(this.right.indexOf(it) < 0){ return it} 44 | let r = ''; 45 | switch(it){ 46 | case ')': 47 | case ']': 48 | case '|': r = `\\right${it}`; 49 | break; 50 | case '‖': r = '\\right\\|'; 51 | break; 52 | case '}': r = '\\right\\}'; 53 | break; 54 | case '⟩': r = ' \\right\\rangle'; 55 | break; 56 | case '⌋': r = ' \\right\\rfloor'; 57 | break; 58 | case '⌉': r = ' \\right\\rceil'; 59 | break; 60 | case '⌝': r = ' \\right\\urcorner'; 61 | break; 62 | } 63 | return (stretchy ? r : r.replace('\\right', '')); 64 | } 65 | } 66 | 67 | export default Brackets; 68 | -------------------------------------------------------------------------------- /test/test-base.js: -------------------------------------------------------------------------------- 1 | const {test, convert} = require('./test_helper'); 2 | 3 | // ---------- math identifier ----------- 4 | test("mi-normal-char", convert({ 5 | from: 'x', 6 | to: 'x' 7 | })); 8 | 9 | test("mi-greek-letter-alpha", convert({ 10 | from: 'α', 11 | to: '\\alpha ' 12 | })); 13 | 14 | test("mi-math-func-name", convert({ 15 | from: 'cos', 16 | to: '\\cos ' 17 | })); 18 | 19 | // ---------- math operation ----------- 20 | test("mo-normal-char", convert({ 21 | from: 'mod', 22 | to: 'mod' 23 | })); 24 | 25 | test("mo-binary-operator", convert({ 26 | from: ' +', 27 | to: ' + ' 28 | })); 29 | 30 | test("mo-relation-char-0", convert({ 31 | from: '<', 32 | to: ' < ' 33 | })); 34 | 35 | // ~ small Tilde 36 | test("mi-relation-char-wave-1", convert({ 37 | from: '~', 38 | to: ' \\sim ' 39 | })); 40 | 41 | // ∼ Tilde Operator (it's different to above ...) 42 | test("mi-relation-char-wave-2", convert({ 43 | from: '', 44 | to: ' \\sim ' 45 | })); 46 | 47 | 48 | test("mo-math-func-name", convert({ 49 | from: 'lim', 50 | to: '\\lim ' 51 | })); 52 | 53 | test("mo-N-Ary-Summation", convert({ 54 | from: '', 55 | to: '\\sum ' 56 | })); 57 | 58 | // overlap names 59 | function testOverlapFunName(name) { 60 | test(`mo-math-func-name(overlap): ${name}`, convert({ 61 | from: `${name}`, 62 | to: `\\${name} ` 63 | })); 64 | } 65 | 66 | // copy from src/math-symbol.js 67 | const names = [ 68 | "arcsin" , "sinh" , "sin" , "sec" , 69 | "arccos" , "cosh" , "cos" , "csc" , 70 | "arctan" , "tanh" , "tan" , 71 | "arccot" , "coth" , "cot" , 72 | 73 | "limsup" , "liminf" , "exp" , "ker" , 74 | "deg" , "gcd" , "lg" , "ln" , 75 | "Pr" , "sup" , "det" , "hom" , 76 | "lim" , "log" , "arg" , "dim" , 77 | "inf" , "max" , "min" , 78 | ]; 79 | 80 | names.forEach(testOverlapFunName) 81 | 82 | 83 | test("mo-math-func-name(overlap), multiply names", convert({ 84 | from: 'sinsinh', 85 | to: '\\sin \\sinh ', 86 | })); 87 | -------------------------------------------------------------------------------- /test/test-scripts-under-over.js: -------------------------------------------------------------------------------- 1 | const {test, convert} = require('./test_helper'); 2 | 3 | test("munder", convert({ 4 | from: ` 5 | 6 | a 7 | i 8 | 9 | `, 10 | to: 'a\\limits_{i}' 11 | })); 12 | 13 | 14 | //------------------------------------------------- 15 | 16 | test("mover", convert({ 17 | from: ` 18 | 19 | a 20 | i 21 | 22 | `, 23 | to: 'a\\limits^{i}' 24 | })); 25 | 26 | test("mover-overbrace", convert({ 27 | from: ` 28 | 29 | a 30 | 31 | 32 | `, 33 | to: '\\overbrace{a}' 34 | })); 35 | 36 | test("mover-overbrace-2-layer-1", convert({ 37 | from: ` 38 | 39 | 40 | 41 | x+...+x 42 | 43 | 44 | 45 | k times 46 | 47 | `, 48 | to: '\\overbrace{x + ... + x}\\limits^{\\text{k times}}' 49 | })); 50 | 51 | test("mover-overbrace-2-layer-2", convert({ 52 | from: ` 53 | 54 | 55 | x+...+x 56 | 57 | 58 | 59 | 60 | k times 61 | 62 | 63 | 64 | `, 65 | to: '\\overbrace{x + ... + x}\\limits^{\\text{k times}}' 66 | })); 67 | 68 | //------------------------------------------------- 69 | 70 | test("munderover", convert({ 71 | from: ` 72 | 73 | a 74 | i 75 | n 76 | 77 | `, 78 | to: 'a\\limits_{i}^{n}' 79 | })); 80 | 81 | /* 82 | test("munderover-overbrace", convert({ 83 | from: ` 84 | 85 | 86 | 87 | x+...+x 88 | 89 | k times 90 | 91 | `, 92 | to: '\\overbrace{x + ... + x}\\limits^{\\text{k times}}' 93 | })); 94 | */ 95 | -------------------------------------------------------------------------------- /src/math-symbol.js: -------------------------------------------------------------------------------- 1 | 2 | // @see https://en.wikibooks.org/wiki/LaTeX/Mathematics#List_of_mathematical_symbols 3 | // @see https://www.andy-roberts.net/res/writing/latex/symbols.pdf (more completed) 4 | // @see http://www.rpi.edu/dept/arc/training/latex/LaTeX_symbols.pdf (wtf) 5 | // https://oeis.org/wiki/List_of_LaTeX_mathematical_symbols 6 | 7 | // accessed directly from keyboard 8 | // + - = ! / ( ) [ ] < > | ' : * 9 | 10 | const MathSymbol = { 11 | parseIdentifier: function(it) { 12 | if(it.length === 0){ return '' } 13 | if(it.length === 1){ 14 | const charCode = it.charCodeAt(0); 15 | let index = this.greekLetter.decimals.indexOf(charCode) 16 | if ( index > -1) { 17 | return this.greekLetter.scripts[index] + ' '; 18 | } else { 19 | return it; 20 | } 21 | } else { 22 | return this.parseMathFunction(it); 23 | } 24 | }, 25 | 26 | parseOperator: function(it) { 27 | if(it.length === 0){ return ''} 28 | if(it.length === 1){ 29 | const charCode = it.charCodeAt(0); 30 | const opSymbols = [ 31 | this.bigCommand, 32 | this.relation, 33 | this.binaryOperation, 34 | this.setAndLogic, 35 | this.delimiter, 36 | this.other 37 | ]; 38 | 39 | const padSpaceBothSide = [false, true, true, false, false, false] 40 | 41 | for(let i = 0; i < opSymbols.length; i++){ 42 | const opSymbol = opSymbols[i]; 43 | const index = opSymbol.decimals.indexOf(charCode); 44 | if(index > -1) { 45 | if(padSpaceBothSide[i]){ 46 | return [' ', opSymbol.scripts[index], ' '].join(''); 47 | }else{ 48 | return opSymbol.scripts[index] + ' '; 49 | } 50 | } 51 | } 52 | return it; 53 | } else { 54 | return this.parseMathFunction(it); 55 | } 56 | }, 57 | 58 | parseMathFunction: function (it) { 59 | const marker = T.createMarker(); 60 | const replacements = []; 61 | this.mathFunction.names.forEach((name, index) => { 62 | const regExp = new RegExp(name, 'g'); 63 | if(it.match(regExp)){ 64 | replacements.push(this.mathFunction.scripts[index]); 65 | it = it.replace(regExp, marker.next() + ' '); 66 | } 67 | }); 68 | return marker.replaceBack(it, replacements); 69 | }, 70 | 71 | //FIXME COMPLETE ME 72 | overScript: { 73 | decimals: [9182], 74 | templates: [ 75 | "\\overbrace{@v}", 76 | ] 77 | }, 78 | 79 | //FIXME COMPLETE ME 80 | underScript: { 81 | decimals: [9183], 82 | templates: [ 83 | "\\underbrace{@v}" 84 | ] 85 | }, 86 | 87 | // sum, integral... 88 | bigCommand: { 89 | decimals: [8721, 8719, 8720, 10753, 10754, 10752, 8899, 8898, 10756, 10758, 8897, 8896, 8747, 8750, 8748, 8749, 10764, 8747], 90 | scripts: [ 91 | "\\sum", 92 | "\\prod", 93 | "\\coprod", 94 | "\\bigoplus", 95 | "\\bigotimes", 96 | "\\bigodot", 97 | "\\bigcup", 98 | "\\bigcap", 99 | "\\biguplus", 100 | "\\bigsqcup", 101 | "\\bigvee", 102 | "\\bigwedge", 103 | "\\int", 104 | "\\oint", 105 | "\\iint", 106 | "\\iiint", 107 | "\\iiiint", 108 | "\\idotsint", 109 | ] 110 | }, 111 | 112 | // mo 113 | relation: { 114 | decimals: [60, 62, 61, 8741, 8742, 8804, 8805, 8784, 8781, 8904, 8810, 8811, 8801, 8866, 8867, 8834, 8835, 8776, 8712, 8715, 8838, 8839, 8773, 8995, 8994, 8840, 8841, 8771, 8872, 8713, 8847, 8848, 126, 8764, 8869, 8739, 8849, 8850, 8733, 8826, 8827, 10927, 10928, 8800, 8738, 8737], 115 | scripts: [ 116 | "<", 117 | ">", 118 | "=", 119 | "\\parallel", 120 | "\\nparallel", 121 | "\\leq", 122 | "\\geq", 123 | "\\doteq", 124 | "\\asymp", 125 | "\\bowtie", 126 | "\\ll", 127 | "\\gg", 128 | "\\equiv", 129 | "\\vdash", 130 | "\\dashv", 131 | "\\subset", 132 | "\\supset", 133 | "\\approx", 134 | "\\in", 135 | "\\ni", 136 | "\\subseteq", 137 | "\\supseteq", 138 | "\\cong", 139 | "\\smile", 140 | "\\frown", 141 | "\\nsubseteq", 142 | "\\nsupseteq", 143 | "\\simeq", 144 | "\\models", 145 | "\\notin", 146 | "\\sqsubset", 147 | "\\sqsupset", 148 | "\\sim", 149 | "\\sim", 150 | "\\perp", 151 | "\\mid", 152 | "\\sqsubseteq", 153 | "\\sqsupseteq", 154 | "\\propto", 155 | "\\prec", 156 | "\\succ", 157 | "\\preceq", 158 | "\\succeq", 159 | "\\neq", 160 | "\\sphericalangle", 161 | "\\measuredangle" 162 | ] 163 | }, 164 | 165 | // complete 166 | binaryOperation: { 167 | decimals: [43, 45, 177, 8745, 8900, 8853, 8723, 8746, 9651, 8854, 215, 8846, 9661, 8855, 247, 8851, 9667, 8856, 8727, 8852, 9657, 8857, 8902, 8744, 9711, 8728, 8224, 8743, 8729, 8726, 8225, 8901, 8768, 10815], 168 | scripts: [ 169 | "+", 170 | "-", 171 | "\\pm", 172 | "\\cap", 173 | "\\diamond", 174 | "\\oplus", 175 | "\\mp", 176 | "\\cup", 177 | "\\bigtriangleup", 178 | "\\ominus", 179 | "\\times", 180 | "\\uplus", 181 | "\\bigtriangledown", 182 | "\\otimes", 183 | "\\div", 184 | "\\sqcap", 185 | "\\triangleleft", 186 | "\\oslash", 187 | "\\ast", 188 | "\\sqcup", 189 | "\\triangleright", 190 | "\\odot", 191 | "\\star", 192 | "\\vee", 193 | "\\bigcirc", 194 | "\\circ", 195 | "\\dagger", 196 | "\\wedge", 197 | "\\bullet", 198 | "\\setminus", 199 | "\\ddagger", 200 | "\\cdot", 201 | "\\wr", 202 | "\\amalg" 203 | ] 204 | }, 205 | 206 | setAndLogic: { 207 | decimals: [8707, 8594, 8594, 8708, 8592, 8592, 8704, 8614, 172, 10233, 8834, 8658, 10233, 8835, 8596, 8712, 10234, 8713, 8660, 8715, 8868, 8743, 8869, 8744, 8709, 8709], 208 | scripts: [ 209 | "\\exists", 210 | "\\rightarrow", 211 | "\\to", 212 | "\\nexists", 213 | "\\leftarrow", 214 | "\\gets", 215 | "\\forall", 216 | "\\mapsto", 217 | "\\neg", 218 | "\\implies", 219 | "\\subset", 220 | "\\Rightarrow", 221 | "\\implies", 222 | "\\supset", 223 | "\\leftrightarrow", 224 | "\\in", 225 | "\\iff", 226 | "\\notin", 227 | "\\Leftrightarrow", 228 | "\\ni", 229 | "\\top", 230 | "\\land", 231 | "\\bot", 232 | "\\lor", 233 | "\\emptyset", 234 | "\\varnothing" 235 | ] 236 | }, 237 | 238 | delimiter: { 239 | decimals: [124, 8739, 8214, 47, 8726, 123, 125, 10216, 10217, 8593, 8657, 8968, 8969, 8595, 8659, 8970, 8971], 240 | scripts: [ 241 | "|", 242 | "\\mid", 243 | "\\|", 244 | "/", 245 | "\\backslash", 246 | "\\{", 247 | "\\}", 248 | "\\langle", 249 | "\\rangle", 250 | "\\uparrow", 251 | "\\Uparrow", 252 | "\\lceil", 253 | "\\rceil", 254 | "\\downarrow", 255 | "\\Downarrow", 256 | "\\lfloor", 257 | "\\rfloor" 258 | ] 259 | }, 260 | 261 | greekLetter: { 262 | decimals: [ 913, 945, 925, 957, 914, 946, 926, 958, 915, 947, 927, 959, 916, 948, 928, 960, 982, 917, 1013, 949, 929, 961, 1009, 918, 950, 931, 963, 962, 919, 951, 932, 964, 920, 952, 977, 933, 965, 921, 953, 934, 981, 966, 922, 954, 1008, 935, 967, 923, 955, 936, 968, 924, 956, 937, 969 ], 263 | scripts: [ 264 | "A" , "\\alpha" , 265 | "N" , "\\nu" , 266 | "B" , "\\beta" , 267 | "\\Xi" , "\\xi" , 268 | "\\Gamma" , "\\gamma" , 269 | "O" , "o" , 270 | "\\Delta" , "\\delta" , 271 | "\\Pi" , "\\pi" , "\\varpi" , 272 | "E" , "\\epsilon" , "\\varepsilon" , 273 | "P" , "\\rho" , "\\varrho" , 274 | "Z" , "\\zeta" , 275 | "\\Sigma" , "\\sigma" , "\\varsigma" , 276 | "H" , "\\eta" , 277 | "T" , "\\tau" , 278 | "\\Theta" , "\\theta" , "\\vartheta" , 279 | "\\Upsilon" , "\\upsilon" , 280 | "I" , "\\iota" , 281 | "\\Phi" , "\\phi" , "\\varphi" , 282 | "K" , "\\kappa" , "\\varkappa" , 283 | "X" , "\\chi" , 284 | "\\Lambda" , "\\lambda" , 285 | "\\Psi" , "\\psi" , 286 | "M" , "\\mu" , 287 | "\\Omega" , "\\omega" 288 | ] 289 | }, 290 | 291 | 292 | other: { 293 | decimals: [8706, 305, 8476, 8711, 8501, 240, 567, 8465, 9723, 8502, 8463, 8467, 8472, 8734, 8503], 294 | scripts: [ 295 | "\\partial", 296 | "\\imath", 297 | "\\Re", 298 | "\\nabla", 299 | "\\aleph", 300 | "\\eth", 301 | "\\jmath", 302 | "\\Im", 303 | "\\Box", 304 | "\\beth", 305 | "\\hbar", 306 | "\\ell", 307 | "\\wp", 308 | "\\infty", 309 | "\\gimel" 310 | ] 311 | }, 312 | 313 | // complete 314 | // Be careful, the order of these name matters (overlap situation). 315 | mathFunction: { 316 | 317 | names: [ 318 | "arcsin" , "sinh" , "sin" , "sec" , 319 | "arccos" , "cosh" , "cos" , "csc" , 320 | "arctan" , "tanh" , "tan" , 321 | "arccot" , "coth" , "cot" , 322 | 323 | "limsup" , "liminf" , "exp" , "ker" , 324 | "deg" , "gcd" , "lg" , "ln" , 325 | "Pr" , "sup" , "det" , "hom" , 326 | "lim" , "log" , "arg" , "dim" , 327 | "inf" , "max" , "min" , 328 | ], 329 | scripts: [ 330 | "\\arcsin" , "\\sinh" , "\\sin" , "\\sec" , 331 | "\\arccos" , "\\cosh" , "\\cos" , "\\csc" , 332 | "\\arctan" , "\\tanh" , "\\tan" , 333 | "\\arccot" , "\\coth" , "\\cot" , 334 | 335 | "\\limsup" , "\\liminf" , "\\exp" , "\\ker" , 336 | "\\deg" , "\\gcd" , "\\lg" , "\\ln" , 337 | "\\Pr" , "\\sup" , "\\det" , "\\hom" , 338 | "\\lim" , "\\log" , "\\arg" , "\\dim" , 339 | "\\inf" , "\\max" , "\\min" , 340 | ] 341 | } 342 | }; 343 | 344 | const T = {}; // Tool 345 | T.createMarker = function() { 346 | return { 347 | idx: -1, 348 | reReplace: /@\[\[(\d+)\]\]/mg, 349 | next: function() { 350 | return `@[[${++this.idx}]]` 351 | }, 352 | replaceBack: function(str, replacements) { 353 | const This = this; 354 | return str.replace(this.reReplace, (match, p1) => { 355 | const index = parseInt(p1); 356 | return replacements[index]; 357 | }); 358 | } 359 | } 360 | } 361 | 362 | 363 | export default MathSymbol; 364 | -------------------------------------------------------------------------------- /src/mathml2latex.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict"; 3 | // latex resource 4 | // https://en.wikibooks.org/wiki/LaTeX/Mathematics 5 | // https://en.wikibooks.org/wiki/LaTeX/Advanced_Mathematics 6 | // https://www.andy-roberts.net/writing/latex/mathematics_1 7 | // https://www.andy-roberts.net/writing/latex/mathematics_2 8 | 9 | import Brackets from './brackets.js'; 10 | import MathSymbol from './math-symbol.js'; 11 | import NodeTool from './node-tool.js'; 12 | 13 | 14 | function convert(mathmlHtml){ 15 | const math = NodeTool.parseMath(mathmlHtml); 16 | return toLatex(parse(math)); 17 | } 18 | 19 | function toLatex(result) { 20 | // binomial coefficients 21 | result = result.replace(/\\left\(\\DELETE_BRACKET_L/g, ''); 22 | result = result.replace(/\\DELETE_BRACKET_R\\right\)/g, ''); 23 | result = result.replace(/\\DELETE_BRACKET_L/g, ''); 24 | result = result.replace(/\\DELETE_BRACKET_R/g, ''); 25 | return result; 26 | } 27 | 28 | function parse(node) { 29 | const children = NodeTool.getChildren(node); 30 | if (!children || children.length === 0) { 31 | return parseLeaf(node); 32 | } else { 33 | return parseContainer(node, children); 34 | } 35 | } 36 | 37 | // @see https://www.w3.org/TR/MathML3/chapter7.html 38 | function parseLeaf(node) { 39 | let r = ''; 40 | const nodeName = NodeTool.getNodeName(node); 41 | switch(nodeName){ 42 | case 'mi': r = parseElementMi(node); 43 | break; 44 | case 'mn': r = parseElementMn(node); 45 | break; 46 | case 'mo': r = parseOperator(node); 47 | break; 48 | case 'ms': r = parseElementMs(node); 49 | break; 50 | case 'mtext': r = parseElementMtext(node); 51 | break; 52 | case 'mglyph': r = parseElementMglyph(node); 53 | break; 54 | case 'mprescripts': r = ''; 55 | break; 56 | case 'mspace': r = parseElementMspace(node); 57 | case 'none': r = '\\:'; 58 | //TODO other usecase of 'none' ? 59 | break; 60 | default: r = escapeSpecialChars(NodeTool.getNodeText(node).trim()); 61 | break; 62 | } 63 | return r; 64 | } 65 | 66 | // operator token, mathematical operators 67 | function parseOperator(node) { 68 | let it = NodeTool.getNodeText(node).trim(); 69 | it = MathSymbol.parseOperator(it); 70 | return escapeSpecialChars(it); 71 | } 72 | 73 | // Math identifier 74 | function parseElementMi(node){ 75 | let it = NodeTool.getNodeText(node).trim(); 76 | it = MathSymbol.parseIdentifier(it); 77 | return escapeSpecialChars(it); 78 | } 79 | 80 | // Math Number 81 | function parseElementMn(node){ 82 | let it = NodeTool.getNodeText(node).trim(); 83 | return escapeSpecialChars(it); 84 | } 85 | 86 | // Math String 87 | function parseElementMs(node){ 88 | const content = NodeTool.getNodeText(node).trimRight(); 89 | const it = escapeSpecialChars(content); 90 | return ['"', it, '"'].join(''); 91 | } 92 | 93 | // Math Text 94 | function parseElementMtext(node){ 95 | const content = NodeTool.getNodeText(node) 96 | const it = escapeSpecialChars(content); 97 | return `\\text{${it}}`; 98 | } 99 | 100 | // Math glyph (image) 101 | function parseElementMglyph(node){ 102 | const it = ['"', NodeTool.getAttr(node, 'alt', ''), '"'].join(''); 103 | return escapeSpecialChars(it); 104 | } 105 | 106 | // TODO need or not 107 | function parseElementMspace(node){ 108 | return ''; 109 | } 110 | 111 | function escapeSpecialChars(text) { 112 | const specialChars = /\$|%|_|&|#|\{|\}/g; 113 | text = text.replace(specialChars, char => `\\${ char }`); 114 | return text; 115 | } 116 | 117 | 118 | function parseContainer(node, children) { 119 | const render = getRender(node); 120 | if(render){ 121 | return render(node, children); 122 | } else { 123 | throw new Error(`Couldn't get render function for container node: ${NodeTool.getNodeName(node)}`); 124 | } 125 | } 126 | 127 | function renderChildren(children) { 128 | const parts = []; 129 | let lefts = []; 130 | Array.prototype.forEach.call(children, (node) => { 131 | if(NodeTool.getNodeName(node) === 'mo'){ 132 | const op = NodeTool.getNodeText(node).trim(); 133 | if(Brackets.contains(op)){ 134 | let stretchy = NodeTool.getAttr(node, 'stretchy', 'true'); 135 | stretchy = ['', 'true'].indexOf(stretchy) > -1; 136 | // 操作符是括號 137 | if(Brackets.isRight(op)){ 138 | const nearLeft = lefts[lefts.length - 1]; 139 | if(nearLeft){ 140 | if(Brackets.isPair(nearLeft, op)){ 141 | parts.push(Brackets.parseRight(op, stretchy)); 142 | lefts.pop(); 143 | } else { 144 | // some brackets left side is same as right side. 145 | if(Brackets.isLeft(op)) { 146 | parts.push(Brackets.parseLeft(op, stretchy)); 147 | lefts.push(op); 148 | } else { 149 | console.error("bracket not match"); 150 | } 151 | } 152 | }else{ 153 | // some brackets left side is same as right side. 154 | if(Brackets.isLeft(op)) { 155 | parts.push(Brackets.parseLeft(op, stretchy)); 156 | lefts.push(op); 157 | }else{ 158 | console.error("bracket not match") 159 | } 160 | } 161 | } else { 162 | parts.push(Brackets.parseLeft(op, stretchy)); 163 | lefts.push(op) 164 | } 165 | } else { 166 | parts.push(parseOperator(node)); 167 | } 168 | } else { 169 | parts.push(parse(node)); 170 | } 171 | }); 172 | // 這裏非常不嚴謹 173 | if(lefts.length > 0){ 174 | for(let i=0; i < lefts.length; i++){ 175 | parts.push("\\right."); 176 | } 177 | } 178 | lefts = undefined; 179 | return parts; 180 | } 181 | 182 | 183 | function getRender(node) { 184 | let render = undefined; 185 | const nodeName = NodeTool.getNodeName(node); 186 | switch(nodeName){ 187 | case 'msub': 188 | render = getRender_default("@1_{@2}"); 189 | break; 190 | case 'msup': 191 | render = getRender_default("@1^{@2}"); 192 | break; 193 | case 'msubsup': 194 | render = getRender_default("@1_{@2}^{@3}"); 195 | break; 196 | case 'mover': 197 | render = renderMover; 198 | break; 199 | case 'munder': 200 | render = renderMunder; 201 | break; 202 | case 'munderover': 203 | render = getRender_default("@1\\limits_{@2}^{@3}"); 204 | break; 205 | case 'mmultiscripts': 206 | render = renderMmultiscripts; 207 | break; 208 | case 'mroot': 209 | render = getRender_default("\\sqrt[@2]{@1}"); 210 | break; 211 | case 'msqrt': 212 | render = getRender_joinSeparator("\\sqrt{@content}"); 213 | break; 214 | case 'mtable': 215 | render = renderTable; 216 | break; 217 | case 'mtr': 218 | render = getRender_joinSeparator("@content \\\\ ", ' & '); 219 | break; 220 | case 'mtd': 221 | render = getRender_joinSeparator("@content"); 222 | break; 223 | case 'mfrac': 224 | render = renderMfrac; 225 | break; 226 | case 'mfenced': 227 | render = renderMfenced; 228 | break; 229 | case 'mi': 230 | case 'mn': 231 | case 'mo': 232 | case 'ms': 233 | case 'mtext': 234 | // they may contains 235 | render = getRender_joinSeparator("@content"); 236 | break; 237 | case 'mphantom': 238 | render = renderMphantom; 239 | break; 240 | default: 241 | // math, mstyle, mrow 242 | render = getRender_joinSeparator("@content"); 243 | break; 244 | } 245 | return render; 246 | } 247 | 248 | function renderTable(node, children) { 249 | const template = "\\begin{matrix} @content \\end{matrix}"; 250 | const render = getRender_joinSeparator(template); 251 | return render(node, children); 252 | } 253 | 254 | function renderMfrac(node, children){ 255 | const [linethickness, bevelled] = [ 256 | NodeTool.getAttr(node, 'linethickness', 'medium'), 257 | NodeTool.getAttr(node, 'bevelled', 'false') 258 | ] 259 | 260 | let render = null; 261 | if(bevelled === 'true') { 262 | render = getRender_default("{}^{@1}/_{@2}"); 263 | } else if(['0', '0px'].indexOf(linethickness) > -1) { 264 | const [prevNode, nextNode] = [ 265 | NodeTool.getPrevNode(node), 266 | NodeTool.getNextNode(node) 267 | ]; 268 | if((prevNode && NodeTool.getNodeText(prevNode).trim() === '(') && 269 | (nextNode && NodeTool.getNodeText(nextNode).trim() === ')') 270 | ) { 271 | render = getRender_default("\\DELETE_BRACKET_L\\binom{@1}{@2}\\DELETE_BRACKET_R"); 272 | } else { 273 | render = getRender_default("{}_{@2}^{@1}"); 274 | } 275 | } else { 276 | render = getRender_default("\\frac{@1}{@2}"); 277 | } 278 | return render(node, children); 279 | } 280 | 281 | function renderMfenced(node, children){ 282 | const [open, close, separatorsStr] = [ 283 | NodeTool.getAttr(node, 'open', '('), 284 | NodeTool.getAttr(node, 'close', ')'), 285 | NodeTool.getAttr(node, 'separators', ',') 286 | ]; 287 | const [left, right] = [ 288 | Brackets.parseLeft(open), 289 | Brackets.parseRight(close) 290 | ]; 291 | 292 | const separators = separatorsStr.split('').filter((c) => c.trim().length === 1); 293 | const template = `${left}@content${right}`; 294 | const render = getRender_joinSeparators(template, separators); 295 | return render(node, children); 296 | } 297 | 298 | function renderMmultiscripts(node, children) { 299 | if(children.length === 0) { return '' } 300 | let sepIndex = -1; 301 | let mprescriptsNode = null; 302 | Array.prototype.forEach.call(children, (node) => { 303 | if(NodeTool.getNodeName(node) === 'mprescripts'){ 304 | mprescriptsNode = node; 305 | } 306 | }); 307 | if(mprescriptsNode) { 308 | sepIndex = Array.prototype.indexOf.call(children, mprescriptsNode); 309 | } 310 | const parts = renderChildren(children); 311 | 312 | const splitArray = (arr, index) => { 313 | return [arr.slice(0, index), arr.slice(index + 1, arr.length)] 314 | } 315 | const renderScripts = (items) => { 316 | if(items.length > 0) { 317 | const subs = []; 318 | const sups = []; 319 | items.forEach((item, index) => { 320 | // one render as sub script, one as super script 321 | if((index + 1) % 2 === 0){ 322 | sups.push(item); 323 | } else { 324 | subs.push(item); 325 | } 326 | }); 327 | return [ 328 | (subs.length > 0 ? `_{${subs.join(' ')}}` : ''), 329 | (sups.length > 0 ? `^{${sups.join(' ')}}` : '') 330 | ].join(''); 331 | } else { 332 | return ''; 333 | } 334 | } 335 | const base = parts.shift(); 336 | let prevScripts = []; 337 | let backScripts = []; 338 | if(sepIndex === -1){ 339 | backScripts = parts; 340 | } else { 341 | [backScripts, prevScripts] = splitArray(parts, sepIndex - 1) 342 | } 343 | return [renderScripts(prevScripts), base, renderScripts(backScripts)].join(''); 344 | } 345 | 346 | function renderMover(node, children){ 347 | const nodes = flattenNodeTreeByNodeName(node, 'mover'); 348 | let result = undefined; 349 | for(let i = 0; i < nodes.length - 1; i++) { 350 | if(!result){ result = parse(nodes[i]) } 351 | const over = parse(nodes[i + 1]); 352 | const template = getMatchValueByChar({ 353 | decimals: MathSymbol.overScript.decimals, 354 | values: MathSymbol.overScript.templates, 355 | judgeChar: over, 356 | defaultValue: "@1\\limits^{@2}" 357 | }) 358 | result = renderTemplate(template.replace("@v", "@1"), [result, over]); 359 | } 360 | return result; 361 | } 362 | 363 | function renderMunder(node, children){ 364 | const nodes = flattenNodeTreeByNodeName(node, 'munder'); 365 | let result = undefined; 366 | for(let i = 0; i < nodes.length - 1; i++) { 367 | if(!result){ result = parse(nodes[i]) } 368 | const under = parse(nodes[i + 1]); 369 | const template = getMatchValueByChar({ 370 | decimals: MathSymbol.underScript.decimals, 371 | values: MathSymbol.underScript.templates, 372 | judgeChar: under, 373 | defaultValue: "@1\\limits_{@2}" 374 | }) 375 | result = renderTemplate(template.replace("@v", "@1"), [result, under]); 376 | } 377 | return result; 378 | } 379 | 380 | function flattenNodeTreeByNodeName(root, nodeName) { 381 | let result = []; 382 | const children = NodeTool.getChildren(root); 383 | Array.prototype.forEach.call(children, (node) => { 384 | if (NodeTool.getNodeName(node) === nodeName) { 385 | result = result.concat(flattenNodeTreeByNodeName(node, nodeName, result)); 386 | } else { 387 | result.push(node); 388 | } 389 | }); 390 | return result; 391 | } 392 | 393 | 394 | function getMatchValueByChar(params) { 395 | const {decimals, values, judgeChar, defaultValue=null} = params; 396 | if (judgeChar && judgeChar.length === 1) { 397 | const index = decimals.indexOf(judgeChar.charCodeAt(0)); 398 | if (index > -1) { 399 | return values[index]; 400 | } 401 | } 402 | return defaultValue; 403 | } 404 | 405 | // https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mphantom 406 | // FIXME :) 407 | function renderMphantom(node, children) { 408 | return ''; 409 | } 410 | 411 | 412 | 413 | function getRender_default(template) { 414 | return function(node, children) { 415 | const parts = renderChildren(children); 416 | return renderTemplate(template, parts) 417 | } 418 | } 419 | 420 | function renderTemplate(template, values) { 421 | return template.replace(/\@\d+/g, (m) => { 422 | const idx = parseInt(m.substring(1, m.length)) - 1; 423 | return values[idx]; 424 | }); 425 | } 426 | 427 | function getRender_joinSeparator(template, separator = '') { 428 | return function(node, children) { 429 | const parts = renderChildren(children); 430 | return template.replace("@content", parts.join(separator)); 431 | } 432 | } 433 | 434 | function getRender_joinSeparators(template, separators) { 435 | return function(node, children) { 436 | const parts = renderChildren(children); 437 | let content = ''; 438 | if(separators.length === 0){ 439 | content = parts.join(''); 440 | } else { 441 | content = parts.reduce((accumulator, part, index) => { 442 | accumulator += part; 443 | if(index < parts.length - 1){ 444 | accumulator += (separators[index] || separators[separators.length - 1]); 445 | } 446 | return accumulator; 447 | }, ''); 448 | } 449 | return template.replace("@content", content); 450 | } 451 | } 452 | 453 | export default {convert: convert}; 454 | --------------------------------------------------------------------------------