├── .husky └── pre-commit ├── .lintstagedrc ├── test ├── fixtures │ ├── hub2docx-lib │ │ ├── mathvariant_bold.mml │ │ ├── mathvariant_italic.mml │ │ ├── mathvariant_bolditalic.mml │ │ ├── mroot_exponent_3.mml │ │ ├── mroot_empty_exponent.mml │ │ ├── copyright.txt │ │ ├── munder.mml │ │ ├── munder_underscore.mml │ │ ├── munder_with_following_sibling.mml │ │ ├── mroot_empty_exponent.omml │ │ ├── mathvariant_italic.omml │ │ ├── mathvariant_bold.omml │ │ ├── msub_sum.mml │ │ ├── munder_sum.mml │ │ ├── mathvariant_bolditalic.omml │ │ ├── mroot_exponent_3.omml │ │ ├── munder.omml │ │ ├── msubsup_sum.mml │ │ ├── munderover_sum.mml │ │ ├── munder_with_following_sibling.omml │ │ ├── munder_underscore.omml │ │ ├── msub_sum.omml │ │ ├── munder_sum.omml │ │ ├── msubsup_sum.omml │ │ ├── munderover_sum.omml │ │ └── LICENSE │ ├── joe_java │ │ ├── copyright.txt │ │ ├── pythagorean.mml │ │ ├── quadratic_formula.mml │ │ ├── demorgan.mml │ │ ├── boolean_algebra.mml │ │ ├── axiom_of_power.mml │ │ ├── complex_number.mml │ │ ├── sophomores_dream.mml │ │ ├── divergence.mml │ │ ├── binomial_coefficient.mml │ │ ├── pythagorean2.mml │ │ ├── matrix.mml │ │ └── LICENSE │ └── mathml2omml │ │ ├── ms.mml │ │ ├── groupchr.mml │ │ ├── single_line.mml │ │ ├── single_line_with_symbol.mml │ │ ├── single_line_with_styling.mml │ │ ├── single_line_with_glyph.mml │ │ ├── mstyle.mml │ │ ├── simple.mml │ │ └── menclose.mml ├── basic.test.js └── __snapshots__ │ └── basic.test.js.snap ├── src ├── ooml │ ├── index.js │ ├── scriptlevel.js │ └── nary.js ├── mathml │ ├── mstyle.js │ ├── mglyph.js │ ├── mrow.js │ ├── mspace.js │ ├── math.js │ ├── index.js │ ├── msqrt.js │ ├── text.js │ ├── mroot.js │ ├── mfrac.js │ ├── msup.js │ ├── msub.js │ ├── msubsup.js │ ├── table.js │ ├── munderover.js │ ├── mmultiscripts.js │ ├── text_style.js │ ├── menclose.js │ ├── text_container.js │ └── under_or_over.js ├── parse-stringify │ ├── index.js │ ├── stringify.js │ ├── parse-tag.js │ └── parse.js ├── helpers.js ├── index.js ├── index.d.ts └── walker.js ├── rollup.config.js ├── .github └── workflows │ └── test.yml ├── biome.json ├── package.json ├── .gitignore ├── README.md └── LICENSE /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm test 2 | lint-staged 3 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.js": ["biome check", "biome format --write", "git add"] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mathvariant_bold.mml: -------------------------------------------------------------------------------- 1 | 2 | ß 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mathvariant_italic.mml: -------------------------------------------------------------------------------- 1 | 2 | ß 3 | 4 | -------------------------------------------------------------------------------- /src/ooml/index.js: -------------------------------------------------------------------------------- 1 | export { getNary, getNaryTarget } from './nary.js' 2 | export { addScriptlevel } from './scriptlevel.js' 3 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mathvariant_bolditalic.mml: -------------------------------------------------------------------------------- 1 | 2 | ß 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mroot_exponent_3.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 3 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mroot_empty_exponent.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/mathml/mstyle.js: -------------------------------------------------------------------------------- 1 | export function mstyle(element, targetParent, previousSibling, nextSibling, ancestors) { 2 | // Ignore as default behavior 3 | return targetParent 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/copyright.txt: -------------------------------------------------------------------------------- 1 | The following applies to all files in this folder: 2 | 3 | Copyright 2020 Josephus Javawaski (Joe Java) 4 | Source: http://eyeasme.com/Joe/MathML/MathML_browser_test.html 5 | License: GFDLv1.3 (see LICENSE) 6 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/copyright.txt: -------------------------------------------------------------------------------- 1 | The following applies to all files in this folder: 2 | 3 | Copyright (c) 2015--2022, transpect.io 4 | Source: https://github.com/transpect/hub2docx-lib 5 | License: BSD 2-Clause "Simplified" License (see LICENSE) 6 | -------------------------------------------------------------------------------- /test/fixtures/mathml2omml/ms.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2 5 | abc 6 | text 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/mathml2omml/groupchr.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | a 6 | 7 | b 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/parse-stringify/index.js: -------------------------------------------------------------------------------- 1 | // Copied and adjusted from html-parse-stringify (MIT) https://github.com/HenrikJoreteg/html-parse-stringify/commit/ce46022f537ef9b050fac592f9fcc30bf838e5ba 2 | 3 | export { parse } from './parse' 4 | export { stringifyDoc } from './stringify' 5 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munder.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | a 5 | 6 | 7 | b 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/mathml2omml/single_line.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2 5 | + 6 | 2 7 | = 8 | 4 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/mathml2omml/single_line_with_symbol.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2 5 | + 6 | 7 | = 8 | 4 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munder_underscore.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | U 6 | 7 | 8 | _ 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/mathml/mglyph.js: -------------------------------------------------------------------------------- 1 | export function mglyph(element, targetParent, previousSibling, nextSibling, ancestors) { 2 | // No support in omml. Output alt text. 3 | if (element.attribs?.alt) { 4 | targetParent.children.push({ 5 | type: 'text', 6 | data: element.attribs.alt 7 | }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munder_with_following_sibling.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | a 5 | 6 | 7 | b 8 | 9 | 10 | c 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/mathml2omml/single_line_with_styling.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | x 5 | + 6 | 2 7 | = 8 | 4 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mroot_empty_exponent.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/mathml/mrow.js: -------------------------------------------------------------------------------- 1 | export function mrow(element, targetParent, previousSibling, nextSibling, ancestors) { 2 | if (previousSibling.isNary) { 3 | const targetSibling = targetParent.children[targetParent.children.length - 1] 4 | return targetSibling.children[targetSibling.children.length - 1] 5 | } 6 | // Ignore as default behavior 7 | return targetParent 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/mathml2omml/single_line_with_glyph.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | + 8 | 2 9 | = 10 | 4 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mathvariant_italic.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ß 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mathvariant_bold.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ß 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/msub_sum.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | j 7 | = 8 | 1 9 | 10 | 11 | 12 | j 13 | 14 | 15 | x 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munder_sum.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | j 7 | = 8 | 1 9 | 10 | 11 | 12 | j 13 | 14 | 15 | x 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mathvariant_bolditalic.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ß 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/mroot_exponent_3.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munder.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | a 6 | 7 | 8 | 9 | 10 | b 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export function getTextContent(node, trim = true) { 2 | let returnString = '' 3 | if (node.type === 'text') { 4 | let text = node.data.replace(/[\u2062]|[\u200B]/g, '') 5 | if (trim) { 6 | text = text.trim() 7 | } 8 | returnString += text 9 | } else if (node.children) { 10 | node.children.forEach((subNode) => { 11 | returnString += getTextContent(subNode, trim) 12 | }) 13 | } 14 | return returnString 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/pythagorean.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | a 6 | 2 7 | 8 | + 9 | 10 | b 11 | 2 12 | 13 | = 14 | 15 | c 16 | 2 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/fixtures/mathml2omml/mstyle.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | a 8 | 2 9 | 10 | 11 | + 12 | 2 13 | = 14 | 4 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/msubsup_sum.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | j 7 | = 8 | 1 9 | 10 | 11 | n 12 | 13 | 14 | 15 | j 16 | 17 | 18 | x 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munderover_sum.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | j 7 | = 8 | 1 9 | 10 | 11 | n 12 | 13 | 14 | 15 | j 16 | 17 | 18 | x 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/fixtures/mathml2omml/simple.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ( 6 | 7 | 4 8 | 9 | x 10 | 11 | 6 12 | 13 | n 14 | 15 | ) 16 | 17 | 34 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/mathml/mspace.js: -------------------------------------------------------------------------------- 1 | export function mspace(element, targetParent, previousSibling, nextSibling, ancestors) { 2 | targetParent.children.push({ 3 | name: 'm:r', 4 | type: 'tag', 5 | attribs: {}, 6 | children: [ 7 | { 8 | name: 'm:t', 9 | type: 'tag', 10 | attribs: { 11 | 'xml:space': 'preserve' 12 | }, 13 | children: [ 14 | { 15 | type: 'text', 16 | data: ' ' 17 | } 18 | ] 19 | } 20 | ] 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munder_with_following_sibling.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | a 6 | 7 | 8 | 9 | 10 | b 11 | 12 | 13 | 14 | 15 | c 16 | 17 | 18 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve' 2 | 3 | const onwarn = (warning) => { 4 | // Silence circular dependency warning for moment package 5 | if (warning.code === 'CIRCULAR_DEPENDENCY') { 6 | return 7 | } 8 | 9 | console.warn(`(!) ${warning.message}`) 10 | } 11 | 12 | export default { 13 | input: 'src/index.js', 14 | output: [ 15 | { 16 | file: 'dist/index.js', 17 | format: 'module', 18 | sourcemap: true 19 | } 20 | ], 21 | plugins: [nodeResolve()], 22 | onwarn 23 | } 24 | -------------------------------------------------------------------------------- /src/ooml/scriptlevel.js: -------------------------------------------------------------------------------- 1 | export function addScriptlevel(target, ancestors) { 2 | const scriptlevel = ancestors.find((ancestor) => ancestor.attribs?.scriptlevel)?.attribs 3 | ?.scriptlevel 4 | if (['0', '1', '2'].includes(scriptlevel)) { 5 | target.children.unshift({ 6 | type: 'tag', 7 | name: 'm:argPr', 8 | attribs: {}, 9 | children: [ 10 | { 11 | type: 'tag', 12 | name: 'm:scrLvl', 13 | attribs: { 'm:val': scriptlevel }, 14 | children: [] 15 | } 16 | ] 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Test 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: "22.x" 20 | - run: npm install 21 | - run: npm test 22 | - run: npm run lint 23 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munder_underscore.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | U 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/mathml/math.js: -------------------------------------------------------------------------------- 1 | export function math(element, targetParent, previousSibling, nextSibling, ancestors) { 2 | targetParent.name = 'm:oMath' 3 | targetParent.attribs = { 4 | 'xmlns:m': 'http://schemas.openxmlformats.org/officeDocument/2006/math', 5 | 'xmlns:w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' 6 | } 7 | targetParent.type = 'tag' 8 | targetParent.children = [] 9 | return targetParent 10 | } 11 | 12 | export function semantics(element, targetParent, previousSibling, nextSibling, ancestors) { 13 | // Ignore as default behavior 14 | return targetParent 15 | } 16 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "style": { 11 | "noParameterAssign": "off" 12 | }, 13 | "complexity": { 14 | "noForEach": "off" 15 | } 16 | } 17 | }, 18 | "formatter": { 19 | "enabled": true, 20 | "indentStyle": "space", 21 | "indentWidth": 2, 22 | "lineWidth": 100 23 | }, 24 | "javascript": { 25 | "formatter": { 26 | "quoteStyle": "single", 27 | "semicolons": "asNeeded", 28 | "trailingCommas": "none" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/msub_sum.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | j=1 13 | 14 | 15 | 16 | 17 | 18 | j 19 | 20 | 21 | 22 | 23 | x 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munder_sum.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | j=1 13 | 14 | 15 | 16 | 17 | 18 | j 19 | 20 | 21 | 22 | 23 | x 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { parse, stringifyDoc } from './parse-stringify' 2 | import { walker } from './walker.js' 3 | 4 | class MML2OMML { 5 | constructor(mmlString, options = {}) { 6 | this.inString = mmlString 7 | this.inXML = parse(mmlString, options) 8 | this.outXML = false 9 | this.outString = false 10 | } 11 | 12 | run() { 13 | const outXML = {} 14 | walker({ children: this.inXML, type: 'root' }, outXML) 15 | this.outXML = outXML 16 | } 17 | 18 | getResult() { 19 | this.outString = stringifyDoc([this.outXML]) 20 | return this.outString 21 | } 22 | } 23 | 24 | export const mml2omml = (mmlString, options) => { 25 | const converter = new MML2OMML(mmlString, options) 26 | converter.run() 27 | return converter.getResult() 28 | } 29 | -------------------------------------------------------------------------------- /src/mathml/index.js: -------------------------------------------------------------------------------- 1 | export { math, semantics } from './math.js' 2 | export { menclose } from './menclose.js' 3 | export { mfrac } from './mfrac.js' 4 | export { mglyph } from './mglyph.js' 5 | export { mmultiscripts } from './mmultiscripts.js' 6 | export { mrow } from './mrow.js' 7 | export { mspace } from './mspace.js' 8 | export { msqrt } from './msqrt.js' 9 | export { mstyle } from './mstyle.js' 10 | export { msub } from './msub.js' 11 | export { msubsup } from './msubsup.js' 12 | export { msup } from './msup.js' 13 | export { mtable, mtd, mtr } from './table.js' 14 | export { munderover } from './munderover.js' 15 | export { mtext, mi, mn, ms, mo } from './text_container.js' 16 | export { munder, mover } from './under_or_over.js' 17 | export { mroot } from './mroot.js' 18 | export { text } from './text.js' 19 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/msubsup_sum.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | j=1 13 | 14 | 15 | 16 | 17 | n 18 | 19 | 20 | 21 | 22 | j 23 | 24 | 25 | 26 | 27 | x 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/munderover_sum.omml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | j=1 13 | 14 | 15 | 16 | 17 | n 18 | 19 | 20 | 21 | 22 | j 23 | 24 | 25 | 26 | 27 | x 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/quadratic_formula.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | x 5 | = 6 | 7 | 8 | 9 | b 10 | ± 11 | 12 | 13 | b 14 | 2 15 | 16 | 17 | 4 18 | 19 | a 20 | 21 | c 22 | 23 | 24 | 25 | 2 26 | 27 | a 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/mathml/msqrt.js: -------------------------------------------------------------------------------- 1 | export function msqrt(element, targetParent, previousSibling, nextSibling, ancestors) { 2 | const targetElement = { 3 | name: 'm:e', 4 | type: 'tag', 5 | attribs: {}, 6 | children: [] 7 | } 8 | targetParent.children.push({ 9 | name: 'm:rad', 10 | type: 'tag', 11 | attribs: {}, 12 | children: [ 13 | { 14 | name: 'm:radPr', 15 | type: 'tag', 16 | attribs: {}, 17 | children: [ 18 | { 19 | name: 'm:degHide', 20 | type: 'tag', 21 | attribs: { 22 | 'm:val': 'on' 23 | }, 24 | children: [] 25 | } 26 | ] 27 | }, 28 | { 29 | name: 'm:deg', 30 | type: 'tag', 31 | attribs: {}, 32 | children: [] 33 | }, 34 | targetElement 35 | ] 36 | }) 37 | return targetElement 38 | } 39 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/demorgan.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logic:  5 | ¬ 6 | 7 | ( 8 | 9 | p 10 | 11 | q 12 | 13 | ) 14 | 15 | 16 | 17 | ( 18 | 19 | ¬ 20 | p 21 | 22 | ) 23 | 24 | 25 | 26 | ( 27 | 28 | ¬ 29 | q 30 | 31 | ) 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/mathml/text.js: -------------------------------------------------------------------------------- 1 | export function text(element, targetParent, previousSibling, nextSibling, ancestors) { 2 | let text = element.data.replace(/[\u2062]|[\u200B]/g, '') 3 | if (ancestors.find((element) => ['mi', 'mn', 'mo'].includes(element.name))) { 4 | text = text.replace(/\s/g, '') 5 | } else { 6 | const ms = ancestors.find((element) => element.name === 'ms') 7 | if (ms) { 8 | text = (ms.attribs?.lquote || '"') + text + (ms.attribs?.rquote || '"') 9 | } 10 | } 11 | if (text.length) { 12 | if ( 13 | targetParent.children.length && 14 | targetParent.children[targetParent.children.length - 1].type === 'text' 15 | ) { 16 | targetParent.children[targetParent.children.length - 1].data += text 17 | } else { 18 | targetParent.children.push({ 19 | type: 'text', 20 | data: text 21 | }) 22 | } 23 | } 24 | return targetParent 25 | } 26 | -------------------------------------------------------------------------------- /src/parse-stringify/stringify.js: -------------------------------------------------------------------------------- 1 | function attrString(attribs) { 2 | const buff = [] 3 | for (const key in attribs) { 4 | buff.push(`${key}="${attribs[key]}"`) 5 | } 6 | if (!buff.length) { 7 | return '' 8 | } 9 | return ` ${buff.join(' ')}` 10 | } 11 | 12 | function stringify(buff, doc) { 13 | switch (doc.type) { 14 | case 'text': 15 | return buff + doc.data 16 | case 'tag': { 17 | const voidElement = 18 | doc.voidElement || (!doc.children.length && doc.attribs['xml:space'] !== 'preserve') 19 | buff += `<${doc.name}${doc.attribs ? attrString(doc.attribs) : ''}${voidElement ? '/>' : '>'}` 20 | if (voidElement) { 21 | return buff 22 | } 23 | return `${buff + doc.children.reduce(stringify, '')}` 24 | } 25 | case 'comment': 26 | buff += `` 27 | return buff 28 | } 29 | } 30 | 31 | export function stringifyDoc(doc) { 32 | return doc.reduce((token, rootEl) => token + stringify('', rootEl), '') 33 | } 34 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface MML2OMMLOptions { 2 | /** 3 | * Whether to disable XML decoding of input 4 | */ 5 | disableDecode?: boolean 6 | } 7 | 8 | /** 9 | * Convert MathML to Office Open XML Math (OMML) format 10 | * 11 | * @param mmlString - MathML string to convert 12 | * @param options - Optional configuration options 13 | * @returns OMML string 14 | */ 15 | export function mml2omml(mmlString: string, options?: MML2OMMLOptions): string 16 | 17 | /** 18 | * MML2OMML class for converting MathML to OMML 19 | */ 20 | export class MML2OMML { 21 | /** 22 | * Construct a new MML2OMML converter 23 | * 24 | * @param mmlString - MathML string to convert 25 | * @param options - Optional configuration options 26 | */ 27 | constructor(mmlString: string, options?: MML2OMMLOptions) 28 | 29 | /** 30 | * Run the conversion process 31 | */ 32 | run(): void 33 | 34 | /** 35 | * Get the resulting OMML as a string 36 | * 37 | * @returns OMML string 38 | */ 39 | getResult(): string 40 | } 41 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/boolean_algebra.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Boolean algebra:  5 | 6 | 7 | 8 | 9 | 10 | i 11 | = 12 | 1 13 | 14 | n 15 | 16 | 17 | A 18 | i 19 | 20 | 21 | ¯ 22 | 23 | = 24 | 25 | 26 | 27 | i 28 | = 29 | 1 30 | 31 | n 32 | 33 | 34 | 35 | A 36 | i 37 | 38 | ¯ 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/axiom_of_power.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A 6 | 7 | P 8 | 9 | B 10 | 11 | 12 | [ 13 | 14 | B 15 | 16 | P 17 | 18 | 19 | C 20 | 21 | 22 | ( 23 | 24 | C 25 | 26 | B 27 | 28 | C 29 | 30 | A 31 | 32 | ) 33 | 34 | 35 | ] 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/complex_number.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | c 5 | = 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | a 14 | 15 | 16 | 17 | 18 | real 19 | 20 | + 21 | 22 | 23 | 24 | 25 | b 26 | 27 | 28 | 29 | 30 | 31 | 32 | imaginary 33 | 34 | 35 | 36 | 37 | complex number 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/sophomores_dream.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 0 7 | 1 8 | 9 | 10 | x 11 | x 12 | 13 | 14 | 15 | x 16 | = 17 | 18 | 19 | 20 | n 21 | = 22 | 1 23 | 24 | 25 | 26 | 27 | 28 | ( 29 | 30 | 31 | 1 32 | 33 | ) 34 | 35 | 36 | n 37 | + 38 | 1 39 | 40 | 41 | 42 | 43 | n 44 | 45 | 46 | n 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/divergence.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | · 6 | 7 | v 8 | 9 | 10 | = 11 | 12 | 13 | 14 | 15 | v 16 | x 17 | 18 | 19 | 20 | 21 | x 22 | 23 | 24 | + 25 | 26 | 27 | 28 | 29 | v 30 | y 31 | 32 | 33 | 34 | 35 | y 36 | 37 | 38 | + 39 | 40 | 41 | 42 | 43 | v 44 | z 45 | 46 | 47 | 48 | 49 | z 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mathml2omml", 3 | "version": "0.5.0", 4 | "description": "a MathML to OMML converter ", 5 | "main": "./dist/index.js", 6 | "type": "module", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "test": "node --experimental-vm-modules ./node_modules/.bin/jest", 10 | "lint": "biome check src/ test/", 11 | "format": "biome format --write src/ test/", 12 | "transpile": "rollup -c", 13 | "copy_types": "cp src/index.d.ts dist/", 14 | "prepublishOnly": "npm run lint && npm run transpile && npm run copy_types", 15 | "prepare": "husky" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/fiduswriter/mathml2omml.git" 20 | }, 21 | "keywords": [ 22 | "mml", 23 | "mathml", 24 | "omml" 25 | ], 26 | "author": "Johannes Wilm", 27 | "license": "LGPL-3.0-or-later", 28 | "bugs": { 29 | "url": "https://github.com/fiduswriter/mathml2omml/issues" 30 | }, 31 | "homepage": "https://github.com/fiduswriter/mathml2omml#readme", 32 | "files": [ 33 | "dist" 34 | ], 35 | "devDependencies": { 36 | "@biomejs/biome": "1.9.4", 37 | "@rollup/plugin-node-resolve": "^16.0.1", 38 | "entities": "^6.0.0", 39 | "husky": "^9.1.7", 40 | "jest": "^29.7.0", 41 | "lint-staged": "^15.5.0", 42 | "rollup": "^4.35.0", 43 | "xml-formatter": "^3.6.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/fixtures/hub2docx-lib/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015--2022, transpect.io 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /src/mathml/mroot.js: -------------------------------------------------------------------------------- 1 | import { getTextContent } from '../helpers.js' 2 | import { walker } from '../walker.js' 3 | 4 | export function mroot(element, targetParent, previousSibling, nextSibling, ancestors) { 5 | // Root 6 | if (element.children.length !== 2) { 7 | // treat as mrow 8 | return targetParent 9 | } 10 | ancestors = [...ancestors] 11 | ancestors.unshift(element) 12 | const base = element.children[0] 13 | const root = element.children[1] 14 | 15 | const baseTarget = { 16 | type: 'tag', 17 | name: 'm:e', 18 | attribs: {}, 19 | children: [] 20 | } 21 | walker(base, baseTarget, false, false, ancestors) 22 | 23 | const rootTarget = { 24 | type: 'tag', 25 | name: 'm:deg', 26 | attribs: {}, 27 | children: [] 28 | } 29 | walker(root, rootTarget, false, false, ancestors) 30 | 31 | const rootText = getTextContent(root) 32 | 33 | targetParent.children.push({ 34 | type: 'tag', 35 | name: 'm:rad', 36 | attribs: {}, 37 | children: [ 38 | { 39 | type: 'tag', 40 | name: 'm:radPr', 41 | attribs: {}, 42 | children: [ 43 | { 44 | type: 'tag', 45 | name: 'm:degHide', 46 | attribs: { 'm:val': rootText.length ? 'off' : 'on' }, 47 | children: [] 48 | } 49 | ] 50 | }, 51 | rootTarget, 52 | baseTarget 53 | ] 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/parse-stringify/parse-tag.js: -------------------------------------------------------------------------------- 1 | const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g 2 | 3 | export default function stringify(tag) { 4 | const res = { 5 | type: 'tag', 6 | name: '', 7 | voidElement: false, 8 | attribs: {}, 9 | children: [] 10 | } 11 | 12 | const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/) 13 | if (tagMatch) { 14 | res.name = tagMatch[1] 15 | if (tag.charAt(tag.length - 2) === '/') { 16 | res.voidElement = true 17 | } 18 | 19 | // handle comment tag 20 | if (res.name.startsWith('!--')) { 21 | const endIndex = tag.indexOf('-->') 22 | return { 23 | type: 'comment', 24 | comment: endIndex !== -1 ? tag.slice(4, endIndex) : '' 25 | } 26 | } 27 | } 28 | 29 | const reg = new RegExp(attrRE) 30 | let result = null 31 | for (;;) { 32 | result = reg.exec(tag) 33 | 34 | if (result === null) { 35 | break 36 | } 37 | 38 | if (!result[0].trim()) { 39 | continue 40 | } 41 | 42 | if (result[1]) { 43 | const attr = result[1].trim() 44 | let arr = [attr, ''] 45 | 46 | if (attr.indexOf('=') > -1) { 47 | arr = attr.split('=') 48 | } 49 | 50 | res.attribs[arr[0]] = arr[1] 51 | reg.lastIndex-- 52 | } else if (result[2]) { 53 | res.attribs[result[2]] = result[3].trim().substring(1, result[3].length - 1) 54 | } 55 | } 56 | 57 | return res 58 | } 59 | -------------------------------------------------------------------------------- /src/mathml/mfrac.js: -------------------------------------------------------------------------------- 1 | import { walker } from '../walker.js' 2 | 3 | export function mfrac(element, targetParent, previousSibling, nextSibling, ancestors) { 4 | if (element.children.length !== 2) { 5 | // treat as mrow 6 | return targetParent 7 | } 8 | 9 | const numerator = element.children[0] 10 | const denumerator = element.children[1] 11 | const numeratorTarget = { 12 | name: 'm:num', 13 | type: 'tag', 14 | attribs: {}, 15 | children: [] 16 | } 17 | const denumeratorTarget = { 18 | name: 'm:den', 19 | type: 'tag', 20 | attribs: {}, 21 | children: [] 22 | } 23 | ancestors = [...ancestors] 24 | ancestors.unshift(element) 25 | walker(numerator, numeratorTarget, false, false, ancestors) 26 | walker(denumerator, denumeratorTarget, false, false, ancestors) 27 | const fracType = element.attribs?.linethickness === '0' ? 'noBar' : 'bar' 28 | targetParent.children.push({ 29 | type: 'tag', 30 | name: 'm:f', 31 | attribs: {}, 32 | children: [ 33 | { 34 | type: 'tag', 35 | name: 'm:fPr', 36 | attribs: {}, 37 | children: [ 38 | { 39 | type: 'tag', 40 | name: 'm:type', 41 | attribs: { 42 | 'm:val': fracType 43 | }, 44 | children: [] 45 | } 46 | ] 47 | }, 48 | numeratorTarget, 49 | denumeratorTarget 50 | ] 51 | }) 52 | // Don't iterate over children in the usual way. 53 | } 54 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/binomial_coefficient.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | C 5 | 6 | ( 7 | n 8 | , 9 | k 10 | ) 11 | 12 | = 13 | 14 | C 15 | k 16 | n 17 | 18 | = 19 | 20 | C 21 | k 22 | 23 | 24 | n 25 | 26 | 27 | = 28 | 29 | ( 30 | 31 | n 32 | k 33 | 34 | ) 35 | 36 | = 37 | 38 | 39 | n 40 | ! 41 | 42 | 43 | k 44 | ! 45 | 46 | 47 | ( 48 | 49 | n 50 | 51 | k 52 | 53 | ) 54 | 55 | ! 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs' 2 | import { dirname, extname, join } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import format from 'xml-formatter' 5 | import { mml2omml } from '../src/index.js' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = dirname(__filename) 9 | const fixtures = join(__dirname, 'fixtures') 10 | 11 | function collectFiles(dirPath, filesList = []) { 12 | readdirSync(dirPath).forEach((file) => { 13 | if (statSync(`${dirPath}/${file}`).isDirectory()) { 14 | filesList = collectFiles(`${dirPath}/${file}`, filesList) 15 | } else { 16 | filesList.push(join(dirPath, '/', file)) 17 | } 18 | }) 19 | 20 | return filesList 21 | } 22 | 23 | const mathfiles = collectFiles(fixtures) 24 | const examples = [] 25 | 26 | for (const fixture of mathfiles) { 27 | if (extname(fixture) !== '.mml') { 28 | continue 29 | } 30 | const ofixture = `${fixture.slice(0, -4)}.omml` 31 | const mml = readFileSync(fixture, 'utf8') 32 | const omml = existsSync(ofixture) ? readFileSync(ofixture, 'utf8') : false 33 | examples.push({ fixture: fixture.slice(fixtures.length + 1), mml, omml }) 34 | } 35 | 36 | test.each(examples)('Can produce OOML from $fixture', ({ fixture, mml, omml }) => { 37 | const outOmml = format(mml2omml(mml)) 38 | if (omml) { 39 | expect(outOmml).toEqual(format(omml)) 40 | } else { 41 | expect(outOmml).toMatchSnapshot() 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # IDEs and editors 33 | .idea 34 | .project 35 | .classpath 36 | .c9/ 37 | *.launch 38 | .settings/ 39 | *.sublime-workspace 40 | 41 | # IDE - VSCode 42 | .vscode/* 43 | !.vscode/settings.json 44 | !.vscode/tasks.json 45 | !.vscode/launch.json 46 | !.vscode/extensions.json 47 | 48 | # misc 49 | .sass-cache 50 | connect.lock 51 | typings 52 | 53 | # Logs 54 | logs 55 | *.log 56 | npm-debug.log* 57 | yarn-debug.log* 58 | yarn-error.log* 59 | 60 | 61 | # Dependency directories 62 | node_modules/ 63 | jspm_packages/ 64 | 65 | # Optional npm cache directory 66 | .npm 67 | 68 | # Optional eslint cache 69 | .eslintcache 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variables file 81 | .env 82 | 83 | # next.js build output 84 | .next 85 | 86 | # Lerna 87 | lerna-debug.log 88 | 89 | # System Files 90 | .DS_Store 91 | Thumbs.db 92 | 93 | package-lock.json 94 | -------------------------------------------------------------------------------- /src/ooml/nary.js: -------------------------------------------------------------------------------- 1 | import { getTextContent } from '../helpers.js' 2 | 3 | const NARY_REGEXP = /^[\u220f-\u2211]|[\u2229-\u2233]|[\u22c0-\u22c3]$/ 4 | const GROW_REGEXP = /^\u220f|\u2211|[\u2229-\u222b]|\u222e|\u222f|\u2232|\u2233|[\u22c0-\u22c3]$/ 5 | 6 | export function getNary(node) { 7 | // Check if node contains only a nary operator. 8 | const text = getTextContent(node) 9 | if (NARY_REGEXP.test(text)) { 10 | return text 11 | } 12 | return false 13 | } 14 | 15 | export function getNaryTarget(naryChar, element, type, subHide = false, supHide = false) { 16 | const stretchy = element.attribs?.stretchy 17 | const grow = 18 | stretchy === 'true' ? '1' : stretchy === 'false' ? '0' : GROW_REGEXP.test(naryChar) ? '1' : '0' 19 | return { 20 | type: 'tag', 21 | name: 'm:nary', 22 | attribs: {}, 23 | children: [ 24 | { 25 | type: 'tag', 26 | name: 'm:naryPr', 27 | attribs: {}, 28 | children: [ 29 | { type: 'tag', name: 'm:chr', attribs: { 'm:val': naryChar }, children: [] }, 30 | { type: 'tag', name: 'm:limLoc', attribs: { 'm:val': type }, children: [] }, 31 | { type: 'tag', name: 'm:grow', attribs: { 'm:val': grow }, children: [] }, 32 | { 33 | type: 'tag', 34 | name: 'm:subHide', 35 | attribs: { 'm:val': subHide ? 'on' : 'off' }, 36 | children: [] 37 | }, 38 | { 39 | type: 'tag', 40 | name: 'm:supHide', 41 | attribs: { 'm:val': supHide ? 'on' : 'off' }, 42 | children: [] 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mathml2omml 2 | 3 | Convert MathML to the OOML format used in DOCX files without XSLT. 4 | 5 | ## Usage 6 | 7 | You can use it like this: 8 | 9 | ```js 10 | import {mml2omml} from "mathml2omml" 11 | 12 | const mml = ' 13 | 14 | 15 | 2 16 | + 17 | 2 18 | = 19 | 4 20 | 21 | 22 | ' 23 | 24 | const omml = mml2omml(mml) 25 | 26 | console.log(omml) 27 | 28 | > 29 | > 30 | > 2+2=4 31 | > 32 | > 33 | ``` 34 | 35 | ## TypeScript Support 36 | 37 | This library includes TypeScript definitions: 38 | 39 | ```ts 40 | import { mml2omml } from "mathml2omml"; 41 | 42 | // Simple usage 43 | const omml = mml2omml(mathmlString); 44 | 45 | // With options 46 | const omml = mml2omml(mathmlString, { disableDecode: true }); 47 | 48 | // Using the class directly 49 | import { MML2OMML } from "mathml2omml"; 50 | const converter = new MML2OMML(mathmlString); 51 | converter.run(); 52 | const result = converter.getResult(); 53 | ``` 54 | 55 | ## License 56 | 57 | LGPL v.3.0 or later. 58 | 59 | The xml parser/stringifier in the `parse-stringify`-folder is based upon [html-parse-stringify](https://github.com/henrikjoreteg/html-parse-stringify) (MIT). 60 | 61 | License of test files depends on source of files. 62 | 63 | Some test fixtures fall under other licenses/copyrights. See the LICENSE and copyright.txt files in the different test fixture folders. 64 | 65 | Remaining test fixtures fall under the same license and copyright as the source code. 66 | -------------------------------------------------------------------------------- /src/walker.js: -------------------------------------------------------------------------------- 1 | import * as mathmlHandlers from './mathml/index.js' 2 | import { addScriptlevel } from './ooml/index.js' 3 | 4 | export function walker( 5 | element, 6 | targetParent, 7 | previousSibling = false, 8 | nextSibling = false, 9 | ancestors = [] 10 | ) { 11 | if ( 12 | !previousSibling && 13 | ['m:deg', 'm:den', 'm:e', 'm:fName', 'm:lim', 'm:num', 'm:sub', 'm:sup'].includes( 14 | targetParent.name 15 | ) 16 | ) { 17 | // We are walking through the first element within one of the 18 | // elements where an might occur. The can specify 19 | // the scriptlevel, but it only makes sense if there is some content. 20 | // The fact that we are here means that there is at least one content item. 21 | // So we will check whether to add the m:rPr. 22 | // For possible parent types, see 23 | // https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.math.argumentproperties?view=openxml-2.8.1#remarks 24 | addScriptlevel(targetParent, ancestors) 25 | } 26 | let targetElement 27 | const nameOrType = element.name || element.type 28 | if (mathmlHandlers[nameOrType]) { 29 | targetElement = mathmlHandlers[nameOrType]( 30 | element, 31 | targetParent, 32 | previousSibling, 33 | nextSibling, 34 | ancestors 35 | ) 36 | } else { 37 | if (nameOrType && nameOrType !== 'root') { 38 | console.warn(`Type not supported: ${nameOrType}`) 39 | } 40 | 41 | targetElement = targetParent 42 | } 43 | 44 | if (!targetElement) { 45 | // Target element hasn't been assigned, so don't handle children. 46 | return 47 | } 48 | if (element.children?.length) { 49 | ancestors = [...ancestors] 50 | ancestors.unshift(element) 51 | for (let i = 0; i < element.children.length; i++) { 52 | walker( 53 | element.children[i], 54 | targetElement, 55 | element.children[i - 1], 56 | element.children[i + 1], 57 | ancestors 58 | ) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/mathml/msup.js: -------------------------------------------------------------------------------- 1 | import { getNary, getNaryTarget } from '../ooml/index.js' 2 | import { walker } from '../walker.js' 3 | 4 | export function msup(element, targetParent, previousSibling, nextSibling, ancestors) { 5 | // Superscript 6 | if (element.children.length !== 2) { 7 | // treat as mrow 8 | return targetParent 9 | } 10 | ancestors = [...ancestors] 11 | ancestors.unshift(element) 12 | const base = element.children[0] 13 | const superscript = element.children[1] 14 | 15 | let topTarget 16 | // 17 | // m:nAry 18 | // 19 | // Conditions: 20 | // 1. base text must be nary operator 21 | // 2. no accents 22 | const naryChar = getNary(base) 23 | if ( 24 | naryChar && 25 | element.attribs?.accent?.toLowerCase() !== 'true' && 26 | element.attribs?.accentunder?.toLowerCase() !== 'true' 27 | ) { 28 | topTarget = getNaryTarget(naryChar, element, 'subSup', true) 29 | element.isNary = true 30 | topTarget.children.push({ type: 'tag', name: 'm:sub' }) 31 | } else { 32 | const baseTarget = { 33 | name: 'm:e', 34 | type: 'tag', 35 | attribs: {}, 36 | children: [] 37 | } 38 | walker(base, baseTarget, false, false, ancestors) 39 | 40 | topTarget = { 41 | type: 'tag', 42 | name: 'm:sSup', 43 | attribs: {}, 44 | children: [ 45 | { 46 | type: 'tag', 47 | name: 'm:sSupPr', 48 | attribs: {}, 49 | children: [ 50 | { 51 | type: 'tag', 52 | name: 'm:ctrlPr', 53 | attribs: {}, 54 | children: [] 55 | } 56 | ] 57 | }, 58 | baseTarget 59 | ] 60 | } 61 | } 62 | 63 | const superscriptTarget = { 64 | name: 'm:sup', 65 | type: 'tag', 66 | attribs: {}, 67 | children: [] 68 | } 69 | 70 | walker(superscript, superscriptTarget, false, false, ancestors) 71 | 72 | topTarget.children.push(superscriptTarget) 73 | if (element.isNary) { 74 | topTarget.children.push({ type: 'tag', name: 'm:e', attribs: {}, children: [] }) 75 | } 76 | targetParent.children.push(topTarget) 77 | // Don't iterate over children in the usual way. 78 | } 79 | -------------------------------------------------------------------------------- /src/mathml/msub.js: -------------------------------------------------------------------------------- 1 | import { getNary, getNaryTarget } from '../ooml/index.js' 2 | import { walker } from '../walker.js' 3 | 4 | export function msub(element, targetParent, previousSibling, nextSibling, ancestors) { 5 | // Subscript 6 | if (element.children.length !== 2) { 7 | // treat as mrow 8 | return targetParent 9 | } 10 | ancestors = [...ancestors] 11 | ancestors.unshift(element) 12 | const base = element.children[0] 13 | const subscript = element.children[1] 14 | 15 | let topTarget 16 | // 17 | // m:nAry 18 | // 19 | // Conditions: 20 | // 1. base text must be nary operator 21 | // 2. no accents 22 | const naryChar = getNary(base) 23 | if ( 24 | naryChar && 25 | element.attribs?.accent?.toLowerCase() !== 'true' && 26 | element.attribs?.accentunder?.toLowerCase() !== 'true' 27 | ) { 28 | topTarget = getNaryTarget(naryChar, element, 'subSup', false, true) 29 | element.isNary = true 30 | } else { 31 | const baseTarget = { 32 | name: 'm:e', 33 | type: 'tag', 34 | attribs: {}, 35 | children: [] 36 | } 37 | walker(base, baseTarget, false, false, ancestors) 38 | topTarget = { 39 | type: 'tag', 40 | name: 'm:sSub', 41 | attribs: {}, 42 | children: [ 43 | { 44 | type: 'tag', 45 | name: 'm:sSubPr', 46 | attribs: {}, 47 | children: [ 48 | { 49 | type: 'tag', 50 | name: 'm:ctrlPr', 51 | attribs: {}, 52 | children: [] 53 | } 54 | ] 55 | }, 56 | baseTarget 57 | ] 58 | } 59 | } 60 | 61 | const subscriptTarget = { 62 | name: 'm:sub', 63 | type: 'tag', 64 | attribs: {}, 65 | children: [] 66 | } 67 | 68 | walker(subscript, subscriptTarget, false, false, ancestors) 69 | topTarget.children.push(subscriptTarget) 70 | if (element.isNary) { 71 | topTarget.children.push({ type: 'tag', name: 'm:sup', attribs: {}, children: [] }) 72 | topTarget.children.push({ type: 'tag', name: 'm:e', attribs: {}, children: [] }) 73 | } 74 | targetParent.children.push(topTarget) 75 | // Don't iterate over children in the usual way. 76 | } 77 | -------------------------------------------------------------------------------- /test/fixtures/joe_java/pythagorean2.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ( 9 | a 10 | + 11 | b 12 | ) 13 | 14 | 2 15 | 16 | 17 | 18 | = 19 | 20 | 21 | 22 | c 23 | 2 24 | 25 | + 26 | 4 27 | 28 | ( 29 | 30 | 1 31 | 2 32 | 33 | a 34 | b 35 | ) 36 | 37 | 38 | 39 | 40 | 41 | a 42 | 2 43 | 44 | + 45 | 2 46 | a 47 | b 48 | + 49 | 50 | b 51 | 2 52 | 53 | 54 | 55 | = 56 | 57 | 58 | 59 | c 60 | 2 61 | 62 | + 63 | 2 64 | a 65 | b 66 | 67 | 68 | 69 | 70 | 71 | a 72 | 2 73 | 74 | + 75 | 76 | b 77 | 2 78 | 79 | 80 | 81 | = 82 | 83 | 84 | 85 | c 86 | 2 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/mathml/msubsup.js: -------------------------------------------------------------------------------- 1 | import { getNary, getNaryTarget } from '../ooml/index.js' 2 | import { walker } from '../walker.js' 3 | 4 | export function msubsup(element, targetParent, previousSibling, nextSibling, ancestors) { 5 | // Sub + superscript 6 | if (element.children.length !== 3) { 7 | // treat as mrow 8 | return targetParent 9 | } 10 | 11 | ancestors = [...ancestors] 12 | ancestors.unshift(element) 13 | 14 | const base = element.children[0] 15 | const subscript = element.children[1] 16 | const superscript = element.children[2] 17 | 18 | let topTarget 19 | // 20 | // m:nAry 21 | // 22 | // Conditions: 23 | // 1. base text must be nary operator 24 | // 2. no accents 25 | const naryChar = getNary(base) 26 | if ( 27 | naryChar && 28 | element.attribs?.accent?.toLowerCase() !== 'true' && 29 | element.attribs?.accentunder?.toLowerCase() !== 'true' 30 | ) { 31 | topTarget = getNaryTarget(naryChar, element, 'subSup') 32 | element.isNary = true 33 | } else { 34 | // fallback: m:sSubSup 35 | const baseTarget = { 36 | name: 'm:e', 37 | type: 'tag', 38 | attribs: {}, 39 | children: [] 40 | } 41 | 42 | walker(base, baseTarget, false, false, ancestors) 43 | topTarget = { 44 | type: 'tag', 45 | name: 'm:sSubSup', 46 | attribs: {}, 47 | children: [ 48 | { 49 | type: 'tag', 50 | name: 'm:sSubSupPr', 51 | attribs: {}, 52 | children: [ 53 | { 54 | type: 'tag', 55 | name: 'm:ctrlPr', 56 | attribs: {}, 57 | children: [] 58 | } 59 | ] 60 | }, 61 | baseTarget 62 | ] 63 | } 64 | } 65 | 66 | const subscriptTarget = { 67 | name: 'm:sub', 68 | type: 'tag', 69 | attribs: {}, 70 | children: [] 71 | } 72 | const superscriptTarget = { 73 | name: 'm:sup', 74 | type: 'tag', 75 | attribs: {}, 76 | children: [] 77 | } 78 | walker(subscript, subscriptTarget, false, false, ancestors) 79 | walker(superscript, superscriptTarget, false, false, ancestors) 80 | topTarget.children.push(subscriptTarget) 81 | topTarget.children.push(superscriptTarget) 82 | if (element.isNary) { 83 | topTarget.children.push({ type: 'tag', name: 'm:e', attribs: {}, children: [] }) 84 | } 85 | targetParent.children.push(topTarget) 86 | // Don't iterate over children in the usual way. 87 | } 88 | -------------------------------------------------------------------------------- /src/mathml/table.js: -------------------------------------------------------------------------------- 1 | export function mtable(element, targetParent, previousSibling, nextSibling, ancestors) { 2 | const cellsPerRowCount = Math.max(...element.children.map((row) => row.children.length)) 3 | const targetElement = { 4 | name: 'm:m', 5 | type: 'tag', 6 | attribs: {}, 7 | children: [ 8 | { 9 | name: 'm:mPr', 10 | type: 'tag', 11 | attribs: {}, 12 | children: [ 13 | { 14 | name: 'm:baseJc', 15 | type: 'tag', 16 | attribs: { 17 | 'm:val': 'center' 18 | }, 19 | children: [] 20 | }, 21 | { 22 | name: 'm:plcHide', 23 | type: 'tag', 24 | attribs: { 25 | 'm:val': 'on' 26 | }, 27 | children: [] 28 | }, 29 | { 30 | name: 'm:mcs', 31 | type: 'tag', 32 | attribs: {}, 33 | children: [ 34 | { 35 | name: 'm:mc', 36 | type: 'tag', 37 | attribs: {}, 38 | children: [ 39 | { 40 | name: 'm:mcPr', 41 | type: 'tag', 42 | attribs: {}, 43 | children: [ 44 | { 45 | name: 'm:count', 46 | type: 'tag', 47 | attribs: { 48 | 'm:val': cellsPerRowCount.toString() 49 | }, 50 | children: [] 51 | }, 52 | { 53 | name: 'm:mcJc', 54 | type: 'tag', 55 | attribs: { 56 | 'm:val': 'center' 57 | }, 58 | children: [] 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | ] 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | targetParent.children.push(targetElement) 71 | return targetElement 72 | } 73 | 74 | export function mtd(element, targetParent, previousSibling, nextSibling, ancestors) { 75 | // table cell 76 | const targetElement = { 77 | name: 'm:e', 78 | type: 'tag', 79 | attribs: {}, 80 | children: [] 81 | } 82 | targetParent.children.push(targetElement) 83 | return targetElement 84 | } 85 | 86 | export function mtr(element, targetParent, previousSibling, nextSibling, ancestors) { 87 | // table row 88 | const targetElement = { 89 | name: 'm:mr', 90 | type: 'tag', 91 | attribs: {}, 92 | children: [] 93 | } 94 | targetParent.children.push(targetElement) 95 | return targetElement 96 | } 97 | -------------------------------------------------------------------------------- /test/fixtures/mathml2omml/menclose.mml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | a 5 | + 6 | b 7 | 8 | 9 | c 10 | + 11 | d 12 | 13 | 14 | e 15 | + 16 | f 17 | 18 | 19 | g 20 | + 21 | h 22 | 23 | 24 | i 25 | + 26 | j 27 | 28 | 29 | k 30 | + 31 | l 32 | 33 | 34 | m 35 | + 36 | n 37 | 38 | 39 | o 40 | + 41 | p 42 | 43 | 44 | q 45 | + 46 | r 47 | 48 | 49 | s 50 | + 51 | t 52 | 53 | 54 | u 55 | + 56 | v 57 | 58 | 59 | w 60 | + 61 | x 62 | 63 | 64 | y 65 | + 66 | z 67 | 68 | 69 | aa 70 | + 71 | bb 72 | 73 | 74 | cc 75 | + 76 | dd 77 | 78 | 79 | ee 80 | + 81 | ff 82 | 83 | 84 | gg 85 | + 86 | hh 87 | 88 | 89 | ii 90 | + 91 | jj 92 | 93 | 94 | kk 95 | + 96 | ll 97 | 98 | 99 | mm 100 | + 101 | nn 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/mathml/munderover.js: -------------------------------------------------------------------------------- 1 | import { getNary, getNaryTarget } from '../ooml/index.js' 2 | import { walker } from '../walker.js' 3 | 4 | export function munderover(element, targetParent, previousSibling, nextSibling, ancestors) { 5 | // Munderover 6 | if (element.children.length !== 3) { 7 | // treat as mrow 8 | return targetParent 9 | } 10 | 11 | ancestors = [...ancestors] 12 | ancestors.unshift(element) 13 | 14 | const base = element.children[0] 15 | const underscript = element.children[1] 16 | const overscript = element.children[2] 17 | 18 | // 19 | // m:nAry 20 | // 21 | // Conditions: 22 | // 1. base text must be nary operator 23 | // 2. no accents 24 | const naryChar = getNary(base) 25 | if ( 26 | naryChar && 27 | element.attributes?.accent?.toLowerCase() !== 'true' && 28 | element.attributes?.accentunder?.toLowerCase() !== 'true' 29 | ) { 30 | const topTarget = getNaryTarget(naryChar, element, 'undOvr') 31 | element.isNary = true 32 | const subscriptTarget = { 33 | name: 'm:sub', 34 | type: 'tag', 35 | attribs: {}, 36 | children: [] 37 | } 38 | const superscriptTarget = { 39 | name: 'm:sup', 40 | type: 'tag', 41 | attribs: {}, 42 | children: [] 43 | } 44 | walker(underscript, subscriptTarget, false, false, ancestors) 45 | walker(overscript, superscriptTarget, false, false, ancestors) 46 | topTarget.children.push(subscriptTarget) 47 | topTarget.children.push(superscriptTarget) 48 | topTarget.children.push({ type: 'tag', name: 'm:e', attribs: {}, children: [] }) 49 | targetParent.children.push(topTarget) 50 | return 51 | } 52 | 53 | // Fallback: m:limUpp()m:limlow 54 | 55 | const baseTarget = { 56 | name: 'm:e', 57 | type: 'tag', 58 | attribs: {}, 59 | children: [] 60 | } 61 | 62 | walker(base, baseTarget, false, false, ancestors) 63 | 64 | const underscriptTarget = { 65 | name: 'm:lim', 66 | type: 'tag', 67 | attribs: {}, 68 | children: [] 69 | } 70 | const overscriptTarget = { 71 | name: 'm:lim', 72 | type: 'tag', 73 | attribs: {}, 74 | children: [] 75 | } 76 | 77 | walker(underscript, underscriptTarget, false, false, ancestors) 78 | walker(overscript, overscriptTarget, false, false, ancestors) 79 | targetParent.children.push({ 80 | type: 'tag', 81 | name: 'm:limUpp', 82 | attribs: {}, 83 | children: [ 84 | { 85 | type: 'tag', 86 | name: 'm:e', 87 | attribs: {}, 88 | children: [ 89 | { 90 | type: 'tag', 91 | name: 'm:limLow', 92 | attribs: {}, 93 | children: [baseTarget, underscriptTarget] 94 | } 95 | ] 96 | }, 97 | overscriptTarget 98 | ] 99 | }) 100 | // Don't iterate over children in the usual way. 101 | } 102 | -------------------------------------------------------------------------------- /src/parse-stringify/parse.js: -------------------------------------------------------------------------------- 1 | import * as entities from 'entities' 2 | 3 | import parseTag from './parse-tag' 4 | 5 | const tagRE = /<[a-zA-Z0-9\-!/](?:"[^"]*"|'[^']*'|[^'">])*>/g 6 | const whitespaceRE = /^\s*$/ 7 | 8 | const textContainerNames = ['mtext', 'mi', 'mn', 'mo', 'ms'] 9 | 10 | // re-used obj for quick lookups of components 11 | const empty = Object.create(null) 12 | 13 | export function parse(html, options = {}) { 14 | const result = [] 15 | const arr = [] 16 | let current 17 | let level = -1 18 | 19 | html.replace(tagRE, (tag, index) => { 20 | const isOpen = tag.charAt(1) !== '/' 21 | const isComment = tag.startsWith('