commit c428abca1edd79d99bdfb308bc2dd7ae887af2ac
parent 07dc11ccb01d6722202d1461962a942d2ae7a4d4
Author: Kevin Barabash <kevinb7@gmail.com>
Date: Tue, 1 Sep 2015 09:00:16 -0600
Merge pull request #292 from kevinb7/fonts-p3_mathml
Adds MathML support for math font commands.
Diffstat:
6 files changed, 295 insertions(+), 74 deletions(-)
diff --git a/src/Options.js b/src/Options.js
@@ -6,7 +6,7 @@
*/
/**
- * This is the main options class. It contains the style, size, color and font
+ * This is the main options class. It contains the style, size, color, and font
* of the current parse level. It also contains the style and size of the parent
* parse level, so size changes can be handled efficiently.
*
diff --git a/src/buildCommon.js b/src/buildCommon.js
@@ -435,6 +435,7 @@ var fontMap = {
};
module.exports = {
+ fontMap: fontMap,
makeSymbol: makeSymbol,
mathsym: mathsym,
makeSpan: makeSpan,
diff --git a/src/buildHTML.js b/src/buildHTML.js
@@ -5,7 +5,6 @@
* called, to produce a final HTML tree.
*/
-var Options = require("./Options");
var ParseError = require("./ParseError");
var Style = require("./Style");
@@ -1330,22 +1329,11 @@ var buildGroup = function(group, options, prev) {
* Take an entire parse tree, and build it into an appropriate set of HTML
* nodes.
*/
-var buildHTML = function(tree, settings) {
+var buildHTML = function(tree, options) {
// buildExpression is destructive, so we need to make a clone
// of the incoming tree so that it isn't accidentally changed
tree = JSON.parse(JSON.stringify(tree));
- var startStyle = Style.TEXT;
- if (settings.displayMode) {
- startStyle = Style.DISPLAY;
- }
-
- // Setup the default options
- var options = new Options({
- style: startStyle,
- size: "size5"
- });
-
// Build the expression contained in the tree
var expression = buildExpression(tree, options);
var body = makeSpan(["base", options.style.cls()], expression);
diff --git a/src/buildMathML.js b/src/buildMathML.js
@@ -5,11 +5,14 @@
*/
var buildCommon = require("./buildCommon");
+var fontMetrics = require("./fontMetrics");
var mathMLTree = require("./mathMLTree");
var ParseError = require("./ParseError");
var symbols = require("./symbols");
+var utils = require("./utils");
var makeSpan = buildCommon.makeSpan;
+var fontMap = buildCommon.fontMap;
/**
* Takes a symbol and converts it into a MathML text node after performing
@@ -24,27 +27,69 @@ var makeText = function(text, mode) {
};
/**
+ * Returns the math variant as a string or null if none is required.
+ */
+var getVariant = function(group, options) {
+ var font = options.font;
+ if (!font) {
+ return null;
+ }
+
+ var mode = group.mode;
+ if (font === "mathit") {
+ return "italic";
+ }
+
+ var value = group.value;
+ if (utils.contains(["\\imath", "\\jmath"], value)) {
+ return null;
+ }
+
+ if (symbols[mode][value] && symbols[mode][value].replace) {
+ value = symbols[mode][value].replace;
+ }
+
+ var fontName = fontMap[font].fontName;
+ if (fontMetrics.getCharacterMetrics(value, fontName)) {
+ return fontMap[options.font].variant;
+ }
+
+ return null;
+};
+
+/**
* Functions for handling the different types of groups found in the parse
* tree. Each function should take a parse group and return a MathML node.
*/
var groupTypes = {
- mathord: function(group) {
+ mathord: function(group, options) {
var node = new mathMLTree.MathNode(
"mi",
[makeText(group.value, group.mode)]);
+ var variant = getVariant(group, options);
+ if (variant) {
+ node.setAttribute("mathvariant", variant);
+ }
return node;
},
- textord: function(group) {
+ textord: function(group, options) {
var text = makeText(group.value, group.mode);
+ var variant = getVariant(group, options) || "normal";
+
var node;
if (/[0-9]/.test(group.value)) {
+ // TODO(kevinb) merge adjacent <mn> nodes
+ // do it as a post processing step
node = new mathMLTree.MathNode("mn", [text]);
+ if (options.font) {
+ node.setAttribute("mathvariant", variant);
+ }
} else {
node = new mathMLTree.MathNode("mi", [text]);
- node.setAttribute("mathvariant", "normal");
+ node.setAttribute("mathvariant", variant);
}
return node;
@@ -94,24 +139,24 @@ var groupTypes = {
return node;
},
- ordgroup: function(group) {
- var inner = buildExpression(group.value);
+ ordgroup: function(group, options) {
+ var inner = buildExpression(group.value, options);
var node = new mathMLTree.MathNode("mrow", inner);
return node;
},
- text: function(group) {
- var inner = buildExpression(group.value.body);
+ text: function(group, options) {
+ var inner = buildExpression(group.value.body, options);
var node = new mathMLTree.MathNode("mtext", inner);
return node;
},
- color: function(group) {
- var inner = buildExpression(group.value.value);
+ color: function(group, options) {
+ var inner = buildExpression(group.value.value, options);
var node = new mathMLTree.MathNode("mstyle", inner);
@@ -120,15 +165,15 @@ var groupTypes = {
return node;
},
- supsub: function(group) {
- var children = [buildGroup(group.value.base)];
+ supsub: function(group, options) {
+ var children = [buildGroup(group.value.base, options)];
if (group.value.sub) {
- children.push(buildGroup(group.value.sub));
+ children.push(buildGroup(group.value.sub, options));
}
if (group.value.sup) {
- children.push(buildGroup(group.value.sup));
+ children.push(buildGroup(group.value.sup, options));
}
var nodeType;
@@ -145,11 +190,11 @@ var groupTypes = {
return node;
},
- genfrac: function(group) {
+ genfrac: function(group, options) {
var node = new mathMLTree.MathNode(
"mfrac",
- [buildGroup(group.value.numer),
- buildGroup(group.value.denom)]);
+ [buildGroup(group.value.numer, options),
+ buildGroup(group.value.denom, options)]);
if (!group.value.hasBarLine) {
node.setAttribute("linethickness", "0px");
@@ -186,35 +231,35 @@ var groupTypes = {
return node;
},
- array: function(group) {
+ array: function(group, options) {
return new mathMLTree.MathNode(
"mtable", group.value.body.map(function(row) {
return new mathMLTree.MathNode(
"mtr", row.map(function(cell) {
return new mathMLTree.MathNode(
- "mtd", [buildGroup(cell)]);
+ "mtd", [buildGroup(cell, options)]);
}));
}));
},
- sqrt: function(group) {
+ sqrt: function(group, options) {
var node;
if (group.value.index) {
node = new mathMLTree.MathNode(
"mroot", [
- buildGroup(group.value.body),
- buildGroup(group.value.index)
+ buildGroup(group.value.body, options),
+ buildGroup(group.value.index, options)
]);
} else {
node = new mathMLTree.MathNode(
- "msqrt", [buildGroup(group.value.body)]);
+ "msqrt", [buildGroup(group.value.body, options)]);
}
return node;
},
- leftright: function(group) {
- var inner = buildExpression(group.value.body);
+ leftright: function(group, options) {
+ var inner = buildExpression(group.value.body, options);
if (group.value.left !== ".") {
var leftNode = new mathMLTree.MathNode(
@@ -239,24 +284,19 @@ var groupTypes = {
return outerNode;
},
- accent: function(group) {
+ accent: function(group, options) {
var accentNode = new mathMLTree.MathNode(
"mo", [makeText(group.value.accent, group.mode)]);
var node = new mathMLTree.MathNode(
"mover",
- [buildGroup(group.value.base),
+ [buildGroup(group.value.base, options),
accentNode]);
node.setAttribute("accent", "true");
return node;
},
-
- font: function(group) {
- // pass through so we can render something without throwing
- return buildGroup(group.value.body);
- },
spacing: function(group) {
var node;
@@ -303,6 +343,11 @@ var groupTypes = {
return node;
},
+ font: function(group, options) {
+ var font = group.value.font;
+ return buildGroup(group.value.body, options.withFont(font));
+ },
+
delimsizing: function(group) {
var children = [];
@@ -326,8 +371,8 @@ var groupTypes = {
return node;
},
- styling: function(group) {
- var inner = buildExpression(group.value.value, inner);
+ styling: function(group, options) {
+ var inner = buildExpression(group.value.value, options);
var node = new mathMLTree.MathNode("mstyle", inner);
@@ -346,28 +391,30 @@ var groupTypes = {
return node;
},
- sizing: function(group) {
- var inner = buildExpression(group.value.value);
+ sizing: function(group, options) {
+ var inner = buildExpression(group.value.value, options);
var node = new mathMLTree.MathNode("mstyle", inner);
// TODO(emily): This doesn't produce the correct size for nested size
// changes, because we don't keep state of what style we're currently
- // in, so we can't reset the size to normal before changing it.
+ // in, so we can't reset the size to normal before changing it. Now
+ // that we're passing an options parameter we should be able to fix
+ // this.
node.setAttribute(
"mathsize", buildCommon.sizingMultiplier[group.value.size] + "em");
return node;
},
- overline: function(group) {
+ overline: function(group, options) {
var operator = new mathMLTree.MathNode(
"mo", [new mathMLTree.TextNode("\u203e")]);
operator.setAttribute("stretchy", "true");
var node = new mathMLTree.MathNode(
"mover",
- [buildGroup(group.value.body),
+ [buildGroup(group.value.body, options),
operator]);
node.setAttribute("accent", "true");
@@ -382,9 +429,9 @@ var groupTypes = {
return node;
},
- llap: function(group) {
+ llap: function(group, options) {
var node = new mathMLTree.MathNode(
- "mpadded", [buildGroup(group.value.body)]);
+ "mpadded", [buildGroup(group.value.body, options)]);
node.setAttribute("lspace", "-1width");
node.setAttribute("width", "0px");
@@ -392,9 +439,9 @@ var groupTypes = {
return node;
},
- rlap: function(group) {
+ rlap: function(group, options) {
var node = new mathMLTree.MathNode(
- "mpadded", [buildGroup(group.value.body)]);
+ "mpadded", [buildGroup(group.value.body, options)]);
node.setAttribute("width", "0px");
@@ -402,7 +449,7 @@ var groupTypes = {
},
phantom: function(group, options, prev) {
- var inner = buildExpression(group.value.value);
+ var inner = buildExpression(group.value.value, options);
return new mathMLTree.MathNode("mphantom", inner);
}
};
@@ -412,11 +459,11 @@ var groupTypes = {
* MathML nodes. A little simpler than the HTML version because we don't do any
* previous-node handling.
*/
-var buildExpression = function(expression) {
+var buildExpression = function(expression, options) {
var groups = [];
for (var i = 0; i < expression.length; i++) {
var group = expression[i];
- groups.push(buildGroup(group));
+ groups.push(buildGroup(group, options));
}
return groups;
};
@@ -425,14 +472,14 @@ var buildExpression = function(expression) {
* Takes a group from the parser and calls the appropriate groupTypes function
* on it to produce a MathML node.
*/
-var buildGroup = function(group) {
+var buildGroup = function(group, options) {
if (!group) {
return new mathMLTree.MathNode("mrow");
}
if (groupTypes[group.type]) {
// Call the groupTypes function
- return groupTypes[group.type](group);
+ return groupTypes[group.type](group, options);
} else {
throw new ParseError(
"Got group of unknown type: '" + group.type + "'");
@@ -447,8 +494,8 @@ var buildGroup = function(group) {
* Note that we actually return a domTree element with a `<math>` inside it so
* we can do appropriate styling.
*/
-var buildMathML = function(tree, texExpression, settings) {
- var expression = buildExpression(tree);
+var buildMathML = function(tree, texExpression, options) {
+ var expression = buildExpression(tree, options);
// Wrap up the expression in an mrow so it is presented in the semantics
// tag correctly.
diff --git a/src/buildTree.js b/src/buildTree.js
@@ -1,15 +1,30 @@
-
var buildHTML = require("./buildHTML");
var buildMathML = require("./buildMathML");
var buildCommon = require("./buildCommon");
+var Options = require("./Options");
+var Settings = require("./Settings");
+var Style = require("./Style");
var makeSpan = buildCommon.makeSpan;
var buildTree = function(tree, expression, settings) {
+ settings = settings || new Settings({});
+
+ var startStyle = Style.TEXT;
+ if (settings.displayMode) {
+ startStyle = Style.DISPLAY;
+ }
+
+ // Setup the default options
+ var options = new Options({
+ style: startStyle,
+ size: "size5"
+ });
+
// `buildHTML` sometimes messes with the parse tree (like turning bins ->
// ords), so we build the MathML version first.
- var mathMLNode = buildMathML(tree, expression, settings);
- var htmlNode = buildHTML(tree, settings);
+ var mathMLNode = buildMathML(tree, expression, options);
+ var htmlNode = buildHTML(tree, options);
var katexNode = makeSpan(["katex"], [
mathMLNode, htmlNode
diff --git a/test/katex-spec.js b/test/katex-spec.js
@@ -4,27 +4,51 @@
/* global it: false */
/* global describe: false */
-var buildHTML = require("../src/buildHTML");
var buildMathML = require("../src/buildMathML");
+var buildTree = require("../src/buildTree");
var katex = require("../katex");
var ParseError = require("../src/ParseError");
var parseTree = require("../src/parseTree");
+var Options = require("../src/Options");
var Settings = require("../src/Settings");
+var Style = require("../src/Style");
var defaultSettings = new Settings({});
+var defaultOptions = new Options({
+ style: Style.TEXT,
+ size: "size5"
+});
-var getBuilt = function(expr, settings) {
+var _getBuilt = function(expr, settings) {
var usedSettings = settings ? settings : defaultSettings;
-
- expect(expr).toBuild(usedSettings);
-
var parsedTree = parseTree(expr, usedSettings);
- var built = buildHTML(parsedTree, usedSettings);
+ var rootNode = buildTree(parsedTree, expr, usedSettings);
+
+ // grab the root node of the HTML rendering
+ var builtHTML = rootNode.children[1];
// Remove the outer .katex and .katex-inner layers
- return built.children[2].children;
+ return builtHTML.children[2].children;
+};
+
+/**
+ * Return the root node of the rendered HTML.
+ * @param expr
+ * @param settings
+ * @returns {Object}
+ */
+var getBuilt = function(expr, settings) {
+ var usedSettings = settings ? settings : defaultSettings;
+ expect(expr).toBuild(usedSettings);
+ return _getBuilt(expr, settings);
};
+/**
+ * Return the root node of the parse tree.
+ * @param expr
+ * @param settings
+ * @returns {Object}
+ */
var getParsed = function(expr, settings) {
var usedSettings = settings ? settings : defaultSettings;
@@ -104,7 +128,7 @@ beforeEach(function() {
expect(actual).toParse(usedSettings);
try {
- buildHTML(parseTree(actual, usedSettings), usedSettings);
+ _getBuilt(actual, settings);
} catch (e) {
result.pass = false;
if (e instanceof ParseError) {
@@ -1269,6 +1293,152 @@ describe("An HTML font tree-builder", function () {
});
});
+
+describe("A MathML font tree-builder", function () {
+ var contents = "Ax2k\\omega\\Omega\\imath+";
+
+ it("should render " + contents + " with the correct mathvariants", function () {
+ var tree = getParsed(contents);
+ var markup = buildMathML(tree, contents, defaultOptions).toMarkup();
+ expect(markup).toContain("<mi>A</mi>");
+ expect(markup).toContain("<mi>x</mi>");
+ expect(markup).toContain("<mn>2</mn>");
+ expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
+ expect(markup).toContain("<mi mathvariant=\"normal\">\u03A9</mi>"); // \Omega
+ expect(markup).toContain("<mi>\u0131</mi>"); // \imath
+ expect(markup).toContain("<mo>+</mo>");
+ });
+
+ it("should render \\mathbb{" + contents + "} with the correct mathvariants", function () {
+ var tex = "\\mathbb{" + contents + "}";
+ var tree = getParsed(tex);
+ var markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ expect(markup).toContain("<mi mathvariant=\"double-struck\">A</mi>");
+ expect(markup).toContain("<mi>x</mi>");
+ expect(markup).toContain("<mn mathvariant=\"normal\">2</mn>");
+ expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
+ expect(markup).toContain("<mi mathvariant=\"normal\">\u03A9</mi>"); // \Omega
+ expect(markup).toContain("<mi>\u0131</mi>"); // \imath
+ expect(markup).toContain("<mo>+</mo>");
+ });
+
+ it("should render \\mathrm{" + contents + "} with the correct mathvariants", function () {
+ var tex = "\\mathrm{" + contents + "}";
+ var tree = getParsed(tex);
+ var markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ expect(markup).toContain("<mi mathvariant=\"normal\">A</mi>");
+ expect(markup).toContain("<mi mathvariant=\"normal\">x</mi>");
+ expect(markup).toContain("<mn mathvariant=\"normal\">2</mn>");
+ expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
+ expect(markup).toContain("<mi mathvariant=\"normal\">\u03A9</mi>"); // \Omega
+ expect(markup).toContain("<mi>\u0131</mi>"); // \imath
+ expect(markup).toContain("<mo>+</mo>");
+ });
+
+ it("should render \\mathit{" + contents + "} with the correct mathvariants", function () {
+ var tex = "\\mathit{" + contents + "}";
+ var tree = getParsed(tex);
+ var markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ expect(markup).toContain("<mi mathvariant=\"italic\">A</mi>");
+ expect(markup).toContain("<mi mathvariant=\"italic\">x</mi>");
+ expect(markup).toContain("<mn mathvariant=\"italic\">2</mn>");
+ expect(markup).toContain("<mi mathvariant=\"italic\">\u03c9</mi>"); // \omega
+ expect(markup).toContain("<mi mathvariant=\"italic\">\u03A9</mi>"); // \Omega
+ expect(markup).toContain("<mi mathvariant=\"italic\">\u0131</mi>"); // \imath
+ expect(markup).toContain("<mo>+</mo>");
+ });
+
+ it("should render \\mathbf{" + contents + "} with the correct mathvariants", function () {
+ var tex = "\\mathbf{" + contents + "}";
+ var tree = getParsed(tex);
+ var markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ expect(markup).toContain("<mi mathvariant=\"bold\">A</mi>");
+ expect(markup).toContain("<mi mathvariant=\"bold\">x</mi>");
+ expect(markup).toContain("<mn mathvariant=\"bold\">2</mn>");
+ expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
+ expect(markup).toContain("<mi mathvariant=\"bold\">\u03A9</mi>"); // \Omega
+ expect(markup).toContain("<mi>\u0131</mi>"); // \imath
+ expect(markup).toContain("<mo>+</mo>");
+ });
+
+ it("should render \\mathcal{" + contents + "} with the correct mathvariants", function () {
+ var tex = "\\mathcal{" + contents + "}";
+ var tree = getParsed(tex);
+ var markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ expect(markup).toContain("<mi mathvariant=\"script\">A</mi>");
+ expect(markup).toContain("<mi>x</mi>"); // script is caps only
+ expect(markup).toContain("<mn mathvariant=\"script\">2</mn>");
+ // MathJax marks everything below as "script" except \omega
+ // We don't have these glyphs in "caligraphic" and neither does MathJax
+ expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
+ expect(markup).toContain("<mi mathvariant=\"normal\">\u03A9</mi>"); // \Omega
+ expect(markup).toContain("<mi>\u0131</mi>"); // \imath
+ expect(markup).toContain("<mo>+</mo>");
+ });
+
+ it("should render \\mathfrak{" + contents + "} with the correct mathvariants", function () {
+ var tex = "\\mathfrak{" + contents + "}";
+ var tree = getParsed(tex);
+ var markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ expect(markup).toContain("<mi mathvariant=\"fraktur\">A</mi>");
+ expect(markup).toContain("<mi mathvariant=\"fraktur\">x</mi>");
+ expect(markup).toContain("<mn mathvariant=\"fraktur\">2</mn>");
+ // MathJax marks everything below as "fraktur" except \omega
+ // We don't have these glyphs in "fraktur" and neither does MathJax
+ expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
+ expect(markup).toContain("<mi mathvariant=\"normal\">\u03A9</mi>"); // \Omega
+ expect(markup).toContain("<mi>\u0131</mi>"); // \imath
+ expect(markup).toContain("<mo>+</mo>");
+ });
+
+ it("should render \\mathscr{" + contents + "} with the correct mathvariants", function () {
+ var tex = "\\mathscr{" + contents + "}";
+ var tree = getParsed(tex);
+ var markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ expect(markup).toContain("<mi mathvariant=\"script\">A</mi>");
+ // MathJax marks everything below as "script" except \omega
+ // We don't have these glyphs in "script" and neither does MathJax
+ expect(markup).toContain("<mi>x</mi>");
+ expect(markup).toContain("<mn mathvariant=\"normal\">2</mn>");
+ expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
+ expect(markup).toContain("<mi mathvariant=\"normal\">\u03A9</mi>"); // \Omega
+ expect(markup).toContain("<mi>\u0131</mi>"); // \imath
+ expect(markup).toContain("<mo>+</mo>");
+ });
+
+ it("should render \\mathsf{" + contents + "} with the correct mathvariants", function () {
+ var tex = "\\mathsf{" + contents + "}";
+ var tree = getParsed(tex);
+ var markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ expect(markup).toContain("<mi mathvariant=\"sans-serif\">A</mi>");
+ expect(markup).toContain("<mi mathvariant=\"sans-serif\">x</mi>");
+ expect(markup).toContain("<mn mathvariant=\"sans-serif\">2</mn>");
+ expect(markup).toContain("<mi>\u03c9</mi>"); // \omega
+ expect(markup).toContain("<mi mathvariant=\"sans-serif\">\u03A9</mi>"); // \Omega
+ expect(markup).toContain("<mi>\u0131</mi>"); // \imath
+ expect(markup).toContain("<mo>+</mo>");
+ });
+
+ it("should render a combination of font and color changes", function () {
+ var tex = "\\color{blue}{\\mathbb R}";
+ var tree = getParsed(tex);
+ var markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ var node = "<mstyle mathcolor=\"blue\">" +
+ "<mi mathvariant=\"double-struck\">R</mi>" +
+ "</mstyle>";
+ expect(markup).toContain(node);
+
+ // reverse the order of the commands
+ tex = "\\mathbb{\\color{blue}{R}}";
+ tree = getParsed(tex);
+ markup = buildMathML(tree, tex, defaultOptions).toMarkup();
+ node = "<mstyle mathcolor=\"blue\">" +
+ "<mi mathvariant=\"double-struck\">R</mi>" +
+ "</mstyle>";
+ expect(markup).toContain(node);
+ });
+});
+
describe("A bin builder", function() {
it("should create mbins normally", function() {
var built = getBuilt("x + y");