Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { MathObjectConverter } from '../types.js';

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

/**
* Convert m:bar (overbar/underbar) to MathML <mover> or <munder>.
*
* OMML structure:
* m:bar → m:barPr (optional: m:pos@m:val="top"|"bot"), m:e (base expression)
*
* MathML output:
* top: <mover> <mrow>base</mrow> <mo>&#x203E;</mo> </mover>
* bot (default): <munder> <mrow>base</mrow> <mo>&#x0332;</mo> </munder>
*
* Word renders an underbar when no position is specified, so the default is "bot".
*
* @spec ECMA-376 §22.1.2.7
*/
export const convertBar: MathObjectConverter = (node, doc, convertChildren) => {
const elements = node.elements ?? [];

const barPr = elements.find((e) => e.name === 'm:barPr');
const pos = barPr?.elements?.find((e) => e.name === 'm:pos');
const posVal = pos?.attributes?.['m:val'];
const isUnder = posVal !== 'top';

Comment on lines +22 to +26
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m:bar default position appears to be inverted. The implementation defaults to <mover> when m:barPr/m:pos is missing (since isUnder is only true for m:val === 'bot'), but the spec/issue description indicates 'bot' is the default. Consider treating missing/unknown m:pos@m:val as underbar and only using overbar when m:val === 'top', and update the doc comment accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Muhammad-Nur-Alamsyah-Anwar this is right — we tested in Word, default is underbar.

const base = elements.find((e) => e.name === 'm:e');

const wrapper = doc.createElementNS(MATHML_NS, isUnder ? 'munder' : 'mover');

const baseContent = convertChildren(base?.elements ?? []);
const mrow = doc.createElementNS(MATHML_NS, 'mrow');
mrow.appendChild(baseContent);
wrapper.appendChild(mrow);

const accent = doc.createElementNS(MATHML_NS, 'mo');
accent.setAttribute('stretchy', 'true');
// U+203E = overline, U+0332 = combining low line (underbar)
accent.textContent = isUnder ? '\u0332' : '\u203E';
wrapper.appendChild(accent);

return wrapper;
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
*/
export { convertMathRun } from './math-run.js';
export { convertFraction } from './fraction.js';
export { convertBar } from './bar.js';
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,64 @@ describe('convertOmmlToMathml', () => {
expect(children.some((c) => c.localName === 'mn')).toBe(true); // 1
});
});

describe('m:bar converter', () => {
it('renders overbar (top) as <mover> with U+203E', () => {
const omml = {
name: 'm:oMath',
elements: [{
name: 'm:bar',
elements: [
{ name: 'm:barPr', elements: [{ name: 'm:pos', attributes: { 'm:val': 'top' } }] },
{ name: 'm:e', elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'x' }] }] }] },
],
}],
};
const result = convertOmmlToMathml(omml, doc);
expect(result).not.toBeNull();
const mover = result!.querySelector('mover');
expect(mover).not.toBeNull();
expect(mover!.firstElementChild!.textContent).toBe('x');
const mo = mover!.querySelector('mo');
expect(mo?.textContent).toBe('\u203E');
});

it('renders underbar (bot) as <munder> with U+0332', () => {
const omml = {
name: 'm:oMath',
elements: [{
name: 'm:bar',
elements: [
{ name: 'm:barPr', elements: [{ name: 'm:pos', attributes: { 'm:val': 'bot' } }] },
{ name: 'm:e', elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'y' }] }] }] },
],
}],
};
const result = convertOmmlToMathml(omml, doc);
expect(result).not.toBeNull();
const munder = result!.querySelector('munder');
expect(munder).not.toBeNull();
expect(munder!.firstElementChild!.textContent).toBe('y');
const mo = munder!.querySelector('mo');
expect(mo?.textContent).toBe('\u0332');
});

it('defaults to underbar when m:barPr is missing (matches Word behavior)', () => {
const omml = {
name: 'm:oMath',
elements: [{
name: 'm:bar',
elements: [
{ name: 'm:e', elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'z' }] }] }] },
],
}],
};
const result = convertOmmlToMathml(omml, doc);
expect(result).not.toBeNull();
const munder = result!.querySelector('munder');
expect(munder).not.toBeNull();
expect(munder!.firstElementChild!.textContent).toBe('z');
const mo = munder!.querySelector('mo');
expect(mo?.textContent).toBe('\u0332');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import type { OmmlJsonNode, MathObjectConverter } from './types.js';
import { convertMathRun, convertFraction } from './converters/index.js';
import { convertMathRun, convertFraction, convertBar } from './converters/index.js';

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

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

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