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,101 @@
import type { MathObjectConverter } from '../types.js';

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

/**
* Convert m:box (box / invisible grouping container) to MathML <mrow>.
*
* OMML structure:
* m:box → m:boxPr (optional), m:e (content)
*
* MathML output:
* <mrow> content </mrow>
*
* The box is purely a grouping mechanism with no visual rendering;
* it maps directly to MathML's <mrow>.
*
* @spec ECMA-376 §22.1.2.13
*/
export const convertBox: MathObjectConverter = (node, doc, convertChildren) => {
const elements = node.elements ?? [];
const base = elements.find((e) => e.name === 'm:e');

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

return mrow.childNodes.length > 0 ? mrow : null;
};

/**
* Convert m:borderBox (bordered box) to MathML <menclose>.
*
* OMML structure:
* m:borderBox → m:borderBoxPr (optional: m:hideTop, m:hideBot, m:hideLeft, m:hideRight,
* m:strikeBLTR, m:strikeH, m:strikeTLBR, m:strikeV),
* m:e (content)
*
* MathML output:
* <menclose notation="..."> content </menclose>
*
* By default all four borders are shown (notation="box"). Individual borders
* can be hidden via m:hide* flags, and diagonal/horizontal/vertical strikes
* can be added via m:strike* flags.
*
* @spec ECMA-376 §22.1.2.11
*/
export const convertBorderBox: MathObjectConverter = (node, doc, convertChildren) => {
const elements = node.elements ?? [];
const props = elements.find((e) => e.name === 'm:borderBoxPr');
const base = elements.find((e) => e.name === 'm:e');

/** OOXML ST_OnOff true values: "1", "on", "true", or boolean-flag presence. */
const isOn = (el?: { attributes?: Record<string, string> }) =>
el &&
(el.attributes?.['m:val'] === '1' ||
el.attributes?.['m:val'] === 'on' ||
el.attributes?.['m:val'] === 'true' ||
!el.attributes);

const hideTop = props?.elements?.find((e) => e.name === 'm:hideTop');
const hideBot = props?.elements?.find((e) => e.name === 'm:hideBot');
const hideLeft = props?.elements?.find((e) => e.name === 'm:hideLeft');
const hideRight = props?.elements?.find((e) => e.name === 'm:hideRight');
const strikeBLTR = props?.elements?.find((e) => e.name === 'm:strikeBLTR');
const strikeH = props?.elements?.find((e) => e.name === 'm:strikeH');
const strikeTLBR = props?.elements?.find((e) => e.name === 'm:strikeTLBR');
const strikeV = props?.elements?.find((e) => e.name === 'm:strikeV');

const notations: string[] = [];

const allHidden = isOn(hideTop) && isOn(hideBot) && isOn(hideLeft) && isOn(hideRight);

if (!allHidden) {
if (!isOn(hideTop) && !isOn(hideBot) && !isOn(hideLeft) && !isOn(hideRight)) {
notations.push('box');
} else {
if (!isOn(hideTop)) notations.push('top');
if (!isOn(hideBot)) notations.push('bottom');
if (!isOn(hideLeft)) notations.push('left');
if (!isOn(hideRight)) notations.push('right');
}
}

if (isOn(strikeBLTR)) notations.push('updiagonalstrike');
if (isOn(strikeH)) notations.push('horizontalstrike');
if (isOn(strikeTLBR)) notations.push('downdiagonalstrike');
if (isOn(strikeV)) notations.push('verticalstrike');

const content = convertChildren(base?.elements ?? []);

if (notations.length === 0) {
const mrow = doc.createElementNS(MATHML_NS, 'mrow');
mrow.appendChild(content);
return mrow;
}

const menclose = doc.createElementNS(MATHML_NS, 'menclose');
menclose.setAttribute('notation', notations.join(' '));
menclose.appendChild(content);

return menclose;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export { convertFunction } from './function.js';
export { convertSubscript } from './subscript.js';
export { convertSuperscript } from './superscript.js';
export { convertSubSuperscript } from './sub-superscript.js';
export { convertBox, convertBorderBox } from './box.js';
Original file line number Diff line number Diff line change
Expand Up @@ -927,3 +927,152 @@ describe('m:func converter', () => {
expect(mis[1]!.textContent).toBe('cos');
});
});

