Cleanup MathML <mrow>, <mtext>, <mn> (#1338)

* Avoid unnecessary <mrow> wrapping

buildMathML gains two helpers:
* `makeRow` helper wraps an array of nodes in `<mrow>`,
  unless the array has length 1, in which case no wrapping is necessary.
* `buildExpressionRow` for common case of `makeRow(buildExpression(...))`

* Combine adjacent <mtext>s in all cases

No more need for `makeTextRow` helper or anything fancy in text MathML handler.

* Concatenate <mn>s and decimal point into single <mn>

Fix #203

* Fix snapshots
This commit is contained in:
Erik Demaine
2018-05-21 22:56:34 -04:00
committed by GitHub
parent ef9cd5c172
commit 485c509879
8 changed files with 104 additions and 113 deletions

View File

@@ -28,32 +28,15 @@ export const makeText = function(text, mode) {
return new mathMLTree.TextNode(text);
};
export const makeTextRow = function(body, options) {
// Convert each element of the body into MathML, and combine consecutive
// <mtext> outputs into a single <mtext> tag. In this way, we don't
// nest non-text items (e.g., $nested-math$) within an <mtext>.
const inner = [];
let currentText = null;
for (let i = 0; i < body.length; i++) {
const group = buildGroup(body[i], options);
if (group.type === 'mtext' && currentText !== null) {
Array.prototype.push.apply(currentText.children, group.children);
} else {
inner.push(group);
if (group.type === 'mtext') {
currentText = group;
} else {
currentText = null;
}
}
}
// If there is a single tag in the end (presumably <mtext>),
// just return it. Otherwise, wrap them in an <mrow>.
if (inner.length === 1) {
return inner[0];
/**
* Wrap the given array of nodes in an <mrow> node if needed, i.e.,
* unless the array has length 1. Always returns a single node.
*/
export const makeRow = function(body) {
if (body.length === 1) {
return body[0];
} else {
return new mathMLTree.MathNode("mrow", inner);
return new mathMLTree.MathNode("mrow", body);
}
};
@@ -97,11 +80,7 @@ export const getVariant = function(group, options) {
export const groupTypes = {};
groupTypes.ordgroup = function(group, options) {
const inner = buildExpression(group.value, options);
const node = new mathMLTree.MathNode("mrow", inner);
return node;
return buildExpressionRow(group.value, options);
};
groupTypes.supsub = function(group, options) {
@@ -119,18 +98,15 @@ groupTypes.supsub = function(group, options) {
}
}
const removeUnnecessaryRow = true;
const children = [
buildGroup(group.value.base, options, removeUnnecessaryRow)];
buildGroup(group.value.base, options)];
if (group.value.sub) {
children.push(
buildGroup(group.value.sub, options, removeUnnecessaryRow));
children.push(buildGroup(group.value.sub, options));
}
if (group.value.sup) {
children.push(
buildGroup(group.value.sup, options, removeUnnecessaryRow));
children.push(buildGroup(group.value.sup, options));
}
let nodeType;
@@ -167,11 +143,11 @@ groupTypes.supsub = function(group, options) {
groupTypes.tag = function(group, options) {
const table = new mathMLTree.MathNode("mtable", [
new mathMLTree.MathNode("mlabeledtr", [
new mathMLTree.MathNode("mtd",
buildExpression(group.value.tag, options)),
new mathMLTree.MathNode("mtd", [
new mathMLTree.MathNode("mrow",
buildExpression(group.value.body, options)),
buildExpressionRow(group.value.tag, options),
]),
new mathMLTree.MathNode("mtd", [
buildExpressionRow(group.value.body, options),
]),
]),
]);
@@ -181,14 +157,30 @@ groupTypes.tag = function(group, options) {
/**
* Takes a list of nodes, builds them, and returns a list of the generated
* MathML nodes. A little simpler than the HTML version because we don't do any
* previous-node handling.
* MathML nodes. Also combine consecutive <mtext> outputs into a single
* <mtext> tag.
*/
export const buildExpression = function(expression, options) {
const groups = [];
let lastGroup;
for (let i = 0; i < expression.length; i++) {
const group = expression[i];
groups.push(buildGroup(group, options));
const group = buildGroup(expression[i], options);
// Concatenate adjacent <mtext>s
if (group.type === 'mtext' && lastGroup && lastGroup.type === 'mtext') {
lastGroup.children.push(...group.children);
// Concatenate adjacent <mn>s
} else if (group.type === 'mn' &&
lastGroup && lastGroup.type === 'mn') {
lastGroup.children.push(...group.children);
// Concatenate <mn>...</mn> followed by <mi>.</mi>
} else if (group.type === 'mi' && group.children.length === 1 &&
group.children[0].text === '.' &&
lastGroup && lastGroup.type === 'mn') {
lastGroup.children.push(...group.children);
} else {
groups.push(group);
lastGroup = group;
}
}
// TODO(kevinb): combine \\not with mrels and mords
@@ -196,13 +188,19 @@ export const buildExpression = function(expression, options) {
return groups;
};
/**
* Equivalent to buildExpression, but wraps the elements in an <mrow>
* if there's more than one. Returns a single node instead of an array.
*/
export const buildExpressionRow = function(expression, options) {
return makeRow(buildExpression(expression, options));
};
/**
* Takes a group from the parser and calls the appropriate groupTypes function
* on it to produce a MathML node.
*/
export const buildGroup = function(
group, options, removeUnnecessaryRow = false,
) {
export const buildGroup = function(group, options) {
if (!group) {
return new mathMLTree.MathNode("mrow");
}
@@ -210,11 +208,6 @@ export const buildGroup = function(
if (groupTypes[group.type]) {
// Call the groupTypes function
const result = groupTypes[group.type](group, options);
if (removeUnnecessaryRow) {
if (result.type === "mrow" && result.children.length === 1) {
return result.children[0];
}
}
return result;
} else {
throw new ParseError(
@@ -234,7 +227,7 @@ export default function buildMathML(tree, texExpression, options) {
const expression = buildExpression(tree, options);
// Wrap up the expression in an mrow so it is presented in the semantics
// tag correctly.
// tag correctly, unless it's a single <mrow> or <mtable>.
let wrapper;
if (expression.length === 1 &&
utils.contains(["mrow", "mtable"], expression[0].type)) {

View File

@@ -261,9 +261,7 @@ defineFunction({
inner.push(rightNode);
}
const outerNode = new mathMLTree.MathNode("mrow", inner);
return outerNode;
return mml.makeRow(inner);
},
});

View File

@@ -243,9 +243,7 @@ defineFunction({
withDelims.push(rightOp);
}
const outerNode = new mathMLTree.MathNode("mrow", withDelims);
return outerNode;
return mml.makeRow(withDelims);
}
return node;

View File

@@ -1,7 +1,6 @@
// @flow
import defineFunction, {ordargument} from "../defineFunction";
import buildCommon from "../buildCommon";
import mathMLTree from "../mathMLTree";
import {assertNodeType} from "../ParseNode";
import * as html from "../buildHTML";
@@ -35,8 +34,7 @@ defineFunction({
return new buildCommon.makeAnchor(href, [], elements, options);
},
mathmlBuilder: (group, options) => {
const inner = mml.buildExpression(group.value.body, options);
const math = new mathMLTree.MathNode("mrow", inner);
const math = mml.buildExpressionRow(group.value.body, options);
math.setAttribute("href", group.value.href);
return math;
},

View File

@@ -1,7 +1,6 @@
// @flow
import defineFunction, {ordargument} from "../defineFunction";
import buildCommon from "../buildCommon";
import mathMLTree from "../mathMLTree";
import Style from "../Style";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
@@ -47,11 +46,6 @@ defineFunction({
},
mathmlBuilder: (group, options) => {
const body = chooseMathStyle(group, options);
const elements = mml.buildExpression(
body,
options,
false
);
return new mathMLTree.MathNode("mrow", elements);
return mml.buildExpressionRow(body, options);
},
});

View File

@@ -62,6 +62,6 @@ defineFunction({
return buildCommon.makeSpan(["mord", "text"], inner, newOptions);
},
mathmlBuilder(group, options) {
return mml.makeTextRow(group.value.body, options);
return mml.buildExpressionRow(group.value.body, options);
},
});

View File

@@ -57,6 +57,35 @@ exports[`A MathML builder accents turn into <mover accent="true"> in MathML 1`]
`;
exports[`A MathML builder should concatenate digits into single <mn> 1`] = `
<math>
<semantics>
<mrow>
<mi>
sin
</mi>
<mo>
</mo>
<mi>
α
</mi>
<mo>
=
</mo>
<mn>
0.34
</mn>
</mrow>
<annotation encoding="application/x-tex">
\\sin{\\alpha}=0.34
</annotation>
</semantics>
</math>
`;
exports[`A MathML builder should generate <mphantom> nodes for \\phantom 1`] = `
<math>
@@ -87,11 +116,9 @@ exports[`A MathML builder should generate the right types of nodes 1`] = `
<mo>
</mo>
<mrow>
<mi>
x
</mi>
</mrow>
<mi>
x
</mi>
<mo>
+
</mo>
@@ -339,11 +366,9 @@ exports[`A MathML builder should render mathchoice as if there was nothing 3`] =
<mi>
x
</mi>
<mrow>
<mi>
T
</mi>
</mrow>
<mi>
T
</mi>
</msub>
</mrow>
<annotation encoding="application/x-tex">
@@ -367,11 +392,9 @@ exports[`A MathML builder should render mathchoice as if there was nothing 4`] =
<mi>
y
</mi>
<mrow>
<mi>
T
</mi>
</mrow>
<mi>
T
</mi>
</msub>
</msub>
</mrow>
@@ -387,8 +410,8 @@ exports[`A MathML builder should set href attribute for href appropriately 1`] =
<math>
<semantics>
<mrow href="http://example.org">
<mi>
<mrow>
<mi href="http://example.org">
α
</mi>
</mrow>
@@ -406,11 +429,9 @@ exports[`A MathML builder should use <menclose> for colorbox 1`] = `
<semantics>
<mrow>
<menclose mathbackground="red">
<mrow>
<mtext>
b
</mtext>
</mrow>
<mtext>
b
</mtext>
</menclose>
</mrow>
<annotation encoding="application/x-tex">
@@ -427,11 +448,9 @@ exports[`A MathML builder should use <mpadded> for raisebox 1`] = `
<semantics>
<mrow>
<mpadded voffset="0.25em">
<mrow>
<mtext>
b
</mtext>
</mrow>
<mtext>
b
</mtext>
</mpadded>
</mrow>
<annotation encoding="application/x-tex">
@@ -507,22 +526,9 @@ exports[`A MathML builder tags use <mlabeledtr> 1`] = `
<mtable side="right">
<mlabeledtr>
<mtd>
<mrow>
<mtext>
(
</mtext>
<mrow>
<mtext>
h
</mtext>
<mtext>
i
</mtext>
</mrow>
<mtext>
)
</mtext>
</mrow>
<mtext>
(hi)
</mtext>
</mtd>
<mtd>
<mrow>

View File

@@ -35,6 +35,10 @@ describe("A MathML builder", function() {
expect(getMathML("\\sin{x}+1\\;\\text{a}")).toMatchSnapshot();
});
it('should concatenate digits into single <mn>', () => {
expect(getMathML("\\sin{\\alpha}=0.34")).toMatchSnapshot();
});
it('should make prime operators into <mo> nodes', () => {
expect(getMathML("f'")).toMatchSnapshot();
});