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
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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");
|
||||
|
@@ -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", () => {
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
test/screenshotter/images/LineBreak-chrome.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
test/screenshotter/images/LineBreak-firefox.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -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} \\
|
||||
|