diff --git a/README.md b/README.md index 77432fdb..172d2710 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,13 @@ will appear larger than 1cm in browser units. - MathJax supports Unicode text characters in math mode, unlike LaTeX. To support this behavior in KaTeX, set the `unicodeTextInMathMode` option to `true`. +- KaTeX breaks lines with `\\` and `\newline` in inline math, but ignores them + in display math (matching LaTeX's behavior, but not MathJax's behavior). + To allow `\\` and `\newline` to break lines in display mode, + add the following CSS rule: + ```css + .katex-display > .katex > .katex-html > .newline { display: block !important; } + ``` ## Libraries diff --git a/src/ParseNode.js b/src/ParseNode.js index 3b74107a..a999431c 100644 --- a/src/ParseNode.js +++ b/src/ParseNode.js @@ -121,6 +121,8 @@ type ParseNodeTypes = { |}, "cr": {| type: "cr", + //newRow: boolean, + newLine: boolean, size: ?ParseNode<*>, |}, "delimsizing": {| diff --git a/src/Parser.js b/src/Parser.js index c2951788..66b95ddd 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -150,7 +150,7 @@ export default class Parser { return expression; } - static endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"]; + static endOfExpression = ["}", "\\end", "\\right", "&", "\\cr"]; /** * Parses an "expression", which is a list of atoms. diff --git a/src/buildHTML.js b/src/buildHTML.js index ca5f410a..b54d6a85 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -700,6 +700,15 @@ export default function buildHTML(tree, options) { htmlNode.children.push(buildHTMLUnbreakable(parts, options)); parts = []; } + } else if (expression[i].hasClass("newline")) { + // Write the line except the newline + parts.pop(); + if (parts.length > 0) { + htmlNode.children.push(buildHTMLUnbreakable(parts, options)); + parts = []; + } + // Put the newline at the top level + htmlNode.children.push(expression[i]); } } if (parts.length > 0) { diff --git a/src/defineFunction.js b/src/defineFunction.js index d3762974..55fcf773 100644 --- a/src/defineFunction.js +++ b/src/defineFunction.js @@ -132,7 +132,7 @@ export type FunctionSpec = {| // FLOW TYPE NOTES: Doing either one of the following two // - // - removing the NOTETYPE type parameter in FunctionSpec above; + // - removing the NODETYPE type parameter in FunctionSpec above; // - using ?FunctionHandler below; // // results in a confusing flow typing error: diff --git a/src/environments/array.js b/src/environments/array.js index 61929c23..e7f5291d 100644 --- a/src/environments/array.js +++ b/src/environments/array.js @@ -64,7 +64,7 @@ function parseArray( numHLinesBeforeRow.push(getNumHLines(parser)); while (true) { // eslint-disable-line no-constant-condition - let cell = parser.parseExpression(false, undefined); + let cell = parser.parseExpression(false, "\\\\"); cell = new ParseNode("ordgroup", cell, parser.mode); if (style) { cell = new ParseNode("styling", { @@ -100,7 +100,7 @@ function parseArray( row = []; body.push(row); } else { - throw new ParseError("Expected & or \\\\ or \\end", + throw new ParseError("Expected & or \\\\ or \\cr or \\end", parser.nextToken); } } diff --git a/src/functions.js b/src/functions.js index ca36276c..dac059ac 100644 --- a/src/functions.js +++ b/src/functions.js @@ -270,18 +270,8 @@ defineFunction("infix", ["\\over", "\\choose", "\\atop"], { }; }); -// Row breaks for aligned data -defineFunction("cr", ["\\\\", "\\cr"], { - numArgs: 0, - numOptionalArgs: 1, - argTypes: ["size"], -}, function(context, args, optArgs) { - const size = optArgs[0]; - return { - type: "cr", - size: size, - }; -}); +// Row and line breaks +import "./functions/cr"; // Environment delimiters defineFunction("environment", ["\\begin", "\\end"], { diff --git a/src/functions/cr.js b/src/functions/cr.js new file mode 100644 index 00000000..db55ae09 --- /dev/null +++ b/src/functions/cr.js @@ -0,0 +1,57 @@ +//@flow +// Row breaks within tabular environments, and line breaks at top level + +import defineFunction from "../defineFunction"; +import buildCommon from "../buildCommon"; +import mathMLTree from "../mathMLTree"; +import { calculateSize } from "../units"; +import ParseError from "../ParseError"; + +defineFunction({ + type: "cr", + names: ["\\\\", "\\cr", "\\newline"], + props: { + numArgs: 0, + numOptionalArgs: 1, + argTypes: ["size"], + allowedInText: true, + }, + + handler: (context, args, optArgs) => { + return { + type: "cr", + // \\ and \cr both end the row in a tabular environment + // This flag isn't currently needed by environments/array.js + //newRow: context.funcName !== "\\newline", + // \\ and \newline both end the line in an inline math environment + newLine: context.funcName !== "\\cr", + size: optArgs[0], + }; + }, + + // The following builders are called only at the top level, + // not within tabular environments. + + htmlBuilder: (group, options) => { + if (!group.value.newLine) { + throw new ParseError( + "\\cr valid only within a tabular environment"); + } + const span = buildCommon.makeSpan(["mspace", "newline"], [], options); + if (group.value.size) { + span.style.marginTop = + calculateSize(group.value.size.value, options) + "em"; + } + return span; + }, + + mathmlBuilder: (group, options) => { + const node = new mathMLTree.MathNode("mspace"); + node.setAttribute("linebreak", "newline"); + if (group.value.size) { + node.setAttribute("height", + calculateSize(group.value.size.value, options) + "em"); + } + return node; + }, +}); diff --git a/src/katex.less b/src/katex.less index b62c5acf..1492040c 100644 --- a/src/katex.less +++ b/src/katex.less @@ -30,6 +30,11 @@ > .katex-html { display: inline-block; + + /* \newline doesn't do anything in display mode */ + > .newline { + display: none; + } } } } @@ -60,6 +65,13 @@ overflow: hidden; } + .katex-html { + /* \newline is an empty block at top level of inline mode */ + > .newline { + display: block; + } + } + .base { position: relative; display: inline-block; diff --git a/src/types.js b/src/types.js index 48887b12..779a911e 100644 --- a/src/types.js +++ b/src/types.js @@ -24,4 +24,4 @@ export type ArgType = "color" | "size" | "url" | "original" | Mode; export type StyleStr = "text" | "display" | "script" | "scriptscript"; // Allowable token text for "break" arguments in parser -export type BreakToken = "]" | "}" | "$" | "\\)"; +export type BreakToken = "]" | "}" | "$" | "\\)" | "\\\\"; diff --git a/test/errors-spec.js b/test/errors-spec.js index 864b0d84..7fcb059f 100644 --- a/test/errors-spec.js +++ b/test/errors-spec.js @@ -208,10 +208,6 @@ describe("Parser.expect calls:", function() { "Expected 'EOF', got '\\end' at position 2:" + " x\\̲e̲n̲d̲{matrix}"); }); - it("complains about top-level \\\\", function() { - expect("1\\\\2").toFailWithParseError( - "Expected 'EOF', got '\\\\' at position 2: 1\\̲\\̲2"); - }); it("complains about top-level &", function() { expect("1&2").toFailWithParseError( "Expected 'EOF', got '&' at position 2: 1&̲2"); @@ -292,12 +288,12 @@ describe("environments.js:", function() { describe("parseArray", function() { it("rejects missing \\end", function() { expect("\\begin{matrix}1").toFailWithParseError( - "Expected & or \\\\ or \\end at end of input:" + + "Expected & or \\\\ or \\cr or \\end at end of input:" + " \\begin{matrix}1"); }); it("rejects incorrectly scoped \\end", function() { expect("{\\begin{matrix}1}\\end{matrix}").toFailWithParseError( - "Expected & or \\\\ or \\end at position 17:" + + "Expected & or \\\\ or \\cr or \\end at position 17:" + " …\\begin{matrix}1}̲\\end{matrix}"); }); }); diff --git a/test/katex-spec.js b/test/katex-spec.js index e629dc62..81f4d11d 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -3126,6 +3126,18 @@ describe("The \\mathchoice function", function() { }); }); +describe("Newlines via \\\\ and \\newline", function() { + it("should build \\\\ and \\newline the same", () => { + expect("hello \\\\ world").toBuildLike("hello \\newline world"); + expect("hello \\\\[1ex] world").toBuildLike( + "hello \\newline[1ex] world"); + }); + + it("should not allow \\cr at top level", () => { + expect("hello \\cr world").toNotParse(); + }); +}); + describe("Symbols", function() { it("should parse \\text{\\i\\j}", () => { expect("\\text{\\i\\j}").toBuild(); diff --git a/test/screenshotter/images/NewLine-chrome.png b/test/screenshotter/images/NewLine-chrome.png new file mode 100644 index 00000000..d3ffb800 Binary files /dev/null and b/test/screenshotter/images/NewLine-chrome.png differ diff --git a/test/screenshotter/images/NewLine-firefox.png b/test/screenshotter/images/NewLine-firefox.png new file mode 100644 index 00000000..29397e0c Binary files /dev/null and b/test/screenshotter/images/NewLine-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 99f18d1a..fc7620ac 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -207,6 +207,14 @@ NegativeSpace: NestedFractions: | \dfrac{\frac{a}{b}}{\frac{c}{d}}\dfrac{\dfrac{a}{b}} {\dfrac{c}{d}}\frac{\frac{a}{b}}{\frac{c}{d}} +NewLine: | + \frac{a^2+b^2}{c^2} \newline + \frac{a^2+b^2}{c^2} \\[1ex] + \begin{pmatrix} + a & b \\ + c & d \cr + \end{pmatrix} \\ + a+b+c+{d+\\e}+f+g Not: | \begin{array}{l} \not=\not>\not\geq\not\in\not<\not\leq\not{abc} \\