Skip to content

Commit db0853d

Browse files
feat(math): implement m:bar overbar/underbar converter (#2619)
* feat(math): implement m:bar overbar/underbar OMML converter Closes #2610 - Add convertBar() in converters/bar.ts: - Reads m:barPr/m:pos@m:val to determine position - 'top' (default) β†’ <mover> with U+203E (overline) - 'bot' β†’ <munder> with U+0332 (combining low line) - stretchy='true' so the bar stretches over the base expression - Register 'm:bar': convertBar in MATH_OBJECT_REGISTRY - Export convertBar from converters/index.ts - Add 3 unit tests: overbar, underbar, missing barPr fallback * fix(math): address review feedback on m:bar converter - Default to munder (underbar) when no position is specified, matching Word's rendering behavior (posVal !== 'top') - Wrap base content in <mrow> to correctly group multi-token expressions like 'x + y' as a single MathML child - Move m:bar entry from 'Not yet implemented' to 'Implemented' section in the registry - Strengthen tests: assert base content text and <mo> character for all three cases (top, bot, default) --------- Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
1 parent 70a2dbe commit db0853d

4 files changed

Lines changed: 107 additions & 2 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { MathObjectConverter } from '../types.js';
2+
3+
const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
4+
5+
/**
6+
* Convert m:bar (overbar/underbar) to MathML <mover> or <munder>.
7+
*
8+
* OMML structure:
9+
* m:bar β†’ m:barPr (optional: m:pos@m:val="top"|"bot"), m:e (base expression)
10+
*
11+
* MathML output:
12+
* top: <mover> <mrow>base</mrow> <mo>&#x203E;</mo> </mover>
13+
* bot (default): <munder> <mrow>base</mrow> <mo>&#x0332;</mo> </munder>
14+
*
15+
* Word renders an underbar when no position is specified, so the default is "bot".
16+
*
17+
* @spec ECMA-376 Β§22.1.2.7
18+
*/
19+
export const convertBar: MathObjectConverter = (node, doc, convertChildren) => {
20+
const elements = node.elements ?? [];
21+
22+
const barPr = elements.find((e) => e.name === 'm:barPr');
23+
const pos = barPr?.elements?.find((e) => e.name === 'm:pos');
24+
const posVal = pos?.attributes?.['m:val'];
25+
const isUnder = posVal !== 'top';
26+
27+
const base = elements.find((e) => e.name === 'm:e');
28+
29+
const wrapper = doc.createElementNS(MATHML_NS, isUnder ? 'munder' : 'mover');
30+
31+
const baseContent = convertChildren(base?.elements ?? []);
32+
const mrow = doc.createElementNS(MATHML_NS, 'mrow');
33+
mrow.appendChild(baseContent);
34+
wrapper.appendChild(mrow);
35+
36+
const accent = doc.createElementNS(MATHML_NS, 'mo');
37+
accent.setAttribute('stretchy', 'true');
38+
// U+203E = overline, U+0332 = combining low line (underbar)
39+
accent.textContent = isUnder ? '\u0332' : '\u203E';
40+
wrapper.appendChild(accent);
41+
42+
return wrapper;
43+
};

β€Žpackages/layout-engine/painters/dom/src/features/math/converters/index.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
*/
99
export { convertMathRun } from './math-run.js';
1010
export { convertFraction } from './fraction.js';
11+
export { convertBar } from './bar.js';

β€Žpackages/layout-engine/painters/dom/src/features/math/omml-to-mathml.test.tsβ€Ž

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,64 @@ describe('convertOmmlToMathml', () => {
204204
expect(children.some((c) => c.localName === 'mn')).toBe(true); // 1
205205
});
206206
});
207+
208+
describe('m:bar converter', () => {
209+
it('renders overbar (top) as <mover> with U+203E', () => {
210+
const omml = {
211+
name: 'm:oMath',
212+
elements: [{
213+
name: 'm:bar',
214+
elements: [
215+
{ name: 'm:barPr', elements: [{ name: 'm:pos', attributes: { 'm:val': 'top' } }] },
216+
{ name: 'm:e', elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'x' }] }] }] },
217+
],
218+
}],
219+
};
220+
const result = convertOmmlToMathml(omml, doc);
221+
expect(result).not.toBeNull();
222+
const mover = result!.querySelector('mover');
223+
expect(mover).not.toBeNull();
224+
expect(mover!.firstElementChild!.textContent).toBe('x');
225+
const mo = mover!.querySelector('mo');
226+
expect(mo?.textContent).toBe('\u203E');
227+
});
228+
229+
it('renders underbar (bot) as <munder> with U+0332', () => {
230+
const omml = {
231+
name: 'm:oMath',
232+
elements: [{
233+
name: 'm:bar',
234+
elements: [
235+
{ name: 'm:barPr', elements: [{ name: 'm:pos', attributes: { 'm:val': 'bot' } }] },
236+
{ name: 'm:e', elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'y' }] }] }] },
237+
],
238+
}],
239+
};
240+
const result = convertOmmlToMathml(omml, doc);
241+
expect(result).not.toBeNull();
242+
const munder = result!.querySelector('munder');
243+
expect(munder).not.toBeNull();
244+
expect(munder!.firstElementChild!.textContent).toBe('y');
245+
const mo = munder!.querySelector('mo');
246+
expect(mo?.textContent).toBe('\u0332');
247+
});
248+
249+
it('defaults to underbar when m:barPr is missing (matches Word behavior)', () => {
250+
const omml = {
251+
name: 'm:oMath',
252+
elements: [{
253+
name: 'm:bar',
254+
elements: [
255+
{ name: 'm:e', elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'z' }] }] }] },
256+
],
257+
}],
258+
};
259+
const result = convertOmmlToMathml(omml, doc);
260+
expect(result).not.toBeNull();
261+
const munder = result!.querySelector('munder');
262+
expect(munder).not.toBeNull();
263+
expect(munder!.firstElementChild!.textContent).toBe('z');
264+
const mo = munder!.querySelector('mo');
265+
expect(mo?.textContent).toBe('\u0332');
266+
});
267+
});

β€Žpackages/layout-engine/painters/dom/src/features/math/omml-to-mathml.tsβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*/
1111

1212
import type { OmmlJsonNode, MathObjectConverter } from './types.js';
13-
import { convertMathRun, convertFraction } from './converters/index.js';
13+
import { convertMathRun, convertFraction, convertBar } from './converters/index.js';
1414

1515
export const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
1616

@@ -30,10 +30,10 @@ export const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
3030
const MATH_OBJECT_REGISTRY: Record<string, MathObjectConverter | null> = {
3131
// ── Implemented ──────────────────────────────────────────────────────────
3232
'm:r': convertMathRun,
33+
'm:bar': convertBar, // Bar (overbar/underbar)
3334

3435
// ── Not yet implemented (community contributions welcome) ────────────────
3536
'm:acc': null, // Accent (diacritical mark above base)
36-
'm:bar': null, // Bar (overbar/underbar)
3737
'm:borderBox': null, // Border box (border around math content)
3838
'm:box': null, // Box (invisible grouping container)
3939
'm:d': null, // Delimiter (parentheses, brackets, braces)

0 commit comments

Comments
Β (0)