mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-06 19:58:40 +00:00
Fix MathML output for ' and large operators with limits (#788)
Summary: (@kevinbarabash) This diff extracts MathML builder tests out into a separate file and updates them to use jest snapshot testing. It updates the output of prime ' from using identifier nodes <mi> to operator nodes <mo>. It also updates large operators with limits to use munderover instead of msupsub. I added an option to remove unnecessary mrows to buildGroup. Right now it's only used for by groupTypes.supsub. I'll see if it can be used elsewhere (everywhere?) in a follow up PR. Test Plan: - make test w/o errors - verify mathml snapshots contain the desired markup
This commit is contained in:
committed by
Erik Demaine
parent
e00738d16f
commit
fafaf85f96
@@ -28,6 +28,7 @@
|
|||||||
"express": "^4.14.0",
|
"express": "^4.14.0",
|
||||||
"glob": "^7.1.1",
|
"glob": "^7.1.1",
|
||||||
"jest": "^20.0.4",
|
"jest": "^20.0.4",
|
||||||
|
"jest-serializer-html": "^4.0.0",
|
||||||
"js-yaml": "^3.3.1",
|
"js-yaml": "^3.3.1",
|
||||||
"jspngopt": "^0.2.0",
|
"jspngopt": "^0.2.0",
|
||||||
"less": "~2.7.1",
|
"less": "~2.7.1",
|
||||||
@@ -48,6 +49,9 @@
|
|||||||
"match-at": "^0.1.0"
|
"match-at": "^0.1.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"snapshotSerializers": [
|
||||||
|
"jest-serializer-html"
|
||||||
|
],
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
"**/test/*-spec.js"
|
"**/test/*-spec.js"
|
||||||
],
|
],
|
||||||
|
@@ -8,6 +8,7 @@ import buildCommon, { makeSpan, fontMap } from "./buildCommon";
|
|||||||
import fontMetrics from "./fontMetrics";
|
import fontMetrics from "./fontMetrics";
|
||||||
import mathMLTree from "./mathMLTree";
|
import mathMLTree from "./mathMLTree";
|
||||||
import ParseError from "./ParseError";
|
import ParseError from "./ParseError";
|
||||||
|
import Style from "./Style";
|
||||||
import symbols from "./symbols";
|
import symbols from "./symbols";
|
||||||
import utils from "./utils";
|
import utils from "./utils";
|
||||||
import stretchy from "./stretchy";
|
import stretchy from "./stretchy";
|
||||||
@@ -91,6 +92,8 @@ groupTypes.textord = function(group, options) {
|
|||||||
// TODO(kevinb) merge adjacent <mn> nodes
|
// TODO(kevinb) merge adjacent <mn> nodes
|
||||||
// do it as a post processing step
|
// do it as a post processing step
|
||||||
node = new mathMLTree.MathNode("mn", [text]);
|
node = new mathMLTree.MathNode("mn", [text]);
|
||||||
|
} else if (group.value === "\\prime") {
|
||||||
|
node = new mathMLTree.MathNode("mo", [text]);
|
||||||
} else {
|
} else {
|
||||||
node = new mathMLTree.MathNode("mi", [text]);
|
node = new mathMLTree.MathNode("mi", [text]);
|
||||||
}
|
}
|
||||||
@@ -207,14 +210,18 @@ groupTypes.supsub = function(group, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const children = [buildGroup(group.value.base, options)];
|
const removeUnnecessaryRow = true;
|
||||||
|
const children = [
|
||||||
|
buildGroup(group.value.base, options, removeUnnecessaryRow)];
|
||||||
|
|
||||||
if (group.value.sub) {
|
if (group.value.sub) {
|
||||||
children.push(buildGroup(group.value.sub, options));
|
children.push(
|
||||||
|
buildGroup(group.value.sub, options, removeUnnecessaryRow));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group.value.sup) {
|
if (group.value.sup) {
|
||||||
children.push(buildGroup(group.value.sup, options));
|
children.push(
|
||||||
|
buildGroup(group.value.sup, options, removeUnnecessaryRow));
|
||||||
}
|
}
|
||||||
|
|
||||||
let nodeType;
|
let nodeType;
|
||||||
@@ -224,9 +231,14 @@ groupTypes.supsub = function(group, options) {
|
|||||||
nodeType = "msup";
|
nodeType = "msup";
|
||||||
} else if (!group.value.sup) {
|
} else if (!group.value.sup) {
|
||||||
nodeType = "msub";
|
nodeType = "msub";
|
||||||
|
} else {
|
||||||
|
const base = group.value.base;
|
||||||
|
if (base && base.value.limits && options.style === Style.DISPLAY) {
|
||||||
|
nodeType = "munderover";
|
||||||
} else {
|
} else {
|
||||||
nodeType = "msubsup";
|
nodeType = "msubsup";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const node = new mathMLTree.MathNode(nodeType, children);
|
const node = new mathMLTree.MathNode(nodeType, children);
|
||||||
|
|
||||||
@@ -457,7 +469,20 @@ groupTypes.delimsizing = function(group) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
groupTypes.styling = function(group, options) {
|
groupTypes.styling = function(group, options) {
|
||||||
const inner = buildExpression(group.value.value, options);
|
// Figure out what style we're changing to.
|
||||||
|
// TODO(kevinb): dedupe this with buildHTML.js
|
||||||
|
// This will be easier of handling of styling nodes is in the same file.
|
||||||
|
const styleMap = {
|
||||||
|
"display": Style.DISPLAY,
|
||||||
|
"text": Style.TEXT,
|
||||||
|
"script": Style.SCRIPT,
|
||||||
|
"scriptscript": Style.SCRIPTSCRIPT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStyle = styleMap[group.value.style];
|
||||||
|
const newOptions = options.havingStyle(newStyle);
|
||||||
|
|
||||||
|
const inner = buildExpression(group.value.value, newOptions);
|
||||||
|
|
||||||
const node = new mathMLTree.MathNode("mstyle", inner);
|
const node = new mathMLTree.MathNode("mstyle", inner);
|
||||||
|
|
||||||
@@ -643,14 +668,21 @@ const buildExpression = function(expression, options) {
|
|||||||
* Takes a group from the parser and calls the appropriate groupTypes function
|
* Takes a group from the parser and calls the appropriate groupTypes function
|
||||||
* on it to produce a MathML node.
|
* on it to produce a MathML node.
|
||||||
*/
|
*/
|
||||||
const buildGroup = function(group, options) {
|
// TODO(kevinb): determine if removeUnnecessaryRow should always be true
|
||||||
|
const buildGroup = function(group, options, removeUnnecessaryRow = false) {
|
||||||
if (!group) {
|
if (!group) {
|
||||||
return new mathMLTree.MathNode("mrow");
|
return new mathMLTree.MathNode("mrow");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupTypes[group.type]) {
|
if (groupTypes[group.type]) {
|
||||||
// Call the groupTypes function
|
// Call the groupTypes function
|
||||||
return groupTypes[group.type](group, options);
|
const result = groupTypes[group.type](group, options);
|
||||||
|
if (removeUnnecessaryRow) {
|
||||||
|
if (result.type === "mrow" && result.children.length === 1) {
|
||||||
|
return result.children[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
} else {
|
} else {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Got group of unknown type: '" + group.type + "'");
|
"Got group of unknown type: '" + group.type + "'");
|
||||||
|
133
test/__snapshots__/mathml-spec.js.snap
Normal file
133
test/__snapshots__/mathml-spec.js.snap
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`A MathML builder should generate <mphantom> nodes for \\phantom 1`] = `
|
||||||
|
|
||||||
|
<math>
|
||||||
|
<semantics>
|
||||||
|
<mrow>
|
||||||
|
<mphantom>
|
||||||
|
<mi>
|
||||||
|
x
|
||||||
|
</mi>
|
||||||
|
</mphantom>
|
||||||
|
</mrow>
|
||||||
|
<annotation encoding="application/x-tex">
|
||||||
|
\\phantom{x}
|
||||||
|
</annotation>
|
||||||
|
</semantics>
|
||||||
|
</math>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`A MathML builder should generate the right types of nodes 1`] = `
|
||||||
|
|
||||||
|
<math>
|
||||||
|
<semantics>
|
||||||
|
<mrow>
|
||||||
|
<mi>
|
||||||
|
sin
|
||||||
|
</mi>
|
||||||
|
<mrow>
|
||||||
|
<mi>
|
||||||
|
x
|
||||||
|
</mi>
|
||||||
|
</mrow>
|
||||||
|
<mo>
|
||||||
|
+
|
||||||
|
</mo>
|
||||||
|
<mn>
|
||||||
|
1
|
||||||
|
</mn>
|
||||||
|
<mspace width="0.277778em">
|
||||||
|
</mspace>
|
||||||
|
<mtext>
|
||||||
|
a
|
||||||
|
</mtext>
|
||||||
|
</mrow>
|
||||||
|
<annotation encoding="application/x-tex">
|
||||||
|
\\sin{x}+1\\;\\text{a}
|
||||||
|
</annotation>
|
||||||
|
</semantics>
|
||||||
|
</math>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`A MathML builder should make prime operators into <mo> nodes 1`] = `
|
||||||
|
|
||||||
|
<math>
|
||||||
|
<semantics>
|
||||||
|
<mrow>
|
||||||
|
<msup>
|
||||||
|
<mi>
|
||||||
|
f
|
||||||
|
</mi>
|
||||||
|
<mo mathvariant="normal">
|
||||||
|
′
|
||||||
|
</mo>
|
||||||
|
</msup>
|
||||||
|
</mrow>
|
||||||
|
<annotation encoding="application/x-tex">
|
||||||
|
f'
|
||||||
|
</annotation>
|
||||||
|
</semantics>
|
||||||
|
</math>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`A MathML builder should use <msupsub> for regular operators 1`] = `
|
||||||
|
|
||||||
|
<math>
|
||||||
|
<semantics>
|
||||||
|
<mrow>
|
||||||
|
<mstyle scriptlevel="0"
|
||||||
|
displaystyle="false"
|
||||||
|
>
|
||||||
|
<msubsup>
|
||||||
|
<mo>
|
||||||
|
∑
|
||||||
|
</mo>
|
||||||
|
<mi>
|
||||||
|
a
|
||||||
|
</mi>
|
||||||
|
<mi>
|
||||||
|
b
|
||||||
|
</mi>
|
||||||
|
</msubsup>
|
||||||
|
</mstyle>
|
||||||
|
</mrow>
|
||||||
|
<annotation encoding="application/x-tex">
|
||||||
|
\\textstyle\\sum_a^b
|
||||||
|
</annotation>
|
||||||
|
</semantics>
|
||||||
|
</math>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`A MathML builder should use <munderover> for large operators 1`] = `
|
||||||
|
|
||||||
|
<math>
|
||||||
|
<semantics>
|
||||||
|
<mrow>
|
||||||
|
<mstyle scriptlevel="0"
|
||||||
|
displaystyle="true"
|
||||||
|
>
|
||||||
|
<munderover>
|
||||||
|
<mo>
|
||||||
|
∑
|
||||||
|
</mo>
|
||||||
|
<mi>
|
||||||
|
a
|
||||||
|
</mi>
|
||||||
|
<mi>
|
||||||
|
b
|
||||||
|
</mi>
|
||||||
|
</munderover>
|
||||||
|
</mstyle>
|
||||||
|
</mrow>
|
||||||
|
<annotation encoding="application/x-tex">
|
||||||
|
\\displaystyle\\sum_a^b
|
||||||
|
</annotation>
|
||||||
|
</semantics>
|
||||||
|
</math>
|
||||||
|
|
||||||
|
`;
|
@@ -2192,50 +2192,6 @@ describe("An aligned environment", function() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const getMathML = function(expr, settings) {
|
|
||||||
const usedSettings = settings ? settings : defaultSettings;
|
|
||||||
|
|
||||||
expect(expr).toParse(usedSettings);
|
|
||||||
|
|
||||||
const built = buildMathML(parseTree(expr, usedSettings), expr, usedSettings);
|
|
||||||
|
|
||||||
// Strip off the surrounding <span>
|
|
||||||
return built.children[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("A MathML builder", function() {
|
|
||||||
it("should generate math nodes", function() {
|
|
||||||
const node = getMathML("x^2");
|
|
||||||
|
|
||||||
expect(node.type).toEqual("math");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should generate appropriate MathML types", function() {
|
|
||||||
const identifier = getMathML("x").children[0].children[0];
|
|
||||||
expect(identifier.children[0].type).toEqual("mi");
|
|
||||||
|
|
||||||
const number = getMathML("1").children[0].children[0];
|
|
||||||
expect(number.children[0].type).toEqual("mn");
|
|
||||||
|
|
||||||
const operator = getMathML("+").children[0].children[0];
|
|
||||||
expect(operator.children[0].type).toEqual("mo");
|
|
||||||
|
|
||||||
const space = getMathML("\\;").children[0].children[0];
|
|
||||||
expect(space.children[0].type).toEqual("mspace");
|
|
||||||
|
|
||||||
const text = getMathML("\\text{a}").children[0].children[0];
|
|
||||||
expect(text.children[0].type).toEqual("mtext");
|
|
||||||
|
|
||||||
const textop = getMathML("\\sin").children[0].children[0];
|
|
||||||
expect(textop.children[0].type).toEqual("mi");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should generate a <mphantom> node for \\phantom", function() {
|
|
||||||
const phantom = getMathML("\\phantom{x}").children[0].children[0];
|
|
||||||
expect(phantom.children[0].type).toEqual("mphantom");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("A parser that does not throw on unsupported commands", function() {
|
describe("A parser that does not throw on unsupported commands", function() {
|
||||||
// The parser breaks on unsupported commands unless it is explicitly
|
// The parser breaks on unsupported commands unless it is explicitly
|
||||||
// told not to
|
// told not to
|
||||||
|
52
test/mathml-spec.js
Normal file
52
test/mathml-spec.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/* global expect: false */
|
||||||
|
/* global it: false */
|
||||||
|
/* global describe: false */
|
||||||
|
|
||||||
|
import buildMathML from "../src/buildMathML";
|
||||||
|
import parseTree from "../src/parseTree";
|
||||||
|
import Options from "../src/Options";
|
||||||
|
import Settings from "../src/Settings";
|
||||||
|
import Style from "../src/Style";
|
||||||
|
|
||||||
|
const defaultSettings = new Settings({});
|
||||||
|
|
||||||
|
const getMathML = function(expr, settings) {
|
||||||
|
const usedSettings = settings ? settings : defaultSettings;
|
||||||
|
|
||||||
|
let startStyle = Style.TEXT;
|
||||||
|
if (usedSettings.displayMode) {
|
||||||
|
startStyle = Style.DISPLAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the default options
|
||||||
|
const options = new Options({
|
||||||
|
style: startStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
const built = buildMathML(parseTree(expr, usedSettings), expr, options);
|
||||||
|
|
||||||
|
// Strip off the surrounding <span>
|
||||||
|
return built.children[0].toMarkup();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("A MathML builder", function() {
|
||||||
|
it('should generate the right types of nodes', () => {
|
||||||
|
expect(getMathML("\\sin{x}+1\\;\\text{a}")).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make prime operators into <mo> nodes', () => {
|
||||||
|
expect(getMathML("f'")).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate <mphantom> nodes for \\phantom', () => {
|
||||||
|
expect(getMathML("\\phantom{x}")).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use <munderover> for large operators', () => {
|
||||||
|
expect(getMathML("\\displaystyle\\sum_a^b")).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use <msupsub> for regular operators', () => {
|
||||||
|
expect(getMathML("\\textstyle\\sum_a^b")).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user