Skip to content

Commit ffdbc40

Browse files
authored
fix(math): use U+2015 for underbar accent instead of U+0332 (SD-2403) (#2621)
U+0332 (combining low line) renders invisibly in some browsers when used as a standalone MathML operator. Replace with U+2015 (horizontal bar) which renders consistently. Also moves m:f to the "Implemented" section in the math object registry.
1 parent 89b59db commit ffdbc40

3 files changed

Lines changed: 42 additions & 27 deletions

File tree

β€Žpackages/layout-engine/painters/dom/src/features/math/converters/bar.tsβ€Ž

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
1010
*
1111
* MathML output:
1212
* top: <mover> <mrow>base</mrow> <mo>&#x203E;</mo> </mover>
13-
* bot (default): <munder> <mrow>base</mrow> <mo>&#x0332;</mo> </munder>
13+
* bot (default): <munder> <mrow>base</mrow> <mo>&#x2015;</mo> </munder>
1414
*
1515
* Word renders an underbar when no position is specified, so the default is "bot".
1616
*
@@ -35,8 +35,8 @@ export const convertBar: MathObjectConverter = (node, doc, convertChildren) => {
3535

3636
const accent = doc.createElementNS(MATHML_NS, 'mo');
3737
accent.setAttribute('stretchy', 'true');
38-
// U+203E = overline, U+0332 = combining low line (underbar)
39-
accent.textContent = isUnder ? '\u0332' : '\u203E';
38+
// U+203E = overline, U+2015 = horizontal bar (underbar)
39+
accent.textContent = isUnder ? '\u2015' : '\u203E';
4040
wrapper.appendChild(accent);
4141

4242
return wrapper;

β€Žpackages/layout-engine/painters/dom/src/features/math/omml-to-mathml.test.tsβ€Ž

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,18 @@ describe('m:bar converter', () => {
209209
it('renders overbar (top) as <mover> with U+203E', () => {
210210
const omml = {
211211
name: 'm:oMath',
212-
elements: [{
213-
name: 'm:bar',
214-
elements: [
215-
{ name: 'm:barPr', elements: [{ name: 'm:pos', attributes: { 'm:val': 'top' } }] },
216-
{ name: 'm:e', elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'x' }] }] }] },
217-
],
218-
}],
212+
elements: [
213+
{
214+
name: 'm:bar',
215+
elements: [
216+
{ name: 'm:barPr', elements: [{ name: 'm:pos', attributes: { 'm:val': 'top' } }] },
217+
{
218+
name: 'm:e',
219+
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'x' }] }] }],
220+
},
221+
],
222+
},
223+
],
219224
};
220225
const result = convertOmmlToMathml(omml, doc);
221226
expect(result).not.toBeNull();
@@ -226,42 +231,52 @@ describe('m:bar converter', () => {
226231
expect(mo?.textContent).toBe('\u203E');
227232
});
228233

229-
it('renders underbar (bot) as <munder> with U+0332', () => {
234+
it('renders underbar (bot) as <munder> with U+2015', () => {
230235
const omml = {
231236
name: 'm:oMath',
232-
elements: [{
233-
name: 'm:bar',
234-
elements: [
235-
{ name: 'm:barPr', elements: [{ name: 'm:pos', attributes: { 'm:val': 'bot' } }] },
236-
{ name: 'm:e', elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'y' }] }] }] },
237-
],
238-
}],
237+
elements: [
238+
{
239+
name: 'm:bar',
240+
elements: [
241+
{ name: 'm:barPr', elements: [{ name: 'm:pos', attributes: { 'm:val': 'bot' } }] },
242+
{
243+
name: 'm:e',
244+
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'y' }] }] }],
245+
},
246+
],
247+
},
248+
],
239249
};
240250
const result = convertOmmlToMathml(omml, doc);
241251
expect(result).not.toBeNull();
242252
const munder = result!.querySelector('munder');
243253
expect(munder).not.toBeNull();
244254
expect(munder!.firstElementChild!.textContent).toBe('y');
245255
const mo = munder!.querySelector('mo');
246-
expect(mo?.textContent).toBe('\u0332');
256+
expect(mo?.textContent).toBe('\u2015');
247257
});
248258

249259
it('defaults to underbar when m:barPr is missing (matches Word behavior)', () => {
250260
const omml = {
251261
name: 'm:oMath',
252-
elements: [{
253-
name: 'm:bar',
254-
elements: [
255-
{ name: 'm:e', elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'z' }] }] }] },
256-
],
257-
}],
262+
elements: [
263+
{
264+
name: 'm:bar',
265+
elements: [
266+
{
267+
name: 'm:e',
268+
elements: [{ name: 'm:r', elements: [{ name: 'm:t', elements: [{ type: 'text', text: 'z' }] }] }],
269+
},
270+
],
271+
},
272+
],
258273
};
259274
const result = convertOmmlToMathml(omml, doc);
260275
expect(result).not.toBeNull();
261276
const munder = result!.querySelector('munder');
262277
expect(munder).not.toBeNull();
263278
expect(munder!.firstElementChild!.textContent).toBe('z');
264279
const mo = munder!.querySelector('mo');
265-
expect(mo?.textContent).toBe('\u0332');
280+
expect(mo?.textContent).toBe('\u2015');
266281
});
267282
});

β€Žpackages/layout-engine/painters/dom/src/features/math/omml-to-mathml.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ const MATH_OBJECT_REGISTRY: Record<string, MathObjectConverter | null> = {
3131
// ── Implemented ──────────────────────────────────────────────────────────
3232
'm:r': convertMathRun,
3333
'm:bar': convertBar, // Bar (overbar/underbar)
34+
'm:f': convertFraction, // Fraction (numerator/denominator)
3435

3536
// ── Not yet implemented (community contributions welcome) ────────────────
3637
'm:acc': null, // Accent (diacritical mark above base)
3738
'm:borderBox': null, // Border box (border around math content)
3839
'm:box': null, // Box (invisible grouping container)
3940
'm:d': null, // Delimiter (parentheses, brackets, braces)
4041
'm:eqArr': null, // Equation array (vertical array of equations)
41-
'm:f': convertFraction, // Fraction (numerator/denominator)
4242
'm:func': null, // Function apply (sin, cos, log, etc.)
4343
'm:groupChr': null, // Group character (overbrace, underbrace)
4444
'm:limLow': null, // Lower limit (e.g., lim)

0 commit comments

Comments
Β (0)