├── .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 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/hub2docx-lib/mathvariant_italic.mml:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/hub2docx-lib/mroot_exponent_3.mml:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/test/fixtures/hub2docx-lib/mroot_empty_exponent.mml:
--------------------------------------------------------------------------------
1 |
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 |
10 |
--------------------------------------------------------------------------------
/test/fixtures/mathml2omml/groupchr.mml:
--------------------------------------------------------------------------------
1 |
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 |
11 |
--------------------------------------------------------------------------------
/test/fixtures/mathml2omml/single_line.mml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/mathml2omml/single_line_with_symbol.mml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/hub2docx-lib/munder_underscore.mml:
--------------------------------------------------------------------------------
1 |
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 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/mathml2omml/single_line_with_styling.mml:
--------------------------------------------------------------------------------
1 |
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 |
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 |
19 |
--------------------------------------------------------------------------------
/test/fixtures/hub2docx-lib/munder_sum.mml:
--------------------------------------------------------------------------------
1 |
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 |
21 |
--------------------------------------------------------------------------------
/test/fixtures/mathml2omml/mstyle.mml:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/test/fixtures/hub2docx-lib/msubsup_sum.mml:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/test/fixtures/hub2docx-lib/munderover_sum.mml:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/test/fixtures/mathml2omml/simple.mml:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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, '')}${doc.name}>`
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 |
43 |
--------------------------------------------------------------------------------
/test/fixtures/joe_java/axiom_of_power.mml:
--------------------------------------------------------------------------------
1 |
40 |
--------------------------------------------------------------------------------
/test/fixtures/joe_java/complex_number.mml:
--------------------------------------------------------------------------------
1 |
42 |
--------------------------------------------------------------------------------
/test/fixtures/joe_java/sophomores_dream.mml:
--------------------------------------------------------------------------------
1 |
52 |
--------------------------------------------------------------------------------
/test/fixtures/joe_java/divergence.mml:
--------------------------------------------------------------------------------
1 |
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 |
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 = ''
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 |
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 |
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('