Skip to content

Commit 965af10

Browse files
feat(math): implement m:d delimiter converter (SD-2380) (#2710)
* feat(math): implement m:d delimiter converter (SD-2380) * fix(math): address delimiter review and sync converters * fix(math): address delimiter review feedback * docs(math): clarify delimiter separator spec * test(math): add behavior tests for m:d delimiter rendering Add 5 behavior tests exercising the delimiter converter with a dedicated test document covering all 21 ECMA-376 §22.1.2.24 compliance cases: default parentheses, U+2502 separator, suppressed delimiters (chr without m:val), custom Unicode delimiters (floor, ceiling, abs), and nesting. Fix stale comment that still referred to delimiter as unimplemented. * refactor(math): inline createExpressionGroup into single loop Replace map + filter + forEach chain with a single for-loop that builds expression groups directly, removing the single-use helper. --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com>
1 parent 06aeef5 commit 965af10

6 files changed

Lines changed: 769 additions & 3 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { MathObjectConverter, OmmlJsonNode } from '../types.js';
2+
3+
const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
4+
5+
const DEFAULT_BEGIN_DELIMITER = '(';
6+
const DEFAULT_END_DELIMITER = ')';
7+
const DEFAULT_SEPARATOR_DELIMITER = '\u2502'; // ECMA-376 22.1.2.95: BOX DRAWINGS LIGHT VERTICAL
8+
9+
function getDelimiterValue(properties: OmmlJsonNode | undefined, name: string, fallback: string): string {
10+
const property = properties?.elements?.find((element) => element.name === name);
11+
if (!property) return fallback;
12+
return property.attributes?.['m:val'] ?? '';
13+
}
14+
15+
/**
16+
* Convert m:d (delimiter) to MathML.
17+
*
18+
* OMML structure:
19+
* m:d → m:dPr (optional: begChr, endChr, sepChr) + one or more m:e expressions
20+
*
21+
* MathML output:
22+
* <mrow><mo>(</mo> ...content... <mo>)</mo></mrow>
23+
*
24+
* @spec ECMA-376 §22.1.2.24
25+
*/
26+
export const convertDelimiter: MathObjectConverter = (node, doc, convertChildren) => {
27+
const elements = node.elements ?? [];
28+
const delimiterProps = elements.find((element) => element.name === 'm:dPr');
29+
const expressions = elements.filter((element) => element.name === 'm:e');
30+
31+
const beginDelimiter = getDelimiterValue(delimiterProps, 'm:begChr', DEFAULT_BEGIN_DELIMITER);
32+
const endDelimiter = getDelimiterValue(delimiterProps, 'm:endChr', DEFAULT_END_DELIMITER);
33+
const separatorDelimiter = getDelimiterValue(delimiterProps, 'm:sepChr', DEFAULT_SEPARATOR_DELIMITER);
34+
35+
const wrapper = doc.createElementNS(MATHML_NS, 'mrow');
36+
37+
const begin = doc.createElementNS(MATHML_NS, 'mo');
38+
begin.textContent = beginDelimiter;
39+
wrapper.appendChild(begin);
40+
41+
let renderedCount = 0;
42+
for (const expression of expressions) {
43+
const fragment = convertChildren(expression?.elements ?? []);
44+
if (fragment.childNodes.length === 0) continue;
45+
46+
if (renderedCount > 0) {
47+
const separator = doc.createElementNS(MATHML_NS, 'mo');
48+
separator.textContent = separatorDelimiter;
49+
wrapper.appendChild(separator);
50+
}
51+
52+
const group = doc.createElementNS(MATHML_NS, 'mrow');
53+
group.appendChild(fragment);
54+
wrapper.appendChild(group);
55+
renderedCount++;
56+
}
57+
58+
const end = doc.createElementNS(MATHML_NS, 'mo');
59+
end.textContent = endDelimiter;
60+
wrapper.appendChild(end);
61+
62+
return wrapper;
63+
};

packages/layout-engine/painters/dom/src/features/math/converters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { convertMathRun } from './math-run.js';
1010
export { convertFraction } from './fraction.js';
1111
export { convertBar } from './bar.js';
1212
export { convertFunction } from './function.js';
13+
export { convertDelimiter } from './delimiter.js';
1314
export { convertSubscript } from './subscript.js';
1415
export { convertSuperscript } from './superscript.js';
1516
export { convertSubSuperscript } from './sub-superscript.js';

0 commit comments

Comments
 (0)