diff --git a/src/Options.js b/src/Options.js index 323920312..7b2e5c965 100644 --- 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 index 36fe24a6c..ee23186bc 100644 --- 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 index 7f0dcbbc1..9e76f82c8 100644 --- 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 index 28cd68054..f0f876af2 100644 --- 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 @@ -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 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 `` 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 index 270c03d42..03ade9ebf 100644 --- 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 index 9d2867f1f..0dc29635a 100644 --- 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("A"); + expect(markup).toContain("x"); + expect(markup).toContain("2"); + expect(markup).toContain("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath + expect(markup).toContain("+"); + }); + + 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("A"); + expect(markup).toContain("x"); + expect(markup).toContain("2"); + expect(markup).toContain("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath + expect(markup).toContain("+"); + }); + + 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("A"); + expect(markup).toContain("x"); + expect(markup).toContain("2"); + expect(markup).toContain("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath + expect(markup).toContain("+"); + }); + + 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("A"); + expect(markup).toContain("x"); + expect(markup).toContain("2"); + expect(markup).toContain("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath + expect(markup).toContain("+"); + }); + + 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("A"); + expect(markup).toContain("x"); + expect(markup).toContain("2"); + expect(markup).toContain("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath + expect(markup).toContain("+"); + }); + + 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("A"); + expect(markup).toContain("x"); // script is caps only + expect(markup).toContain("2"); + // MathJax marks everything below as "script" except \omega + // We don't have these glyphs in "caligraphic" and neither does MathJax + expect(markup).toContain("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath + expect(markup).toContain("+"); + }); + + 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("A"); + expect(markup).toContain("x"); + expect(markup).toContain("2"); + // MathJax marks everything below as "fraktur" except \omega + // We don't have these glyphs in "fraktur" and neither does MathJax + expect(markup).toContain("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath + expect(markup).toContain("+"); + }); + + 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("A"); + // MathJax marks everything below as "script" except \omega + // We don't have these glyphs in "script" and neither does MathJax + expect(markup).toContain("x"); + expect(markup).toContain("2"); + expect(markup).toContain("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath + expect(markup).toContain("+"); + }); + + 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("A"); + expect(markup).toContain("x"); + expect(markup).toContain("2"); + expect(markup).toContain("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath + expect(markup).toContain("+"); + }); + + 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 = "" + + "R" + + ""; + 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 = "" + + "R" + + ""; + expect(markup).toContain(node); + }); +}); + describe("A bin builder", function() { it("should create mbins normally", function() { var built = getBuilt("x + y");