├── 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 ``;
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 = '';
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 |
--------------------------------------------------------------------------------