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
Expand Up @@ -10,3 +10,4 @@ export { convertMathRun } from './math-run.js';
export { convertFraction } from './fraction.js';
export { convertBar } from './bar.js';
export { convertSubscript } from './subscript.js';
export { convertSuperscript } from './superscript.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { MathObjectConverter } from '../types.js';

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

/**
* Convert m:sSup (superscript) to MathML <msup>.
*
* OMML structure:
* m:sSup → m:sSupPr (optional), m:e (base), m:sup (superscript)
*
* MathML output:
* <msup> <mrow>base</mrow> <mrow>sup</mrow> </msup>
*
* @spec ECMA-376 §22.1.2.105
*/
export const convertSuperscript: MathObjectConverter = (node, doc, convertChildren) => {
const elements = node.elements ?? [];
const base = elements.find((e) => e.name === 'm:e');
const sup = elements.find((e) => e.name === 'm:sup');

const msup = doc.createElementNS(MATHML_NS, 'msup');

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

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

return msup;
};
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,118 @@ describe('m:sSub converter', () => {
expect(msub!.children[0]!.textContent).toBe('a');
});
});

describe('m:sSup converter', () => {
it('converts m:sSup to <msup> with base and superscript', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:sSup',
elements: [
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'x' }] }] }],
},
{
name: 'm:sup',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: '2' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
expect(result).not.toBeNull();
const msup = result!.querySelector('msup');
expect(msup).not.toBeNull();
expect(msup!.children.length).toBe(2);
expect(msup!.children[0]!.textContent).toBe('x');
expect(msup!.children[1]!.textContent).toBe('2');
});

it('ignores m:sSupPr properties element', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:sSup',
elements: [
{ name: 'm:sSupPr', elements: [{ name: 'm:ctrlPr' }] },
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'a' }] }] }],
},
{
name: 'm:sup',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'b' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
expect(result).not.toBeNull();
const msup = result!.querySelector('msup');
expect(msup).not.toBeNull();
expect(msup!.children.length).toBe(2);
expect(msup!.children[0]!.textContent).toBe('a');
expect(msup!.children[1]!.textContent).toBe('b');
});

it('wraps multi-part base and superscript in <mrow> for valid arity', () => {
// (x+1)^2 — base has 3 runs that must be grouped
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:sSup',
elements: [
{
name: 'm:e',
elements: [
{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'x' }] }] },
{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: '+' }] }] },
{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: '1' }] }] },
],
},
{
name: 'm:sup',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: '2' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
expect(result).not.toBeNull();
const msup = result!.querySelector('msup');
expect(msup).not.toBeNull();
// <msup> must have exactly 2 children (base + superscript), each wrapped in <mrow>
expect(msup!.children.length).toBe(2);
expect(msup!.children[0]!.textContent).toBe('x+1');
expect(msup!.children[1]!.textContent).toBe('2');
});

it('handles missing m:sup gracefully', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:sSup',
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();
const msup = result!.querySelector('msup');
expect(msup).not.toBeNull();
expect(msup!.children[0]!.textContent).toBe('x');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
*/

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

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

Expand All @@ -33,6 +39,7 @@ const MATH_OBJECT_REGISTRY: Record<string, MathObjectConverter | null> = {
'm:bar': convertBar, // Bar (overbar/underbar)
'm:f': convertFraction, // Fraction (numerator/denominator)
'm:sSub': convertSubscript, // Subscript
'm:sSup': convertSuperscript, // Superscript

// ── Not yet implemented (community contributions welcome) ────────────────
'm:acc': null, // Accent (diacritical mark above base)
Expand All @@ -50,7 +57,6 @@ const MATH_OBJECT_REGISTRY: Record<string, MathObjectConverter | null> = {
'm:rad': null, // Radical (square root, nth root)
'm:sPre': null, // Pre-sub-superscript (left of base)
'm:sSubSup': null, // Sub-superscript (both)
'm:sSup': null, // Superscript
};

/** OMML argument/container elements that wrap children in <mrow>. */
Expand Down
Loading