Support for top-level \newline and \\ in inline math (#1298)

* Support for top-level \newline and \\ in inline math

This was a little tricky because `\\` was defined as an endOfExpression.
Instead made `\\` a termination specific to an array environment.
Outside an array environment, buildHTML handles the `cr` object,
resulting in a `.newline` class.  Currently this turns into a
`display: block` (with appropriate vertical spacing) only in inline math,
matching LaTeX.

* Simplify code

* Fix Jest errors

* NewLine screenshot test

* Bug fix: \\ only works at top level of inline

* Add \newline and \cr to test

* Switch test to pmatrix

* Add vertical space test

* Add \\ vs. \newline tests

* Fix flow errors

* Add \cr test

* Add documentation for \\ at top level

* Comment out newRow

* Fix commenting out
This commit is contained in:
Erik Demaine
2018-05-13 09:58:24 -04:00
committed by GitHub
parent bb1dc0c431
commit 4801ab875a
15 changed files with 116 additions and 23 deletions

View File

@@ -132,6 +132,13 @@ will appear larger than 1cm in browser units.
- MathJax supports Unicode text characters in math mode, unlike LaTeX. - MathJax supports Unicode text characters in math mode, unlike LaTeX.
To support this behavior in KaTeX, set the `unicodeTextInMathMode` option To support this behavior in KaTeX, set the `unicodeTextInMathMode` option
to `true`. 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 ## Libraries

View File

@@ -121,6 +121,8 @@ type ParseNodeTypes = {
|}, |},
"cr": {| "cr": {|
type: "cr", type: "cr",
//newRow: boolean,
newLine: boolean,
size: ?ParseNode<*>, size: ?ParseNode<*>,
|}, |},
"delimsizing": {| "delimsizing": {|

View File

@@ -150,7 +150,7 @@ export default class Parser {
return expression; return expression;
} }
static endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"]; static endOfExpression = ["}", "\\end", "\\right", "&", "\\cr"];
/** /**
* Parses an "expression", which is a list of atoms. * Parses an "expression", which is a list of atoms.

View File

@@ -700,6 +700,15 @@ export default function buildHTML(tree, options) {
htmlNode.children.push(buildHTMLUnbreakable(parts, options)); htmlNode.children.push(buildHTMLUnbreakable(parts, options));
parts = []; 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) { if (parts.length > 0) {

View File

@@ -132,7 +132,7 @@ export type FunctionSpec<NODETYPE: NodeType> = {|
// FLOW TYPE NOTES: Doing either one of the following two // 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<NODETYPE> below; // - using ?FunctionHandler<NODETYPE> below;
// //
// results in a confusing flow typing error: // results in a confusing flow typing error:

View File

@@ -64,7 +64,7 @@ function parseArray(
numHLinesBeforeRow.push(getNumHLines(parser)); numHLinesBeforeRow.push(getNumHLines(parser));
while (true) { // eslint-disable-line no-constant-condition 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); cell = new ParseNode("ordgroup", cell, parser.mode);
if (style) { if (style) {
cell = new ParseNode("styling", { cell = new ParseNode("styling", {
@@ -100,7 +100,7 @@ function parseArray(
row = []; row = [];
body.push(row); body.push(row);
} else { } else {
throw new ParseError("Expected & or \\\\ or \\end", throw new ParseError("Expected & or \\\\ or \\cr or \\end",
parser.nextToken); parser.nextToken);
} }
} }

View File

@@ -270,18 +270,8 @@ defineFunction("infix", ["\\over", "\\choose", "\\atop"], {
}; };
}); });
// Row breaks for aligned data // Row and line breaks
defineFunction("cr", ["\\\\", "\\cr"], { import "./functions/cr";
numArgs: 0,
numOptionalArgs: 1,
argTypes: ["size"],
}, function(context, args, optArgs) {
const size = optArgs[0];
return {
type: "cr",
size: size,
};
});
// Environment delimiters // Environment delimiters
defineFunction("environment", ["\\begin", "\\end"], { defineFunction("environment", ["\\begin", "\\end"], {

57
src/functions/cr.js Normal file
View File

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

View File

@@ -30,6 +30,11 @@
> .katex-html { > .katex-html {
display: inline-block; display: inline-block;
/* \newline doesn't do anything in display mode */
> .newline {
display: none;
}
} }
} }
} }
@@ -60,6 +65,13 @@
overflow: hidden; overflow: hidden;
} }
.katex-html {
/* \newline is an empty block at top level of inline mode */
> .newline {
display: block;
}
}
.base { .base {
position: relative; position: relative;
display: inline-block; display: inline-block;

View File

@@ -24,4 +24,4 @@ export type ArgType = "color" | "size" | "url" | "original" | Mode;
export type StyleStr = "text" | "display" | "script" | "scriptscript"; export type StyleStr = "text" | "display" | "script" | "scriptscript";
// Allowable token text for "break" arguments in parser // Allowable token text for "break" arguments in parser
export type BreakToken = "]" | "}" | "$" | "\\)"; export type BreakToken = "]" | "}" | "$" | "\\)" | "\\\\";

View File

@@ -208,10 +208,6 @@ describe("Parser.expect calls:", function() {
"Expected 'EOF', got '\\end' at position 2:" + "Expected 'EOF', got '\\end' at position 2:" +
" x\\̲e̲n̲d̲{matrix}"); " 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() { it("complains about top-level &", function() {
expect("1&2").toFailWithParseError( expect("1&2").toFailWithParseError(
"Expected 'EOF', got '&' at position 2: 1&̲2"); "Expected 'EOF', got '&' at position 2: 1&̲2");
@@ -292,12 +288,12 @@ describe("environments.js:", function() {
describe("parseArray", function() { describe("parseArray", function() {
it("rejects missing \\end", function() { it("rejects missing \\end", function() {
expect("\\begin{matrix}1").toFailWithParseError( 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"); " \\begin{matrix}1");
}); });
it("rejects incorrectly scoped \\end", function() { it("rejects incorrectly scoped \\end", function() {
expect("{\\begin{matrix}1}\\end{matrix}").toFailWithParseError( 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}"); " …\\begin{matrix}1}̲\\end{matrix}");
}); });
}); });

View File

@@ -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() { describe("Symbols", function() {
it("should parse \\text{\\i\\j}", () => { it("should parse \\text{\\i\\j}", () => {
expect("\\text{\\i\\j}").toBuild(); expect("\\text{\\i\\j}").toBuild();

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -207,6 +207,14 @@ NegativeSpace:
NestedFractions: | NestedFractions: |
\dfrac{\frac{a}{b}}{\frac{c}{d}}\dfrac{\dfrac{a}{b}} \dfrac{\frac{a}{b}}{\frac{c}{d}}\dfrac{\dfrac{a}{b}}
{\dfrac{c}{d}}\frac{\frac{a}{b}}{\frac{c}{d}} {\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: | Not: |
\begin{array}{l} \begin{array}{l}
\not=\not>\not\geq\not\in\not<\not\leq\not{abc} \\ \not=\not>\not\geq\not\in\not<\not\leq\not{abc} \\