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 @@ -21,3 +21,4 @@ export { convertRadical } from './radical.js';
export { convertLowerLimit } from './lower-limit.js';
export { convertUpperLimit } from './upper-limit.js';
export { convertNary } from './nary.js';
export { convertPhantom } from './phantom.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { MathObjectConverter } from '../types.js';

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

/**
* Convert m:phant (phantom) to MathML <mphantom> or styled <mpadded>.
*
* OMML structure:
* m:phant → m:phantPr (optional: m:show, m:zeroWid, m:zeroAsc, m:zeroDesc), m:e (content)
*
* MathML output:
* Full phantom (default): <mphantom> content </mphantom>
* Visible with zeroed dimensions: <mpadded> with width/height/depth="0"
*
* A phantom reserves the space its content would occupy but renders invisibly.
* Property flags can zero-out individual dimensions or force visibility.
*
* @spec ECMA-376 §22.1.2.81
*/
export const convertPhantom: MathObjectConverter = (node, doc, convertChildren) => {
const elements = node.elements ?? [];
const phantPr = elements.find((e) => e.name === 'm:phantPr');
const base = elements.find((e) => e.name === 'm:e');

const show = phantPr?.elements?.find((e) => e.name === 'm:show');
const zeroWid = phantPr?.elements?.find((e) => e.name === 'm:zeroWid');
const zeroAsc = phantPr?.elements?.find((e) => e.name === 'm:zeroAsc');
const zeroDesc = phantPr?.elements?.find((e) => e.name === 'm:zeroDesc');

/** OOXML ST_OnOff true values. */
const isOnOffTrue = (val?: string) => val === '1' || val === 'on' || val === 'true';

// Per ECMA-376 §22.1.2.96: when m:show is omitted, the base is shown.
const isVisible = show == null || !show.attributes || isOnOffTrue(show.attributes['m:val']);
const hasZeroDimension = zeroWid || zeroAsc || zeroDesc;

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

if (!isVisible && !hasZeroDimension) {
const mphantom = doc.createElementNS(MATHML_NS, 'mphantom');
mphantom.appendChild(content);
return mphantom;
}

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

const isZeroVal = (el?: typeof zeroWid) => el && (isOnOffTrue(el.attributes?.['m:val']) || !el.attributes);

if (isZeroVal(zeroWid)) mpadded.setAttribute('width', '0');
if (isZeroVal(zeroAsc)) mpadded.setAttribute('height', '0');
if (isZeroVal(zeroDesc)) mpadded.setAttribute('depth', '0');

if (!isVisible) {
const mphantom = doc.createElementNS(MATHML_NS, 'mphantom');
mphantom.appendChild(content);
mpadded.appendChild(mphantom);
} else {
mpadded.appendChild(content);
}

return mpadded;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3292,3 +3292,164 @@ describe('m:nary converter', () => {
expect(mo!.textContent).toBe('');
});
});

describe('m:phant converter', () => {
it('renders phantom with no properties as visible (m:show default)', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:phant',
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!.querySelector('mphantom')).toBeNull();
expect(result!.textContent).toBe('x');
});

it('hides content when m:show has m:val="0"', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:phant',
elements: [
{
name: 'm:phantPr',
elements: [{ name: 'm:show', attributes: { 'm:val': '0' } }],
},
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'x' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
const mphantom = result!.querySelector('mphantom');
expect(mphantom).not.toBeNull();
expect(mphantom!.textContent).toBe('x');
});

it('converts visible phantom with zeroed width to <mpadded width="0">', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:phant',
elements: [
{
name: 'm:phantPr',
elements: [
{ name: 'm:show', attributes: { 'm:val': '1' } },
{ name: 'm:zeroWid', 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 mpadded = result!.querySelector('mpadded');
expect(mpadded).not.toBeNull();
expect(mpadded!.getAttribute('width')).toBe('0');
expect(mpadded!.textContent).toBe('y');
});

it('treats bare <m:show/> (no attributes) as visible', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:phant',
elements: [
{
name: 'm:phantPr',
elements: [{ name: 'm:show' }, { name: 'm:zeroWid', attributes: { 'm:val': '1' } }],
},
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'v' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
const mpadded = result!.querySelector('mpadded');
expect(mpadded).not.toBeNull();
expect(mpadded!.getAttribute('width')).toBe('0');
const mphantom = mpadded!.querySelector('mphantom');
expect(mphantom).toBeNull();
expect(mpadded!.textContent).toBe('v');
});

it('renders visible phantom with zeroed ascent as <mpadded height="0"> without hiding', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:phant',
elements: [
{
name: 'm:phantPr',
elements: [{ name: 'm:zeroAsc', attributes: { 'm:val': '1' } }],
},
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'z' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
const mpadded = result!.querySelector('mpadded');
expect(mpadded).not.toBeNull();
expect(mpadded!.getAttribute('height')).toBe('0');
expect(mpadded!.querySelector('mphantom')).toBeNull();
expect(mpadded!.textContent).toBe('z');
});

it('renders invisible phantom with m:show="0" and zeroed height as <mpadded> wrapping <mphantom>', () => {
const omml = {
name: 'm:oMath',
elements: [
{
name: 'm:phant',
elements: [
{
name: 'm:phantPr',
elements: [
{ name: 'm:show', attributes: { 'm:val': '0' } },
{ name: 'm:zeroAsc', attributes: { 'm:val': '1' } },
],
},
{
name: 'm:e',
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'z' }] }] }],
},
],
},
],
};
const result = convertOmmlToMathml(omml, doc);
const mpadded = result!.querySelector('mpadded');
expect(mpadded).not.toBeNull();
expect(mpadded!.getAttribute('height')).toBe('0');
expect(mpadded!.querySelector('mphantom')).not.toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
convertLowerLimit,
convertUpperLimit,
convertNary,
convertPhantom,
} from './converters/index.js';

export const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
Expand Down Expand Up @@ -55,6 +56,7 @@ const MATH_OBJECT_REGISTRY: Record<string, MathObjectConverter | null> = {
'm:limLow': convertLowerLimit, // Lower limit (e.g., lim)
'm:limUpp': convertUpperLimit, // Upper limit
'm:nary': convertNary, // N-ary operator (integral, summation, product)
'm:phant': convertPhantom, // Phantom (invisible spacing placeholder)
'm:rad': convertRadical, // Radical (square root, nth root)
'm:sSub': convertSubscript, // Subscript
'm:sSup': convertSuperscript, // Superscript
Expand All @@ -66,7 +68,6 @@ const MATH_OBJECT_REGISTRY: Record<string, MathObjectConverter | null> = {
'm:box': null, // Box (invisible grouping container)
'm:groupChr': null, // Group character (overbrace, underbrace)
'm:m': null, // Matrix (grid of elements)
'm:phant': null, // Phantom (invisible spacing placeholder)
};

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