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

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

View File

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

View File

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

View File

@@ -132,7 +132,7 @@ export type FunctionSpec<NODETYPE: NodeType> = {|
// 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;
//
// results in a confusing flow typing error:

View File

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

View File

@@ -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"], {

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 {
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;

View File

@@ -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 = "]" | "}" | "$" | "\\)" | "\\\\";