Merge pull request #292 from kevinb7/fonts-p3_mathml

Adds MathML support for math font commands.
This commit is contained in:
Kevin Barabash 2015-09-01 09:00:16 -06:00
commit c428abca1e
6 changed files with 295 additions and 74 deletions

View File

@ -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.
*

View File

@ -435,6 +435,7 @@ var fontMap = {
};
module.exports = {
fontMap: fontMap,
makeSymbol: makeSymbol,
mathsym: mathsym,
makeSpan: makeSpan,

View File

@ -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);

View File

@ -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
@ -23,28 +26,70 @@ var makeText = function(text, mode) {
return new mathMLTree.TextNode(text);
};
/**
* 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.

View File

@ -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

View File

@ -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");