dwml-ts 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,178 +1,178 @@
1
- # dwml-ts
2
-
3
- Convert **MS Office OMML** (Office Math Markup Language, the `<m:oMath>` markup used
4
- in `.docx` / `.pptx` / `.xlsx`) to **LaTeX** — in TypeScript.
5
-
6
- A faithful port of the Python [`dwml`](https://github.com/xiilei/dwml) library with
7
- **100% output fidelity** on its reference fixtures, hardened for real-world OOXML
8
- import pipelines (PowerPoint, Word).
9
-
10
- - **Pure, synchronous, deterministic** — no async, no config singletons.
11
- - **DOM-free** — runs identically in the browser bundle and in Node. No
12
- `DOMParser` / `XMLSerializer` / `xmldom`.
13
- - **One tiny runtime dep**: [`txml`](https://github.com/TobiasNickel/tXml) for parsing
14
- (which [PPTist](https://github.com/pipipi-pikachu/PPTist) already ships — zero new bytes).
15
- - **Never throws on unknown constructs** — degrades per-node to concatenated text
16
- content, so one odd `<m:oMath>` can't kill a whole paragraph import.
17
-
18
- ## Install
19
-
20
- ```bash
21
- npm install dwml-ts
22
- ```
23
-
24
- ## API
25
-
26
- All output is **bare LaTeX** — no `$`/`$$` delimiters, no trailing whitespace. The
27
- caller decides inline vs. display math (e.g. PPTist wraps it in its own
28
- `span.pptist-math[data-latex]` element).
29
-
30
- ### `ommlToLatex(xml, options?): string`
31
-
32
- Convert one OMML fragment to bare LaTeX. Accepts `<m:oMath>`, `<m:oMathPara>` or the
33
- `<a14:m>` wrapper as an XML string, and tolerates **missing `xmlns` declarations**
34
- (fragments sliced out of slide XML where the namespace lives on a root you don't have).
35
-
36
- ```ts
37
- import { ommlToLatex } from 'dwml-ts';
38
-
39
- ommlToLatex('<m:oMath><m:r><m:t>x+1</m:t></m:r></m:oMath>');
40
- // => 'x+1'
41
-
42
- ommlToLatex('<m:oMath><m:rad><m:radPr><m:degHide m:val="1"/></m:radPr><m:deg/><m:e><m:r><m:t>5</m:t></m:r></m:e></m:rad></m:oMath>');
43
- // => '\\sqrt{5}'
44
- ```
45
-
46
- ### `ommlNodeToLatex(node, options?): string`
47
-
48
- Node-level entry: convert an **already-parsed txml `TNode`**. This is the important one
49
- for import pipelines — walk each `<a:p>` of the slide XML yourself to preserve run order
50
- (text run → math → text run), and pass math nodes straight in. One parse per slide, no
51
- re-parsing fragments.
52
-
53
- ```ts
54
- import { parse } from 'txml';
55
- import { ommlNodeToLatex } from 'dwml-ts';
56
-
57
- const nodes = parse(slideXml);
58
- // ... find the <m:oMath> node(s) you care about, then:
59
- const latex = ommlNodeToLatex(omathNode);
60
- ```
61
-
62
- ### `extractOmmlLatex(xml, options?): string[]`
63
-
64
- Batch: find and convert every `<m:oMath>` in any OOXML document/fragment, returning one
65
- bare LaTeX string per formula, in document order.
66
-
67
- ```ts
68
- import { extractOmmlLatex } from 'dwml-ts';
69
-
70
- const [first, second] = extractOmmlLatex(documentXml);
71
- ```
72
-
73
- ### `OmmlToLatexOptions`
74
-
75
- ```ts
76
- export interface OmmlToLatexOptions {
77
- /** Called once per element local-name the converter had to degrade. */
78
- onUnsupported?: (localName: string) => void;
79
- }
80
- ```
81
-
82
- ## Behavior guarantees
83
-
84
- | Guarantee | Notes |
85
- | --- | --- |
86
- | **Bare LaTeX out** | No delimiters, no trailing whitespace. |
87
- | **Never throws** | Unknown constructs degrade per-node to concatenated text (`m:borderBox`, `m:phant`, unknown `m:fName`, unknown `m:limLow` base, …). A dropped `\frac` bar is recoverable; an exception is not. |
88
- | **Namespace by local name** | Matches `oMath` / `f` / `r` / … regardless of prefix (`m:`, none, or re-prefixed). Real-world producers vary. |
89
- | **Namespace-agnostic attributes** | `m:val` / `val` / re-prefixed attributes resolve by local name. |
90
-
91
- ## Supported OMML constructs
92
-
93
- `m:oMath` / `m:oMathPara` / `<a14:m>` wrappers, and:
94
-
95
- - **Runs** — `m:r` / `m:t` with Unicode→LaTeX symbol mapping (see below)
96
- - **Fractions** — `m:f` with `m:fPr` types `bar` (default, `\frac`), `skw` (`^{n}/_{d}`),
97
- `noBar` (`\genfrac`, binomial), `lin` (`{n}/{d}`)
98
- - **Scripts** — `m:sSub`, `m:sSup`, `m:sSubSup` (and `m:sPre`, degraded)
99
- - **Radicals** — `m:rad` with `m:deg` and `degHide` (`\sqrt`, `\sqrt[n]`)
100
- - **N-ary** — `m:nary` (`\sum`, `\int`, `\prod`, … via `CHR_BO`)
101
- - **Delimiters** — `m:d` with `begChr` / `endChr` / `sepChr` (`\left…\right`)
102
- - **Functions** — `m:func` / `m:fName` (`\sin`, `\cos`, `\log`, `\ln`, …)
103
- - **Accents** — `m:acc` (`\hat`, `\tilde`, `\vec`, … via `CHR`)
104
- - **Bars** — `m:bar` (`\overline` / `\underline`)
105
- - **Limits** — `m:limLow` (`\lim_{…}`), `m:limUpp` (`\overset`)
106
- - **Matrix** — `m:m` / `m:mr` (`\begin{matrix}…\end{matrix}`)
107
- - **Equation array** — `m:eqArr` (`\begin{array}{c}…\end{array}`)
108
- - **Group char** — `m:groupChr` (`\overbrace`, `\underbrace`, …)
109
- - **Box** — `m:box` (passthrough)
110
-
111
- ## Unicode → LaTeX mapping
112
-
113
- Run text is mapped to LaTeX with a two-tier lookup (per Unicode code point, so
114
- astral-plane Mathematical Alphanumeric Symbols are handled correctly):
115
-
116
- 1. **dwml's symbol table** (`src/latex-dict.ts`) — Mathematical Alphanumeric Symbols
117
- `U+1D400–U+1D7FF` → ASCII (`𝐴→A`, `𝑥→x`), Greek (`π→\pi`, both plain `U+03B1..` and
118
- math-italic `U+1D6FC..`), relation/operator symbols (`≤→\leq`, `≥→\geq`, `≠→\ne`,
119
- `·→\cdot`, `×→\times`, `∞→\infty`, `→→\rightarrow`, …).
120
- 2. **pylatexenc's builtin map** (`src/uni2latex.generated.ts`, internalized from
121
- pylatexenc 2.x, MIT) — everything else with a known LaTeX escape.
122
-
123
- LaTeX-special characters in runs (`%`, `&`, `_`, `{`, `}`, `#`, `$`, `~`, `^`) are escaped
124
- via dwml's `escape_latex` (`a%` → `a\%`).
125
-
126
- ## Examples
127
-
128
- | OMML input | LaTeX output |
129
- | --- | --- |
130
- | `\sin(\sqrt[3]{x})^{x^{11}}/_{b}x_{m}^{n}` fixture | `\sin(\sqrt[3]{x})^{x^{11}}/_{b}x_{m}^{n}` |
131
- | group fixture | `A\overbrace{123}\underbrace{456}=\left\{a+b\right)` |
132
- | matrix fixture | `A=\left(\begin{matrix}1&2&3\\4&5&6\end{matrix}\right)\sum_{1}^{20}x` |
133
- | no-bar fraction (binomial) | `\left(x+a\right)^{n}=\sum_{k=0}^{n}\left(\genfrac{}{}{0pt}{}{n}{k}\right)x^{k}a^{n-k}` |
134
-
135
- ## Project layout
136
-
137
- ```
138
- src/
139
- index.ts public API (ommlToLatex / ommlNodeToLatex / extractOmmlLatex)
140
- omml.ts the converter (never-throw, namespace-agnostic)
141
- latex-dict.ts dwml symbol dictionaries
142
- latex-encode.ts pylatexenc-derived unicode->latex encoder
143
- uni2latex.generated.ts generated pylatexenc map (do not edit by hand)
144
- tests/
145
- api.test.ts consumer contract + dwml reference fidelity
146
- constructs.test.ts per-construct coverage
147
- ground-truth.json Python dwml reference outputs
148
- reference/ original Python sources (base + two forks + canonical merge)
149
- scripts/
150
- gen_uni2latex.py regenerate the unicode map from pylatexenc
151
- gen_ground_truth.py regenerate tests/ground-truth.json from Python dwml
152
- ```
153
-
154
- ## Development
155
-
156
- ```bash
157
- npm install
158
- npm test # rstest
159
- npm run build # rslib -> dist/{index.js,index.d.ts} (ESM only)
160
-
161
- # regenerate the unicode map / ground truth (requires Python + pylatexenc)
162
- npm run gen:unicode
163
- npm run gen:groundtruth
164
- ```
165
-
166
- ## Attribution
167
-
168
- - Port of [`dwml`](https://github.com/xiilei/dwml) (Apache-2.0), incorporating fixes from
169
- the [`xavier-bw/dwml`](https://github.com/xavier-bw/dwml) fork (log/ln functions) and
170
- [`bazingarj/OMML-to-Latex-py`](https://github.com/bazingarj/OMML-to-Latex-py) fork
171
- (whitespace-tolerant function names). Test fixtures under `reference/tests-base/` are
172
- Apache-2.0, from the original project.
173
- - The Unicode→LaTeX map is derived from [`pylatexenc`](https://github.com/phfaist/pylatexenc)
174
- (MIT, © Philippe Faist).
175
-
176
- ## License
177
-
178
- Apache-2.0
1
+ # dwml-ts
2
+
3
+ Convert **MS Office OMML** (Office Math Markup Language, the `<m:oMath>` markup used
4
+ in `.docx` / `.pptx` / `.xlsx`) to **LaTeX** — in TypeScript.
5
+
6
+ A faithful port of the Python [`dwml`](https://github.com/xiilei/dwml) library with
7
+ **100% output fidelity** on its reference fixtures, hardened for real-world OOXML
8
+ import pipelines (PowerPoint, Word).
9
+
10
+ - **Pure, synchronous, deterministic** — no async, no config singletons.
11
+ - **DOM-free** — runs identically in the browser bundle and in Node. No
12
+ `DOMParser` / `XMLSerializer` / `xmldom`.
13
+ - **One tiny runtime dep**: [`txml`](https://github.com/TobiasNickel/tXml) for parsing
14
+ (which [PPTist](https://github.com/pipipi-pikachu/PPTist) already ships — zero new bytes).
15
+ - **Never throws on unknown constructs** — degrades per-node to concatenated text
16
+ content, so one odd `<m:oMath>` can't kill a whole paragraph import.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install dwml-ts
22
+ ```
23
+
24
+ ## API
25
+
26
+ All output is **bare LaTeX** — no `$`/`$$` delimiters, no trailing whitespace. The
27
+ caller decides inline vs. display math (e.g. PPTist wraps it in its own
28
+ `span.pptist-math[data-latex]` element).
29
+
30
+ ### `ommlToLatex(xml, options?): string`
31
+
32
+ Convert one OMML fragment to bare LaTeX. Accepts `<m:oMath>`, `<m:oMathPara>` or the
33
+ `<a14:m>` wrapper as an XML string, and tolerates **missing `xmlns` declarations**
34
+ (fragments sliced out of slide XML where the namespace lives on a root you don't have).
35
+
36
+ ```ts
37
+ import { ommlToLatex } from 'dwml-ts';
38
+
39
+ ommlToLatex('<m:oMath><m:r><m:t>x+1</m:t></m:r></m:oMath>');
40
+ // => 'x+1'
41
+
42
+ ommlToLatex('<m:oMath><m:rad><m:radPr><m:degHide m:val="1"/></m:radPr><m:deg/><m:e><m:r><m:t>5</m:t></m:r></m:e></m:rad></m:oMath>');
43
+ // => '\\sqrt{5}'
44
+ ```
45
+
46
+ ### `ommlNodeToLatex(node, options?): string`
47
+
48
+ Node-level entry: convert an **already-parsed txml `TNode`**. This is the important one
49
+ for import pipelines — walk each `<a:p>` of the slide XML yourself to preserve run order
50
+ (text run → math → text run), and pass math nodes straight in. One parse per slide, no
51
+ re-parsing fragments.
52
+
53
+ ```ts
54
+ import { parse } from 'txml';
55
+ import { ommlNodeToLatex } from 'dwml-ts';
56
+
57
+ const nodes = parse(slideXml);
58
+ // ... find the <m:oMath> node(s) you care about, then:
59
+ const latex = ommlNodeToLatex(omathNode);
60
+ ```
61
+
62
+ ### `extractOmmlLatex(xml, options?): string[]`
63
+
64
+ Batch: find and convert every `<m:oMath>` in any OOXML document/fragment, returning one
65
+ bare LaTeX string per formula, in document order.
66
+
67
+ ```ts
68
+ import { extractOmmlLatex } from 'dwml-ts';
69
+
70
+ const [first, second] = extractOmmlLatex(documentXml);
71
+ ```
72
+
73
+ ### `OmmlToLatexOptions`
74
+
75
+ ```ts
76
+ export interface OmmlToLatexOptions {
77
+ /** Called once per element local-name the converter had to degrade. */
78
+ onUnsupported?: (localName: string) => void;
79
+ }
80
+ ```
81
+
82
+ ## Behavior guarantees
83
+
84
+ | Guarantee | Notes |
85
+ | --- | --- |
86
+ | **Bare LaTeX out** | No delimiters, no trailing whitespace. |
87
+ | **Never throws** | Unknown constructs degrade per-node to concatenated text (`m:borderBox`, `m:phant`, unknown `m:fName`, unknown `m:limLow` base, …). A dropped `\frac` bar is recoverable; an exception is not. |
88
+ | **Namespace by local name** | Matches `oMath` / `f` / `r` / … regardless of prefix (`m:`, none, or re-prefixed). Real-world producers vary. |
89
+ | **Namespace-agnostic attributes** | `m:val` / `val` / re-prefixed attributes resolve by local name. |
90
+
91
+ ## Supported OMML constructs
92
+
93
+ `m:oMath` / `m:oMathPara` / `<a14:m>` wrappers, and:
94
+
95
+ - **Runs** — `m:r` / `m:t` with Unicode→LaTeX symbol mapping (see below)
96
+ - **Fractions** — `m:f` with `m:fPr` types `bar` (default, `\frac`), `skw` (`^{n}/_{d}`),
97
+ `noBar` (`\genfrac`, binomial), `lin` (`{n}/{d}`)
98
+ - **Scripts** — `m:sSub`, `m:sSup`, `m:sSubSup` (and `m:sPre`, degraded)
99
+ - **Radicals** — `m:rad` with `m:deg` and `degHide` (`\sqrt`, `\sqrt[n]`)
100
+ - **N-ary** — `m:nary` (`\sum`, `\int`, `\prod`, … via `CHR_BO`)
101
+ - **Delimiters** — `m:d` with `begChr` / `endChr` / `sepChr` (`\left…\right`)
102
+ - **Functions** — `m:func` / `m:fName` (`\sin`, `\cos`, `\log`, `\ln`, …)
103
+ - **Accents** — `m:acc` (`\hat`, `\tilde`, `\vec`, … via `CHR`)
104
+ - **Bars** — `m:bar` (`\overline` / `\underline`)
105
+ - **Limits** — `m:limLow` (`\lim_{…}`), `m:limUpp` (`\overset`)
106
+ - **Matrix** — `m:m` / `m:mr` (`\begin{matrix}…\end{matrix}`)
107
+ - **Equation array** — `m:eqArr` (`\begin{array}{c}…\end{array}`)
108
+ - **Group char** — `m:groupChr` (`\overbrace`, `\underbrace`, …)
109
+ - **Box** — `m:box` (passthrough)
110
+
111
+ ## Unicode → LaTeX mapping
112
+
113
+ Run text is mapped to LaTeX with a two-tier lookup (per Unicode code point, so
114
+ astral-plane Mathematical Alphanumeric Symbols are handled correctly):
115
+
116
+ 1. **dwml's symbol table** (`src/latex-dict.ts`) — Mathematical Alphanumeric Symbols
117
+ `U+1D400–U+1D7FF` → ASCII (`𝐴→A`, `𝑥→x`), Greek (`π→\pi`, both plain `U+03B1..` and
118
+ math-italic `U+1D6FC..`), relation/operator symbols (`≤→\leq`, `≥→\geq`, `≠→\ne`,
119
+ `·→\cdot`, `×→\times`, `∞→\infty`, `→→\rightarrow`, …).
120
+ 2. **pylatexenc's builtin map** (`src/uni2latex.generated.ts`, internalized from
121
+ pylatexenc 2.x, MIT) — everything else with a known LaTeX escape.
122
+
123
+ LaTeX-special characters in runs (`%`, `&`, `_`, `{`, `}`, `#`, `$`, `~`, `^`) are escaped
124
+ via dwml's `escape_latex` (`a%` → `a\%`).
125
+
126
+ ## Examples
127
+
128
+ | OMML input | LaTeX output |
129
+ | --- | --- |
130
+ | `\sin(\sqrt[3]{x})^{x^{11}}/_{b}x_{m}^{n}` fixture | `\sin(\sqrt[3]{x})^{x^{11}}/_{b}x_{m}^{n}` |
131
+ | group fixture | `A\overbrace{123}\underbrace{456}=\left\{a+b\right)` |
132
+ | matrix fixture | `A=\left(\begin{matrix}1&2&3\\4&5&6\end{matrix}\right)\sum_{1}^{20}x` |
133
+ | no-bar fraction (binomial) | `\left(x+a\right)^{n}=\sum_{k=0}^{n}\left(\genfrac{}{}{0pt}{}{n}{k}\right)x^{k}a^{n-k}` |
134
+
135
+ ## Project layout
136
+
137
+ ```
138
+ src/
139
+ index.ts public API (ommlToLatex / ommlNodeToLatex / extractOmmlLatex)
140
+ omml.ts the converter (never-throw, namespace-agnostic)
141
+ latex-dict.ts dwml symbol dictionaries
142
+ latex-encode.ts pylatexenc-derived unicode->latex encoder
143
+ uni2latex.generated.ts generated pylatexenc map (do not edit by hand)
144
+ tests/
145
+ api.test.ts consumer contract + dwml reference fidelity
146
+ constructs.test.ts per-construct coverage
147
+ ground-truth.json Python dwml reference outputs
148
+ reference/ original Python sources (base + two forks + canonical merge)
149
+ scripts/
150
+ gen_uni2latex.py regenerate the unicode map from pylatexenc
151
+ gen_ground_truth.py regenerate tests/ground-truth.json from Python dwml
152
+ ```
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ npm install
158
+ npm test # rstest
159
+ npm run build # rslib -> dist/{index.js,index.d.ts} (ESM only)
160
+
161
+ # regenerate the unicode map / ground truth (requires Python + pylatexenc)
162
+ npm run gen:unicode
163
+ npm run gen:groundtruth
164
+ ```
165
+
166
+ ## Attribution
167
+
168
+ - Port of [`dwml`](https://github.com/xiilei/dwml) (Apache-2.0), incorporating fixes from
169
+ the [`xavier-bw/dwml`](https://github.com/xavier-bw/dwml) fork (log/ln functions) and
170
+ [`bazingarj/OMML-to-Latex-py`](https://github.com/bazingarj/OMML-to-Latex-py) fork
171
+ (whitespace-tolerant function names). Test fixtures under `reference/tests-base/` are
172
+ Apache-2.0, from the original project.
173
+ - The Unicode→LaTeX map is derived from [`pylatexenc`](https://github.com/phfaist/pylatexenc)
174
+ (MIT, © Philippe Faist).
175
+
176
+ ## License
177
+
178
+ Apache-2.0
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { TNode } from 'txml';
1
+ import { TNode } from 'txml/txml';
2
2
 
3
3
  export declare function elementChildren(node: TNode): TNode[];
4
4
 
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { parse } from "txml";
1
+ import { parse } from "txml/txml";
2
2
  const UNI2LATEX = {
3
3
  34: "''",
4
4
  35: "\\#",
@@ -1808,7 +1808,32 @@ const T = {
1808
1808
  𝑤: 'w',
1809
1809
  𝑥: 'x',
1810
1810
  𝑦: 'y',
1811
- 𝑧: 'z'
1811
+ 𝑧: 'z',
1812
+ 𝒉: 'h',
1813
+ 𝛢: 'A',
1814
+ 𝛣: 'B',
1815
+ 𝛤: '\\Gamma ',
1816
+ 𝛥: '\\Delta ',
1817
+ 𝛦: 'E',
1818
+ 𝛧: 'Z',
1819
+ 𝛨: 'H',
1820
+ 𝛩: '\\Theta ',
1821
+ 𝛪: 'I',
1822
+ 𝛫: 'K',
1823
+ 𝛬: '\\Lambda ',
1824
+ 𝛭: 'M',
1825
+ 𝛮: 'N',
1826
+ 𝛯: '\\Xi ',
1827
+ 𝛰: 'O',
1828
+ 𝛱: '\\Pi ',
1829
+ 𝛲: 'P',
1830
+ 𝛴: '\\Sigma ',
1831
+ 𝛵: 'T',
1832
+ 𝛶: '\\Upsilon ',
1833
+ 𝛷: '\\Phi ',
1834
+ 𝛸: 'X',
1835
+ 𝛹: '\\Psi ',
1836
+ 𝛺: '\\Omega '
1812
1837
  };
1813
1838
  const FUNC = {
1814
1839
  sin: '\\sin({fe})',
@@ -1866,12 +1891,21 @@ const F = {
1866
1891
  lin: '{{{num}}}/{{{den}}}'
1867
1892
  };
1868
1893
  const F_DEFAULT = '\\frac{{{num}}}{{{den}}}';
1894
+ const F_SMALL = '\\tfrac{{{num}}}{{{den}}}';
1869
1895
  const D = '\\left{left}{text}\\right{right}';
1870
1896
  const D_DEFAULT = {
1871
1897
  left: '(',
1872
1898
  right: ')',
1899
+ sep: '|',
1873
1900
  null: '.'
1874
1901
  };
1902
+ const BORDER_BOX = '\\boxed{{{0}}}';
1903
+ const PHANT = '\\phantom{{{0}}}';
1904
+ const M_JC = {
1905
+ left: '\\begin{{array}}{{l}}{text}\\end{{array}}',
1906
+ right: '\\begin{{array}}{{r}}{text}\\end{{array}}',
1907
+ center: '\\begin{{matrix}}{text}\\end{{matrix}}'
1908
+ };
1875
1909
  const RAD = '\\sqrt[{deg}]{{{text}}}';
1876
1910
  const RAD_DEFAULT = '\\sqrt{{{text}}}';
1877
1911
  const ARR = '\\begin{{array}}{{c}}{text}\\end{{array}}';
@@ -1967,12 +2001,25 @@ function getVal(key, def, store) {
1967
2001
  }
1968
2002
  return def;
1969
2003
  }
2004
+ function isOn(val, def = false) {
2005
+ if (void 0 === val) return def;
2006
+ const v = val.toLowerCase();
2007
+ return 'on' === v || '1' === v || 'true' === v;
2008
+ }
1970
2009
  const PR_VAL_TAGS = new Set([
1971
2010
  'chr',
1972
2011
  'pos',
1973
2012
  'begChr',
1974
2013
  'endChr',
1975
- 'type'
2014
+ 'sepChr',
2015
+ 'type',
2016
+ 'show',
2017
+ 'smallFrac',
2018
+ 'limLoc',
2019
+ 'intLim',
2020
+ 'naryLim',
2021
+ 'mcJc',
2022
+ 'degHide'
1976
2023
  ]);
1977
2024
  class Pr {
1978
2025
  get(name) {
@@ -1997,6 +2044,17 @@ function getAttr(node, attrLocal) {
1997
2044
  return null == v ? void 0 : v;
1998
2045
  }
1999
2046
  }
2047
+ function findDescendantAttr(node, elemLocal, attrLocal) {
2048
+ for (const child of node.children){
2049
+ if (!isElement(child)) continue;
2050
+ if (localName(child.tagName) === elemLocal) {
2051
+ const v = getAttr(child, attrLocal);
2052
+ if (void 0 !== v) return v;
2053
+ }
2054
+ const deep = findDescendantAttr(child, elemLocal, attrLocal);
2055
+ if (void 0 !== deep) return deep;
2056
+ }
2057
+ }
2000
2058
  const DIRECT_TAGS = new Set([
2001
2059
  'box',
2002
2060
  'sSub',
@@ -2086,6 +2144,12 @@ class Converter {
2086
2144
  return this.doMr(elm);
2087
2145
  case 'nary':
2088
2146
  return this.doNary(elm);
2147
+ case 'borderBox':
2148
+ return this.doBorderBox(elm);
2149
+ case 'phant':
2150
+ return this.doPhant(elm);
2151
+ case 'sPre':
2152
+ return this.doSPre(elm);
2089
2153
  default:
2090
2154
  return null;
2091
2155
  }
@@ -2120,7 +2184,12 @@ class Converter {
2120
2184
  const nullChar = D_DEFAULT["null"];
2121
2185
  const sVal = getVal(pr?.get('begChr'), D_DEFAULT.left, T);
2122
2186
  const eVal = getVal(pr?.get('endChr'), D_DEFAULT.right, T);
2123
- const text = c['e'] ?? '';
2187
+ const sepVal = getVal(pr?.get('sepChr'), D_DEFAULT.sep, T);
2188
+ const bases = this.processChildrenList(elm, new Set([
2189
+ 'e'
2190
+ ])).map((x)=>String(x.value));
2191
+ const sep = sepVal ? escapeLatex(sepVal) : '';
2192
+ const text = bases.join(sep);
2124
2193
  return (pr?.text ?? '') + pyFormat(D, [], {
2125
2194
  left: sVal ? escapeLatex(sVal) : nullChar,
2126
2195
  text,
@@ -2140,7 +2209,8 @@ class Converter {
2140
2209
  doF(elm) {
2141
2210
  const c = this.processChildrenDict(elm);
2142
2211
  const pr = c['fPr'];
2143
- const latexS = getVal(pr?.get('type'), F_DEFAULT, F) ?? '';
2212
+ let latexS = getVal(pr?.get('type'), F_DEFAULT, F) ?? '';
2213
+ if (latexS === F_DEFAULT && isOn(pr?.get('smallFrac'))) latexS = F_SMALL;
2144
2214
  const num = c['num'] ?? '';
2145
2215
  const den = c['den'] ?? '';
2146
2216
  return (pr?.text ?? '') + pyFormat(latexS, [], {
@@ -2229,10 +2299,16 @@ class Converter {
2229
2299
  }
2230
2300
  doM(elm) {
2231
2301
  const rows = [];
2232
- for (const { stag, value } of this.processChildrenList(elm))if ('mPr' !== stag) {
2302
+ let template = M;
2303
+ for (const { stag, value, node } of this.processChildrenList(elm)){
2304
+ if ('mPr' === stag) {
2305
+ const jc = findDescendantAttr(node, 'mcJc', VAL_SUFFIX);
2306
+ if (jc && M_JC[jc]) template = M_JC[jc];
2307
+ continue;
2308
+ }
2233
2309
  if ('mr' === stag) rows.push(String(value));
2234
2310
  }
2235
- return pyFormat(M, [], {
2311
+ return pyFormat(template, [], {
2236
2312
  text: rows.join("\\\\")
2237
2313
  });
2238
2314
  }
@@ -2244,9 +2320,37 @@ class Converter {
2244
2320
  doNary(elm) {
2245
2321
  const res = [];
2246
2322
  let bo = '';
2247
- for (const { stag, value } of this.processChildrenList(elm))if ('naryPr' === stag) bo = getVal(value.get('chr'), void 0, CHR_BO) ?? '';
2248
- else res.push(String(value));
2249
- return bo + res.join("");
2323
+ let limLoc = '';
2324
+ for (const { stag, value } of this.processChildrenList(elm))if ('naryPr' === stag) {
2325
+ const pr = value;
2326
+ bo = getVal(pr.get('chr'), void 0, CHR_BO) ?? '';
2327
+ limLoc = pr.get('naryLim') ?? pr.get('limLoc') ?? pr.get('intLim') ?? '';
2328
+ } else res.push(String(value));
2329
+ const limits = 'undOvr' === limLoc ? '\\limits' : '';
2330
+ return bo + limits + res.join("");
2331
+ }
2332
+ doBorderBox(elm) {
2333
+ const c = this.processChildrenDict(elm);
2334
+ const e = c['e'] ?? '';
2335
+ return pyFormat(BORDER_BOX, [
2336
+ e
2337
+ ]);
2338
+ }
2339
+ doPhant(elm) {
2340
+ const c = this.processChildrenDict(elm);
2341
+ const pr = c['phantPr'];
2342
+ const e = c['e'] ?? '';
2343
+ if (isOn(pr?.get('show'), true)) return e;
2344
+ return pyFormat(PHANT, [
2345
+ e
2346
+ ]);
2347
+ }
2348
+ doSPre(elm) {
2349
+ const c = this.processChildrenDict(elm);
2350
+ const e = c['e'] ?? '';
2351
+ const sub = c['sub'] ?? '';
2352
+ const sup = c['sup'] ?? '';
2353
+ return `{}${sub}${sup}{${e}}`;
2250
2354
  }
2251
2355
  processUnicode(s) {
2252
2356
  let out = '';