Skip to content
Open
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>&#x2015;</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';

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+2015 = horizontal bar (underbar)
accent.textContent = isUnder ? '\u2015' : '\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,79 @@ 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+2015', () => {
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('\u2015');
});

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('\u2015');
});
});
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,15 +30,15 @@ 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)
'm:f': convertFraction, // Fraction (numerator/denominator)

// ── 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)
'm:eqArr': null, // Equation array (vertical array of equations)
'm:f': convertFraction, // Fraction (numerator/denominator)
'm:func': null, // Function apply (sin, cos, log, etc.)
'm:groupChr': null, // Group character (overbrace, underbrace)
'm:limLow': null, // Lower limit (e.g., lim)
Expand Down
Loading