describe('m:box converter', () => {
it('converts m:box to <mrow>', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:box',
elements: [
{
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();
expect(result!.textContent).toBe('x');
});

it('returns null for empty m:box', () => {
const omml = {
name: 'm:oMath',
elements: [{ name: 'm:box', elements: [] }],
};
const result = convertOmmlToMathml(omml, doc);
expect(result).toBeNull();
});
});

describe('m:borderBox converter', () => {
it('converts m:borderBox to <menclose notation="box"> by default', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:borderBox',
elements: [
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'E' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
expect(result).not.toBeNull();
const menclose = result!.querySelector('menclose');
expect(menclose).not.toBeNull();
expect(menclose!.getAttribute('notation')).toBe('box');
expect(menclose!.textContent).toBe('E');
});

it('hides individual sides when hide flags are set', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:borderBox',
elements: [
{
name: 'm:borderBoxPr',
elements: [
{ name: 'm:hideTop', attributes: { 'm:val': '1' } },
{ name: 'm:hideBot', attributes: { 'm:val': '1' } },
],
},
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'x' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
const menclose = result!.querySelector('menclose');
expect(menclose).not.toBeNull();
const notation = menclose!.getAttribute('notation')!;
expect(notation).toContain('left');
expect(notation).toContain('right');
expect(notation).not.toContain('top');
expect(notation).not.toContain('bottom');
});

it('adds strike notations', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:borderBox',
elements: [
{
name: 'm:borderBoxPr',
elements: [
{ name: 'm:hideTop', attributes: { 'm:val': '1' } },
{ name: 'm:hideBot', attributes: { 'm:val': '1' } },
{ name: 'm:hideLeft', attributes: { 'm:val': '1' } },
{ name: 'm:hideRight', attributes: { 'm:val': '1' } },
{ name: 'm:strikeH', attributes: { 'm:val': '1' } },
],
},
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'y' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
const menclose = result!.querySelector('menclose');
expect(menclose).not.toBeNull();
expect(menclose!.getAttribute('notation')).toBe('horizontalstrike');
});

it('falls back to <mrow> when all borders hidden and no strikes', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:borderBox',
elements: [
{
name: 'm:borderBoxPr',
elements: [
{ name: 'm:hideTop', attributes: { 'm:val': '1' } },
{ name: 'm:hideBot', attributes: { 'm:val': '1' } },
{ name: 'm:hideLeft', attributes: { 'm:val': '1' } },
{ name: 'm:hideRight', attributes: { 'm:val': '1' } },
],
},
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'q' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
const menclose = result!.querySelector('menclose');
expect(menclose).toBeNull();
expect(result!.textContent).toBe('q');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
convertSubscript,
convertSuperscript,
convertSubSuperscript,
convertBox,
convertBorderBox,
} from './converters/index.js';

export const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
Expand Down Expand Up @@ -47,8 +49,8 @@ const MATH_OBJECT_REGISTRY: Record<string, MathObjectConverter | null> = {

// ── Not yet implemented (community contributions welcome) ────────────────
'm:acc': null, // Accent (diacritical mark above base)
'm:borderBox': null, // Border box (border around math content)
'm:box': null, // Box (invisible grouping container)
'm:borderBox': convertBorderBox, // Border box (border around math content)
'm:box': convertBox, // Box (invisible grouping container)
'm:d': null, // Delimiter (parentheses, brackets, braces)
'm:eqArr': null, // Equation array (vertical array of equations)
'm:groupChr': null, // Group character (overbrace, underbrace)
Expand Down