mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-04 18:58:39 +00:00
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:
@@ -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
|
||||||
|
|
||||||
|
@@ -121,6 +121,8 @@ type ParseNodeTypes = {
|
|||||||
|},
|
|},
|
||||||
"cr": {|
|
"cr": {|
|
||||||
type: "cr",
|
type: "cr",
|
||||||
|
//newRow: boolean,
|
||||||
|
newLine: boolean,
|
||||||
size: ?ParseNode<*>,
|
size: ?ParseNode<*>,
|
||||||
|},
|
|},
|
||||||
"delimsizing": {|
|
"delimsizing": {|
|
||||||
|
@@ -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.
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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:
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
57
src/functions/cr.js
Normal 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;
|
||||||
|
},
|
||||||
|
});
|
@@ -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;
|
||||||
|
@@ -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 = "]" | "}" | "$" | "\\)" | "\\\\";
|
||||||
|
@@ -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}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
|
BIN
test/screenshotter/images/NewLine-chrome.png
Normal file
BIN
test/screenshotter/images/NewLine-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
test/screenshotter/images/NewLine-firefox.png
Normal file
BIN
test/screenshotter/images/NewLine-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
@@ -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} \\
|
||||||
|
Reference in New Issue
Block a user