Line breaks for inline formulas (#1287)

* Line breaks for inline formulas

* Basic support for \allowbreak and \nobreak

* Fix spacing around \nobreak, and add documentation

* Backwards-compatibility _getBuilt to fix tests

* Put operator spacing on same line as operator

* One approach to ~

* Simplify \allowbreak/\nobreak, make ~/\nobreakspace prevent line breaks

* Adapt to #1295

* Prevent wrapping within a .base

* Implement \hspace* properly

* Fix flow error

* Update comment for regularSpace

* Update screenshots

* Move `width: min-content` from .katex into .base

* Fix screenshot

* Add min-width rule to .vlist-s

* Factor out hasClass method

* Cleanup nobreak test

* Pull out buildHTMLUnbreakable

* Fix \hspace* test (no longer the same as \hspace)

* Fix \nobreak handling

* Add screenshot test
This commit is contained in:
Erik Demaine
2018-05-10 19:44:26 -04:00
committed by Kevin Barabash
parent 34e6458245
commit 523df299e5
32 changed files with 149 additions and 43 deletions

View File

@@ -679,16 +679,31 @@ const spacingFunctions: {[string]: {| size: string, className: string |}} = {
size: "-0.16667em",
className: "negativethinspace",
},
"\\nobreak": {
size: "0em",
className: "nobreak",
},
"\\allowbreak": {
size: "0em",
className: "allowbreak",
},
};
// A lookup table to determine whether a spacing function/symbol should be
// treated like a regular space character.
const regularSpace: {[string]: boolean} = {
" ": true,
"\\ ": true,
"~": true,
"\\space": true,
"\\nobreakspace": true,
// treated like a regular space character. If a symbol or command is a key
// in this table, then it should be a regular space character. Furthermore,
// the associated value may have a `className` specifying an extra CSS class
// to add to the created `span`.
const regularSpace: {[string]: { className?: string }} = {
" ": {},
"\\ ": {},
"~": {
className: "nobreak",
},
"\\space": {},
"\\nobreakspace": {
className: "nobreak",
},
};
/**

View File

@@ -205,7 +205,7 @@ export const getTypeOfDomTree = function(node, side = "right") {
// 'mtight' indicates that the node is script or scriptscript style.
export const isLeftTight = function(node) {
node = getOutermostNode(node, "left");
return utils.contains(node.classes, "mtight");
return node.hasClass("mtight");
};
/**
@@ -395,13 +395,16 @@ export const groupTypes = {
spacing(group, options) {
if (buildCommon.regularSpace.hasOwnProperty(group.value)) {
const className = buildCommon.regularSpace[group.value].className;
// Spaces are generated by adding an actual space. Each of these
// things has an entry in the symbols table, so these will be turned
// into appropriate outputs.
if (group.mode === "text") {
return buildCommon.makeOrd(group, options, "textord");
const ord = buildCommon.makeOrd(group, options, "textord");
ord.classes.push(className);
return ord;
} else {
return makeSpan(["mspace"],
return makeSpan(["mspace", className],
[buildCommon.mathsym(group.value, group.mode, options)],
options);
}
@@ -629,21 +632,18 @@ export const buildGroup = function(group, options, baseOptions) {
};
/**
* Take an entire parse tree, and build it into an appropriate set of HTML
* nodes.
* Combine an array of HTML DOM nodes (e.g., the output of `buildExpression`)
* into an unbreakable HTML node of class .base, with proper struts to
* guarantee correct vertical extent. `buildHTML` calls this repeatedly to
* make up the entire expression as a sequence of unbreakable units.
*/
export default function buildHTML(tree, options) {
// buildExpression is destructive, so we need to make a clone
// of the incoming tree so that it isn't accidentally changed
tree = JSON.parse(JSON.stringify(tree));
function buildHTMLUnbreakable(children, options) {
// Compute height and depth of this chunk.
const body = makeSpan(["base"], children, options);
// Build the expression contained in the tree
const expression = buildExpression(tree, options, true);
const body = makeSpan(["base"], expression, options);
// Add struts, which ensure that the top of the HTML element falls at the
// height of the expression, and the bottom of the HTML element falls at the
// depth of the expression.
// Add struts, which ensure that the top of the HTML element falls at
// the height of the expression, and the bottom of the HTML element
// falls at the depth of the expression.
const topStrut = makeSpan(["strut"]);
const bottomStrut = makeSpan(["strut", "bottom"]);
@@ -654,10 +654,60 @@ export default function buildHTML(tree, options) {
// normal place) so we use an absolute value for vertical-align instead
bottomStrut.style.verticalAlign = -body.depth + "em";
// Wrap the struts and body together
const htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]);
body.children.unshift(topStrut, bottomStrut);
return body;
}
/**
* Take an entire parse tree, and build it into an appropriate set of HTML
* nodes.
*/
export default function buildHTML(tree, options) {
// buildExpression is destructive, so we need to make a clone
// of the incoming tree so that it isn't accidentally changed
tree = JSON.parse(JSON.stringify(tree));
// Build the expression contained in the tree
const expression = buildExpression(tree, options, true);
const htmlNode = makeSpan(["katex-html"], []);
htmlNode.setAttribute("aria-hidden", "true");
// Create one base node for each chunk between potential line breaks.
// The TeXBook [p.173] says "A formula will be broken only after a
// relation symbol like $=$ or $<$ or $\rightarrow$, or after a binary
// operation symbol like $+$ or $-$ or $\times$, where the relation or
// binary operation is on the ``outer level'' of the formula (i.e., not
// enclosed in {...} and not part of an \over construction)."
let parts = [];
for (let i = 0; i < expression.length; i++) {
parts.push(expression[i]);
if (expression[i].hasClass("mbin") ||
expression[i].hasClass("mrel") ||
expression[i].hasClass("allowbreak")) {
// Put any post-operator glue on same line as operator.
// Watch for \nobreak along the way.
let nobreak = false;
while (i < expression.length - 1 &&
expression[i + 1].hasClass("mspace")) {
i++;
parts.push(expression[i]);
if (expression[i].hasClass("nobreak")) {
nobreak = true;
}
}
// Don't allow break if \nobreak among the post-operator glue.
if (!nobreak) {
htmlNode.children.push(buildHTMLUnbreakable(parts, options));
parts = [];
}
}
}
if (parts.length > 0) {
htmlNode.children.push(buildHTMLUnbreakable(parts, options));
}
return htmlNode;
}

View File

@@ -40,6 +40,7 @@ export interface HtmlDomNode extends VirtualNodeInterface {
depth: number;
maxFontSize: number;
hasClass(className: string): boolean;
tryCombine(sibling: HtmlDomNode): boolean;
}
@@ -104,6 +105,10 @@ class span<ChildType: VirtualNodeInterface> implements HtmlDomNode {
this.attributes[attribute] = value;
}
hasClass(className: string): boolean {
return utils.contains(this.classes, className);
}
tryCombine(sibling: HtmlDomNode): boolean {
return false;
}
@@ -235,6 +240,10 @@ class anchor implements HtmlDomNode {
this.attributes[attribute] = value;
}
hasClass(className: string): boolean {
return utils.contains(this.classes, className);
}
tryCombine(sibling: HtmlDomNode): boolean {
return false;
}
@@ -344,6 +353,10 @@ class documentFragment implements HtmlDomNode {
this.maxFontSize = 0;
}
hasClass(className: string): boolean {
return utils.contains(this.classes, className);
}
tryCombine(sibling: HtmlDomNode): boolean {
return false;
}
@@ -439,6 +452,10 @@ class symbolNode implements HtmlDomNode {
}
}
hasClass(className: string): boolean {
return utils.contains(this.classes, className);
}
tryCombine(sibling: HtmlDomNode): boolean {
if (!sibling
|| !(sibling instanceof symbolNode)

View File

@@ -25,13 +25,17 @@
> .katex {
display: inline-block;
text-align: initial;
white-space: nowrap;
> .katex-html {
display: inline-block;
}
}
}
.katex {
font: normal 1.21em KaTeX_Main, Times New Roman, serif;
line-height: 1.2;
white-space: nowrap;
// Protect elements inside .katex from inheriting text-indent.
text-indent: 0;
@@ -39,20 +43,10 @@
// Prevent a rendering bug that misplaces \vec in Chrome.
text-rendering: auto;
// Fix width of containers of negative spaces, working around Chrome bug.
width: min-content;
// Prevent background resetting on elements in Windows's high-contrast
// mode, while still allowing background/foreground setting on root .katex
* { -ms-high-contrast-adjust: none !important; }
.katex-html {
// Making .katex inline-block allows line breaks before and after,
// which is undesireable ("to $x$,"). Instead, adjust the .katex-html
// style and put nowrap on the inline .katex element.
display: inline-block;
}
.katex-mathml {
// Accessibility hack to only show to screen readers
// Found at: http://a11yproject.com/posts/how-to-hide-content/
@@ -68,6 +62,10 @@
.base {
position: relative;
display: inline-block;
white-space: nowrap;
// Fix width of containers of negative spaces, working around Chrome bug.
width: min-content;
}
.strut {
@@ -206,11 +204,13 @@
// issues; absolute units prevent user font-size overrides from breaking
// rendering. Safari refuses to make the box zero-width, so we give it
// a known width and compensate with negative right margin on the
// inline-table.
// inline-table. To prevent the "width: min-content" Chrome workaround
// from shrinking this box, we also set min-width.
display: table-cell;
vertical-align: bottom;
font-size: 1px;
width: 2px;
min-width: 2px;
}
.msupsub {

View File

@@ -390,8 +390,11 @@ defineMacro("\\KaTeX",
// \DeclareRobustCommand\hspace{\@ifstar\@hspacer\@hspace}
// \def\@hspace#1{\hskip #1\relax}
// KaTeX doesn't do line breaks, so \hspace and \hspace* are the same as \kern
defineMacro("\\hspace", "\\@ifstar\\kern\\kern");
// \def\@hspacer#1{\vrule \@width\z@\nobreak
// \hskip #1\hskip \z@skip}
defineMacro("\\hspace", "\\@ifstar\\@hspacer\\@hspace");
defineMacro("\\@hspace", "\\hskip #1\\relax");
defineMacro("\\@hspacer", "\\rule{0pt}{0pt}\\hskip #1\\relax");
//////////////////////////////////////////////////////////////////////
// mathtools.sty

View File

@@ -594,6 +594,8 @@ defineSymbol(text, main, spacing, null, "\\qquad");
defineSymbol(text, main, spacing, null, "\\quad");
defineSymbol(text, main, spacing, "\u00a0", "\\space");
defineSymbol(text, main, spacing, "\u00a0", "\\nobreakspace");
defineSymbol(math, main, spacing, null, "\\nobreak");
defineSymbol(math, main, spacing, null, "\\allowbreak");
defineSymbol(math, main, punct, ",", ",");
defineSymbol(math, main, punct, ";", ";");
defineSymbol(math, main, punct, ":", "\\colon");

View File

@@ -53,8 +53,13 @@ const _getBuilt = function(expr, settings) {
// grab the root node of the HTML rendering
const builtHTML = rootNode.children[1];
// Remove the outer .katex and .katex-inner layers
return builtHTML.children[2].children;
// combine the non-strut children of all base spans
const children = [];
for (let i = 0; i < builtHTML.children.length; i++) {
children.push(...builtHTML.children[i].children.filter(
(node) => node.classes.indexOf("strut") < 0));
}
return children;
};
/**
@@ -2889,9 +2894,9 @@ describe("A macro expander", function() {
// This may change in the future, if we support the extra features of
// \hspace.
it("should treat \\hspace, \\hspace*, \\hskip like \\kern", function() {
it("should treat \\hspace, \\hskip like \\kern", function() {
expect("\\hspace{1em}").toParseLike("\\kern1em");
expect("\\hspace*{1em}").toParseLike("\\kern1em");
expect("\\hskip{1em}").toParseLike("\\kern1em");
});
it("should expand \\limsup as expected", () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -144,6 +144,20 @@ LeftRightStyleSizing: |
LimitControls: |
\displaystyle\int\limits_2^3 3x^2\,dx + \sum\nolimits^n_{i=1}i +
\textstyle\int\limits_x^y z
LineBreak: |
\frac{x^2}{y^2} + z^2 =
z^2 + \frac{x^2}{y^2} =
\frac{x^2}{y^2} +\nobreak z^2 =
z^2 + \frac{x^2}{y^2} =
\frac{x^2}{y^2} + ~ z^2 =
z^2 + \frac{x^2}{y^2} =
\frac{x^2}{y^2} + \hspace{1em} z^2 =
z^2 + \frac{x^2}{y^2} =
\frac{x^2}{y^2} + z^2 = \hspace*{1em}
z^2 + \frac{x^2}{y^2} =
\frac{x^2}{y^2} + z^2 =
hi \allowbreak there =
hi \allowbreak there
LowerAccent: |
\begin{matrix}
\underleftarrow{AB} \quad \underrightarrow{AB} \quad \underleftrightarrow{AB} \quad \undergroup{AB} \\