diff --git a/src/Parser.js b/src/Parser.js index 0cf911ce..4f56b911 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -507,6 +507,20 @@ Parser.prototype.parseImplicitGroup = function() { body: new ParseNode("ordgroup", body, this.mode), }, this.mode); } + } else if (func === "$") { + if (this.mode === "math") { + throw new ParseError("$ within math mode"); + } + this.consume(); + const outerMode = this.mode; + this.switchMode("math"); + const body = this.parseExpression(false, "$"); + this.expect("$", true); + this.switchMode(outerMode); + return new ParseNode("styling", { + style: "text", + value: body, + }, "math"); } else { // Defer to parseFunction if it's not a function we handle return this.parseFunction(start); @@ -878,6 +892,10 @@ Parser.prototype.parseSymbol = function() { return new ParseFuncOrArgument( new ParseNode("textord", nucleus.text, this.mode, nucleus), false, nucleus); + } else if (nucleus.text === "$") { + return new ParseFuncOrArgument( + nucleus.text, + false, nucleus); } else { return null; } diff --git a/src/buildMathML.js b/src/buildMathML.js index 400324d3..7f106d2d 100644 --- a/src/buildMathML.js +++ b/src/buildMathML.js @@ -63,13 +63,19 @@ const getVariant = function(group, options) { */ const groupTypes = {}; +const defaultVariant = { + "mi": "italic", + "mn": "normal", + "mtext": "normal", +}; + groupTypes.mathord = function(group, options) { const node = new mathMLTree.MathNode( "mi", [makeText(group.value, group.mode)]); - const variant = getVariant(group, options); - if (variant) { + const variant = getVariant(group, options) || "italic"; + if (variant !== defaultVariant[node.type]) { node.setAttribute("mathvariant", variant); } return node; @@ -81,15 +87,16 @@ groupTypes.textord = function(group, options) { const variant = getVariant(group, options) || "normal"; let node; - if (/[0-9]/.test(group.value)) { + if (group.mode === 'text') { + node = new mathMLTree.MathNode("mtext", [text]); + } else 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]); + } + if (variant !== defaultVariant[node.type]) { node.setAttribute("mathvariant", variant); } @@ -149,11 +156,32 @@ groupTypes.ordgroup = function(group, options) { }; groupTypes.text = function(group, options) { - const inner = buildExpression(group.value.body, options); + const body = group.value.body; - const node = new mathMLTree.MathNode("mtext", inner); + // Convert each element of the body into MathML, and combine consecutive + // outputs into a single tag. In this way, we don't + // nest non-text items (e.g., $nested-math$) within an . + 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; + } + } + } - return node; + // If there is a single tag in the end (presumably ), + // just return it. Otherwise, wrap them in an . + if (inner.length === 1) { + return inner[0]; + } else { + return new mathMLTree.MathNode("mrow", inner); + } }; groupTypes.color = function(group, options) { diff --git a/test/katex-spec.js b/test/katex-spec.js index caefcc0a..33c2f5fc 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -732,6 +732,7 @@ describe("A text parser", function() { const badTextExpression = "\\text{a b%}"; const badFunctionExpression = "\\text{\\sqrt{x}}"; const mathTokenAfterText = "\\text{sin}^2"; + const textWithEmbeddedMath = "\\text{graph: $y = mx + b$}"; it("should not fail", function() { expect(textExpression).toParse(); @@ -789,6 +790,10 @@ describe("A text parser", function() { parse.value.body.map(function(n) { return n.value; }).join("") ).toBe("moo"); }); + + it("should parse math within text group", function() { + expect(textWithEmbeddedMath).toParse(); + }); }); describe("A color parser", function() { @@ -1539,7 +1544,7 @@ describe("A MathML font tree-builder", function() { const markup = buildMathML(tree, tex, defaultOptions).toMarkup(); expect(markup).toContain("A"); expect(markup).toContain("x"); - expect(markup).toContain("2"); + expect(markup).toContain("2"); expect(markup).toContain("\u03c9"); // \omega expect(markup).toContain("\u03A9"); // \Omega expect(markup).toContain("\u0131"); // \imath @@ -1552,7 +1557,7 @@ describe("A MathML font tree-builder", function() { const markup = buildMathML(tree, tex, defaultOptions).toMarkup(); expect(markup).toContain("A"); expect(markup).toContain("x"); - expect(markup).toContain("2"); + expect(markup).toContain("2"); expect(markup).toContain("\u03c9"); // \omega expect(markup).toContain("\u03A9"); // \Omega expect(markup).toContain("\u0131"); // \imath @@ -1563,12 +1568,12 @@ describe("A MathML font tree-builder", function() { const tex = "\\mathit{" + contents + "}"; const tree = getParsed(tex); const markup = buildMathML(tree, tex, defaultOptions).toMarkup(); - expect(markup).toContain("A"); - expect(markup).toContain("x"); + 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("\u03c9"); // \omega + expect(markup).toContain("\u03A9"); // \Omega + expect(markup).toContain("\u0131"); // \imath expect(markup).toContain("+"); }); @@ -1623,7 +1628,7 @@ describe("A MathML font tree-builder", function() { // 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("2"); expect(markup).toContain("\u03c9"); // \omega expect(markup).toContain("\u03A9"); // \Omega expect(markup).toContain("\u0131"); // \imath @@ -1661,6 +1666,22 @@ describe("A MathML font tree-builder", function() { ""; expect(markup).toContain(node); }); + + it("should render text as ", function() { + const tex = "\\text{for }"; + const tree = getParsed(tex); + const markup = buildMathML(tree, tex, defaultOptions).toMarkup(); + expect(markup).toContain("for\u00a0"); + }); + + it("should render math within text as side-by-side children", function() { + const tex = "\\text{graph: $y = mx + b$}"; + const tree = getParsed(tex); + const markup = buildMathML(tree, tex, defaultOptions).toMarkup(); + expect(markup).toContain("graph:\u00a0"); + expect(markup).toContain( + "y=mx+b"); + }); }); describe("A bin builder", function() { diff --git a/test/screenshotter/images/TextWithMath-chrome.png b/test/screenshotter/images/TextWithMath-chrome.png new file mode 100644 index 00000000..c33d83c7 Binary files /dev/null and b/test/screenshotter/images/TextWithMath-chrome.png differ diff --git a/test/screenshotter/images/TextWithMath-firefox.png b/test/screenshotter/images/TextWithMath-firefox.png new file mode 100644 index 00000000..8f163149 Binary files /dev/null and b/test/screenshotter/images/TextWithMath-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index b7962466..dc1acc9d 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -170,6 +170,7 @@ Symbols1: | \maltese\degree\pounds\$ \text{\maltese\degree\pounds\textdollar} Text: \frac{a}{b}\text{c~ {ab} \ e}+fg +TextWithMath: \text{for $a < b$ and $ c < d $}. Unicode: \begin{matrix}\text{ÀàÇçÉéÏïÖöÛû} \\ \text{БГДЖЗЙЛФЦШЫЮЯ} \\ \text{여보세요} \\ \text{私はバナナです} \end{matrix} UnsupportedCmds: tex: \err\,\frac\fracerr3\,2^\superr_\suberr\,\sqrt\sqrterr