diff --git a/src/buildCommon.js b/src/buildCommon.js index ef16fbb6..2e3bc1ac 100644 --- a/src/buildCommon.js +++ b/src/buildCommon.js @@ -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", + }, }; /** diff --git a/src/buildHTML.js b/src/buildHTML.js index 7f835dee..5fc35e4d 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -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; } diff --git a/src/domTree.js b/src/domTree.js index d9255bc5..6a143eaf 100644 --- a/src/domTree.js +++ b/src/domTree.js @@ -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 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) diff --git a/src/katex.less b/src/katex.less index 93d77beb..b199a4d6 100644 --- a/src/katex.less +++ b/src/katex.less @@ -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 { diff --git a/src/macros.js b/src/macros.js index 8dbf8f62..37702eed 100644 --- a/src/macros.js +++ b/src/macros.js @@ -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 diff --git a/src/symbols.js b/src/symbols.js index 993c8ccb..cb410458 100644 --- a/src/symbols.js +++ b/src/symbols.js @@ -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"); diff --git a/test/katex-spec.js b/test/katex-spec.js index e82540c0..adc65f3b 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -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", () => { diff --git a/test/screenshotter/images/ArrayMode-chrome.png b/test/screenshotter/images/ArrayMode-chrome.png index b461c21e..1859b946 100644 Binary files a/test/screenshotter/images/ArrayMode-chrome.png and b/test/screenshotter/images/ArrayMode-chrome.png differ diff --git a/test/screenshotter/images/Baseline-chrome.png b/test/screenshotter/images/Baseline-chrome.png index 75394cdf..75a9bffc 100644 Binary files a/test/screenshotter/images/Baseline-chrome.png and b/test/screenshotter/images/Baseline-chrome.png differ diff --git a/test/screenshotter/images/BoldSpacing-chrome.png b/test/screenshotter/images/BoldSpacing-chrome.png index bfa90a0f..7a3e4bff 100644 Binary files a/test/screenshotter/images/BoldSpacing-chrome.png and b/test/screenshotter/images/BoldSpacing-chrome.png differ diff --git a/test/screenshotter/images/BoldSymbol-chrome.png b/test/screenshotter/images/BoldSymbol-chrome.png index 9617073b..001666d6 100644 Binary files a/test/screenshotter/images/BoldSymbol-chrome.png and b/test/screenshotter/images/BoldSymbol-chrome.png differ diff --git a/test/screenshotter/images/Cases-chrome.png b/test/screenshotter/images/Cases-chrome.png index 4a73db53..00dfef79 100644 Binary files a/test/screenshotter/images/Cases-chrome.png and b/test/screenshotter/images/Cases-chrome.png differ diff --git a/test/screenshotter/images/ColorSpacing-chrome.png b/test/screenshotter/images/ColorSpacing-chrome.png index 52005442..5419a1de 100644 Binary files a/test/screenshotter/images/ColorSpacing-chrome.png and b/test/screenshotter/images/ColorSpacing-chrome.png differ diff --git a/test/screenshotter/images/DeepFontSizing-chrome.png b/test/screenshotter/images/DeepFontSizing-chrome.png index 9e29083c..1b260601 100644 Binary files a/test/screenshotter/images/DeepFontSizing-chrome.png and b/test/screenshotter/images/DeepFontSizing-chrome.png differ diff --git a/test/screenshotter/images/DisplayMode-chrome.png b/test/screenshotter/images/DisplayMode-chrome.png index 873a02db..568e1884 100644 Binary files a/test/screenshotter/images/DisplayMode-chrome.png and b/test/screenshotter/images/DisplayMode-chrome.png differ diff --git a/test/screenshotter/images/Gathered-chrome.png b/test/screenshotter/images/Gathered-chrome.png index 5e1eb0a1..4adb43ce 100644 Binary files a/test/screenshotter/images/Gathered-chrome.png and b/test/screenshotter/images/Gathered-chrome.png differ diff --git a/test/screenshotter/images/HorizontalBraces-chrome.png b/test/screenshotter/images/HorizontalBraces-chrome.png index 956c954b..cf705f7c 100644 Binary files a/test/screenshotter/images/HorizontalBraces-chrome.png and b/test/screenshotter/images/HorizontalBraces-chrome.png differ diff --git a/test/screenshotter/images/LeftRightListStyling-chrome.png b/test/screenshotter/images/LeftRightListStyling-chrome.png index ad558c69..0e56e9f6 100644 Binary files a/test/screenshotter/images/LeftRightListStyling-chrome.png and b/test/screenshotter/images/LeftRightListStyling-chrome.png differ diff --git a/test/screenshotter/images/LimitControls-chrome.png b/test/screenshotter/images/LimitControls-chrome.png index 66aa0231..1b75bec5 100644 Binary files a/test/screenshotter/images/LimitControls-chrome.png and b/test/screenshotter/images/LimitControls-chrome.png differ diff --git a/test/screenshotter/images/LineBreak-chrome.png b/test/screenshotter/images/LineBreak-chrome.png new file mode 100644 index 00000000..835bdcb4 Binary files /dev/null and b/test/screenshotter/images/LineBreak-chrome.png differ diff --git a/test/screenshotter/images/LineBreak-firefox.png b/test/screenshotter/images/LineBreak-firefox.png new file mode 100644 index 00000000..50c38197 Binary files /dev/null and b/test/screenshotter/images/LineBreak-firefox.png differ diff --git a/test/screenshotter/images/MathDefaultFonts-chrome.png b/test/screenshotter/images/MathDefaultFonts-chrome.png index e8524e12..8478b4be 100644 Binary files a/test/screenshotter/images/MathDefaultFonts-chrome.png and b/test/screenshotter/images/MathDefaultFonts-chrome.png differ diff --git a/test/screenshotter/images/MathOp-chrome.png b/test/screenshotter/images/MathOp-chrome.png index 95a6267b..3b51dff6 100644 Binary files a/test/screenshotter/images/MathOp-chrome.png and b/test/screenshotter/images/MathOp-chrome.png differ diff --git a/test/screenshotter/images/NegativeSpace-chrome.png b/test/screenshotter/images/NegativeSpace-chrome.png index 150fab9e..25e15403 100644 Binary files a/test/screenshotter/images/NegativeSpace-chrome.png and b/test/screenshotter/images/NegativeSpace-chrome.png differ diff --git a/test/screenshotter/images/PrimeSpacing-chrome.png b/test/screenshotter/images/PrimeSpacing-chrome.png index e11344a2..d5503282 100644 Binary files a/test/screenshotter/images/PrimeSpacing-chrome.png and b/test/screenshotter/images/PrimeSpacing-chrome.png differ diff --git a/test/screenshotter/images/PrimeSuper-chrome.png b/test/screenshotter/images/PrimeSuper-chrome.png index 2ca6bdf9..eb2dade3 100644 Binary files a/test/screenshotter/images/PrimeSuper-chrome.png and b/test/screenshotter/images/PrimeSuper-chrome.png differ diff --git a/test/screenshotter/images/Raisebox-chrome.png b/test/screenshotter/images/Raisebox-chrome.png index cff4d5c0..e0c99d2f 100644 Binary files a/test/screenshotter/images/Raisebox-chrome.png and b/test/screenshotter/images/Raisebox-chrome.png differ diff --git a/test/screenshotter/images/SizingBaseline-chrome.png b/test/screenshotter/images/SizingBaseline-chrome.png index b7400a0a..51649149 100644 Binary files a/test/screenshotter/images/SizingBaseline-chrome.png and b/test/screenshotter/images/SizingBaseline-chrome.png differ diff --git a/test/screenshotter/images/SqrtRoot-chrome.png b/test/screenshotter/images/SqrtRoot-chrome.png index 9d592fe9..06b70705 100644 Binary files a/test/screenshotter/images/SqrtRoot-chrome.png and b/test/screenshotter/images/SqrtRoot-chrome.png differ diff --git a/test/screenshotter/images/StyleSwitching-chrome.png b/test/screenshotter/images/StyleSwitching-chrome.png index 7b5efde1..8aa63bc5 100644 Binary files a/test/screenshotter/images/StyleSwitching-chrome.png and b/test/screenshotter/images/StyleSwitching-chrome.png differ diff --git a/test/screenshotter/images/SupSubOffsets-chrome.png b/test/screenshotter/images/SupSubOffsets-chrome.png index 2bd4a106..81f09685 100644 Binary files a/test/screenshotter/images/SupSubOffsets-chrome.png and b/test/screenshotter/images/SupSubOffsets-chrome.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 3ecfecf9..64d318be 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -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} \\