diff --git a/src/buildHTML.js b/src/buildHTML.js index 129f6877..ddcbd19c 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -762,20 +762,57 @@ groupTypes.spacing = function(group, options) { } }; -groupTypes.llap = function(group, options) { - const inner = makeSpan( - ["inner"], [buildGroup(group.value.body, options)]); +groupTypes.lap = function(group, options) { + // mathllap, mathrlap, mathclap + let inner; + if (group.value.alignment === "clap") { + // ref: https://www.math.lsu.edu/~aperlis/publications/mathclap/ + inner = makeSpan([], [buildGroup(group.value.body, options)]); + // wrap, since CSS will center a .clap > .inner > span + inner = makeSpan(["inner"], [inner], options); + } else { + inner = makeSpan( + ["inner"], [buildGroup(group.value.body, options)]); + } const fix = makeSpan(["fix"], []); return makeSpan( - ["mord", "llap"], [inner, fix], options); + ["mord", group.value.alignment], [inner, fix], options); }; -groupTypes.rlap = function(group, options) { - const inner = makeSpan( - ["inner"], [buildGroup(group.value.body, options)]); - const fix = makeSpan(["fix"], []); - return makeSpan( - ["mord", "rlap"], [inner, fix], options); +groupTypes.smash = function(group, options) { + const node = makeSpan(["mord"], [buildGroup(group.value.body, options)]); + + if (!group.value.smashHeight && !group.value.smashDepth) { + return node; + } + + if (group.value.smashHeight) { + node.height = 0; + // In order to influence makeVList, we have to reset the children. + if (node.children) { + for (let i = 0; i < node.children.length; i++) { + node.children[i].height = 0; + } + } + } + + if (group.value.smashDepth) { + node.depth = 0; + if (node.children) { + for (let i = 0; i < node.children.length; i++) { + node.children[i].depth = 0; + } + } + } + + // At this point, we've reset the TeX-like height and depth values. + // But the span still has an HTML line height. + // makeVList applies "display: table-cell", which prevents the browser + // from acting on that line height. So we'll call makeVList now. + + return buildCommon.makeVList([ + {type: "elem", elem: node}, + ], "firstBaseline", null, options); }; groupTypes.op = function(group, options) { @@ -1698,6 +1735,34 @@ groupTypes.phantom = function(group, options) { return new buildCommon.makeFragment(elements); }; +groupTypes.hphantom = function(group, options) { + let node = makeSpan( + [], [buildGroup(group.value.body, options.withPhantom())]); + node.height = 0; + node.depth = 0; + if (node.children) { + for (let i = 0; i < node.children.length; i++) { + node.children[i].height = 0; + node.children[i].depth = 0; + } + } + + // See smash for comment re: use of makeVList + node = buildCommon.makeVList([ + {type: "elem", elem: node}, + ], "firstBaseline", null, options); + + return node; +}; + +groupTypes.vphantom = function(group, options) { + const inner = makeSpan( + ["inner"], [buildGroup(group.value.body, options.withPhantom())]); + const fix = makeSpan(["fix"], []); + return makeSpan( + ["mord", "rlap"], [inner, fix], options); +}; + groupTypes.mclass = function(group, options) { const elements = buildExpression(group.value.value, options, true); diff --git a/src/buildMathML.js b/src/buildMathML.js index 9979397d..79e4aa78 100644 --- a/src/buildMathML.js +++ b/src/buildMathML.js @@ -621,21 +621,31 @@ groupTypes.kern = function(group) { return node; }; -groupTypes.llap = function(group, options) { +groupTypes.lap = function(group, options) { + // mathllap, mathrlap, mathclap const node = new mathMLTree.MathNode( "mpadded", [buildGroup(group.value.body, options)]); - node.setAttribute("lspace", "-1width"); + if (group.value.alignment !== "rlap") { + const offset = (group.value.alignment === "llap" ? "-1" : "-0.5"); + node.setAttribute("lspace", offset + "width"); + } node.setAttribute("width", "0px"); return node; }; -groupTypes.rlap = function(group, options) { +groupTypes.smash = function(group, options) { const node = new mathMLTree.MathNode( "mpadded", [buildGroup(group.value.body, options)]); - node.setAttribute("width", "0px"); + if (group.value.smashHeight) { + node.setAttribute("height", "0px"); + } + + if (group.value.smashDepth) { + node.setAttribute("depth", "0px"); + } return node; }; @@ -645,6 +655,20 @@ groupTypes.phantom = function(group, options) { return new mathMLTree.MathNode("mphantom", inner); }; +groupTypes.hphantom = function(group, options) { + const inner = buildExpression(group.value.value, options); + const node = new mathMLTree.MathNode("mphantom", inner); + node.setAttribute("height", "0px"); + return node; +}; + +groupTypes.vphantom = function(group, options) { + const inner = buildExpression(group.value.value, options); + const node = new mathMLTree.MathNode("mphantom", inner); + node.setAttribute("width", "0px"); + return node; +}; + groupTypes.mclass = function(group, options) { const inner = buildExpression(group.value.value, options); return new mathMLTree.MathNode("mstyle", inner); diff --git a/src/functions.js b/src/functions.js index 6d09a82d..5db624c2 100644 --- a/src/functions.js +++ b/src/functions.js @@ -234,13 +234,14 @@ defineFunction("\\KaTeX", { }; }); -defineFunction("\\phantom", { +defineFunction(["\\phantom", "\\hphantom", "\\vphantom"], { numArgs: 1, }, function(context, args) { const body = args[0]; return { - type: "phantom", + type: context.funcName.slice(1), value: ordargument(body), + body: body, }; }); @@ -516,18 +517,59 @@ defineFunction([ }; }); -// Left and right overlap functions -defineFunction(["\\llap", "\\rlap"], { +// Horizontal overlap functions +defineFunction(["\\mathllap", "\\mathrlap", "\\mathclap"], { numArgs: 1, allowedInText: true, }, function(context, args) { const body = args[0]; return { - type: context.funcName.slice(1), + type: "lap", + alignment: context.funcName.slice(5), body: body, }; }); +// smash, with optional [tb], as in AMS +defineFunction("\\smash", { + numArgs: 1, + numOptionalArgs: 1, + allowedInText: true, +}, function(context, args) { + let smashHeight = false; + let smashDepth = false; + const tbArg = args[0]; + if (tbArg) { + // Optional [tb] argument is engaged. + // ref: amsmath: \renewcommand{\smash}[1][tb]{% + // def\mb@t{\ht}\def\mb@b{\dp}\def\mb@tb{\ht\z@\z@\dp}% + let letter = ""; + for (let i = 0; i < tbArg.value.length; ++i) { + letter = tbArg.value[i].value; + if (letter === "t") { + smashHeight = true; + } else if (letter === "b") { + smashDepth = true; + } else { + smashHeight = false; + smashDepth = false; + break; + } + } + } else { + smashHeight = true; + smashDepth = true; + } + + const body = args[1]; + return { + type: "smash", + body: body, + smashHeight: smashHeight, + smashDepth: smashDepth, + }; +}); + // Delimiter functions const checkDelimiter = function(delim, context) { if (utils.contains(delimiters, delim.value)) { diff --git a/src/macros.js b/src/macros.js index 9d172e85..63ef710e 100644 --- a/src/macros.js +++ b/src/macros.js @@ -19,6 +19,11 @@ defineMacro("\\endgroup", "}"); // (In TeX, the mu unit works only with \mkern.) defineMacro("\\mkern", "\\kern"); +// \llap and \rlap render their contents in text mode +defineMacro("\\llap", "\\mathllap{\\textrm{#1}}"); +defineMacro("\\rlap", "\\mathrlap{\\textrm{#1}}"); +defineMacro("\\clap", "\\mathclap{\\textrm{#1}}"); + ////////////////////////////////////////////////////////////////////// // amsmath.sty @@ -38,6 +43,19 @@ defineMacro("\\iff", "\\;\\Longleftrightarrow\\;"); defineMacro("\\implies", "\\;\\Longrightarrow\\;"); defineMacro("\\impliedby", "\\;\\Longleftarrow\\;"); +// http://texdoc.net/texmf-dist/doc/latex/amsmath/amsmath.pdf +defineMacro("\\thinspace", "\\,"); // \let\thinspace\, +defineMacro("\\medspace", "\\:"); // \let\medspace\: +defineMacro("\\thickspace", "\\;"); // \let\thickspace\; + +////////////////////////////////////////////////////////////////////// +// LaTeX source2e + +// \DeclareRobustCommand\hspace{\@ifstar\@hspacer\@hspace} +// \def\@hspace#1{\hskip #1\relax} +// KaTeX doesn't do line breaks, so \hspace is the same as \kern +defineMacro("\\hspace", "\\kern{#1}"); + ////////////////////////////////////////////////////////////////////// // mathtools.sty diff --git a/static/katex.less b/static/katex.less index 1a97a23e..b009b7bf 100644 --- a/static/katex.less +++ b/static/katex.less @@ -350,7 +350,8 @@ } .llap, - .rlap { + .rlap, + .clap { width: 0; position: relative; @@ -367,10 +368,16 @@ right: 0; } - .rlap > .inner { + .rlap > .inner, + .clap > .inner { left: 0; } + .clap > .inner > span { + margin-left: -50%; + margin-right: 50%; + } + .katex-logo { .a { font-size: 0.75em; diff --git a/test/katex-spec.js b/test/katex-spec.js index 06e78f81..5f217990 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -1253,15 +1253,15 @@ describe("A TeX-compliant parser", function() { "\\frac x \\frac y z", "\\frac \\sqrt x y", "\\frac x \\sqrt y", - "\\frac \\llap x y", - "\\frac x \\llap y", + "\\frac \\mathllap x y", + "\\frac x \\mathllap y", // This actually doesn't work in real TeX, but it is suprisingly // hard to get this to correctly work. So, we take hit of very small // amounts of non-compatiblity in order for the rest of the tests to // work // "\\llap \\frac x y", - "\\llap \\llap x", - "\\sqrt \\llap x", + "\\mathllap \\mathllap x", + "\\sqrt \\mathllap x", ]; for (let i = 0; i < badArguments.length; i++) { @@ -1275,11 +1275,11 @@ describe("A TeX-compliant parser", function() { "\\frac x {\\frac y z}", "\\frac {\\sqrt x} y", "\\frac x {\\sqrt y}", - "\\frac {\\llap x} y", - "\\frac x {\\llap y}", - "\\llap {\\frac x y}", - "\\llap {\\llap x}", - "\\sqrt {\\llap x}", + "\\frac {\\mathllap x} y", + "\\frac x {\\mathllap y}", + "\\mathllap {\\frac x y}", + "\\mathllap {\\mathllap x}", + "\\sqrt {\\mathllap x}", ]; for (let i = 0; i < goodArguments.length; i++) { @@ -1290,9 +1290,9 @@ describe("A TeX-compliant parser", function() { it("should fail when sup/subscripts require arguments", function() { const badSupSubscripts = [ "x^\\sqrt x", - "x^\\llap x", + "x^\\mathllap x", "x_\\sqrt x", - "x_\\llap x", + "x_\\mathllap x", ]; for (let i = 0; i < badSupSubscripts.length; i++) { @@ -1303,9 +1303,9 @@ describe("A TeX-compliant parser", function() { it("should work when sup/subscripts arguments have braces", function() { const goodSupSubscripts = [ "x^{\\sqrt x}", - "x^{\\llap x}", + "x^{\\mathllap x}", "x_{\\sqrt x}", - "x_{\\llap x}", + "x_{\\mathllap x}", ]; for (let i = 0; i < goodSupSubscripts.length; i++) { @@ -1341,7 +1341,7 @@ describe("A TeX-compliant parser", function() { const badLeftArguments = [ "\\frac \\left( x \\right) y", "\\frac x \\left( y \\right)", - "\\llap \\left( x \\right)", + "\\mathllap \\left( x \\right)", "\\sqrt \\left( x \\right)", "x^\\left( x \\right)", ]; @@ -1355,7 +1355,7 @@ describe("A TeX-compliant parser", function() { const goodLeftArguments = [ "\\frac {\\left( x \\right)} y", "\\frac x {\\left( y \\right)}", - "\\llap {\\left( x \\right)}", + "\\mathllap {\\left( x \\right)}", "\\sqrt {\\left( x \\right)}", "x^{\\left( x \\right)}", ]; @@ -2088,6 +2088,10 @@ describe("A phantom parser", function() { expect("\\phantom{x^2}").toParse(); expect("\\phantom{x}^2").toParse(); expect("\\phantom x").toParse(); + expect("\\hphantom{x}").toParse(); + expect("\\hphantom{x^2}").toParse(); + expect("\\hphantom{x}^2").toParse(); + expect("\\hphantom x").toParse(); }); it("should build a phantom node", function() { @@ -2104,6 +2108,11 @@ describe("A phantom builder", function() { expect("\\phantom{x^2}").toBuild(); expect("\\phantom{x}^2").toBuild(); expect("\\phantom x").toBuild(); + + expect("\\hphantom{x}").toBuild(); + expect("\\hphantom{x^2}").toBuild(); + expect("\\hphantom{x}^2").toBuild(); + expect("\\hphantom x").toBuild(); }); it("should make the children transparent", function() { @@ -2121,6 +2130,45 @@ describe("A phantom builder", function() { }); }); +describe("A smash parser", function() { + it("should not fail", function() { + expect("\\smash{x}").toParse(); + expect("\\smash{x^2}").toParse(); + expect("\\smash{x}^2").toParse(); + expect("\\smash x").toParse(); + + expect("\\smash[b]{x}").toParse(); + expect("\\smash[b]{x^2}").toParse(); + expect("\\smash[b]{x}^2").toParse(); + expect("\\smash[b] x").toParse(); + + expect("\\smash[]{x}").toParse(); + expect("\\smash[]{x^2}").toParse(); + expect("\\smash[]{x}^2").toParse(); + expect("\\smash[] x").toParse(); + }); + + it("should build a smash node", function() { + const parse = getParsed("\\smash{x}")[0]; + + expect(parse.type).toEqual("smash"); + }); +}); + +describe("A smash builder", function() { + it("should not fail", function() { + expect("\\smash{x}").toBuild(); + expect("\\smash{x^2}").toBuild(); + expect("\\smash{x}^2").toBuild(); + expect("\\smash x").toBuild(); + + expect("\\smash[b]{x}").toBuild(); + expect("\\smash[b]{x^2}").toBuild(); + expect("\\smash[b]{x}^2").toBuild(); + expect("\\smash[b] x").toBuild(); + }); +}); + describe("A parser error", function() { it("should report the position of an error", function() { try { diff --git a/test/screenshotter/images/Lap-chrome.png b/test/screenshotter/images/Lap-chrome.png index 5e5d36bd..a8d14b29 100644 Binary files a/test/screenshotter/images/Lap-chrome.png and b/test/screenshotter/images/Lap-chrome.png differ diff --git a/test/screenshotter/images/Lap-firefox.png b/test/screenshotter/images/Lap-firefox.png index 6b7ae932..17c982d8 100644 Binary files a/test/screenshotter/images/Lap-firefox.png and b/test/screenshotter/images/Lap-firefox.png differ diff --git a/test/screenshotter/images/Phantom-chrome.png b/test/screenshotter/images/Phantom-chrome.png index da7a6d68..9f1db3f6 100644 Binary files a/test/screenshotter/images/Phantom-chrome.png and b/test/screenshotter/images/Phantom-chrome.png differ diff --git a/test/screenshotter/images/Phantom-firefox.png b/test/screenshotter/images/Phantom-firefox.png index 730f9a82..e9f70656 100644 Binary files a/test/screenshotter/images/Phantom-firefox.png and b/test/screenshotter/images/Phantom-firefox.png differ diff --git a/test/screenshotter/images/Smash-chrome.png b/test/screenshotter/images/Smash-chrome.png new file mode 100644 index 00000000..8d0a0551 Binary files /dev/null and b/test/screenshotter/images/Smash-chrome.png differ diff --git a/test/screenshotter/images/Smash-firefox.png b/test/screenshotter/images/Smash-firefox.png new file mode 100644 index 00000000..2e58032b Binary files /dev/null and b/test/screenshotter/images/Smash-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 62a52c1d..1b7abf48 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -107,7 +107,7 @@ KaTeX: \KaTeX Kern: tex: \frac{a\kern{1em}b}{c}a\kern{1em}b\kern{1ex}c\kern{-0.25em}d nolatex: LaTeX fails to typeset this, “Missing number, treated as zero.” -Lap: ab\llap{f}cd\rlap{g}h +Lap: ab\mathllap{f}cd\mathrlap{g}hij\mathclap{k}lm \; ab\llap{f}cd\rlap{g}hij\clap{k}lm LargeRuleNumerator: \frac{\textcolor{blue}{\rule{1em}{2em}}}{x} LeftRight: \left( x^2 \right) \left\{ x^{x^{x^{x^x}}} \right. LeftRightListStyling: a+\left(x+y\right)-x @@ -178,7 +178,7 @@ OverUnderset: | a+b+c+d\overset{b+c=0}\longrightarrow a+d\\ \overset { x = y } { \sqrt { a b } } \end{array} -Phantom: \dfrac{1+\phantom{x^{\blue{2}}} = x}{1+x^{\blue{2}} = x} +Phantom: \dfrac{1+\phantom{x^{\blue{2}}} = x}{1+x^{\blue{2}} = x} \left(\vphantom{\int_t} zzz \right) \left( X \hphantom{\frac{\frac X X}{X}} \right) PrimeSpacing: f'+f_2'+f^{f'} PrimeSuper: x'^2+x'''^2+x'^2_3+x_3'^2 RelativeUnits: | @@ -190,7 +190,7 @@ RelativeUnits: | \rule{1em}{1em}^{\rule{1em}{1em}}\rule{18mu}{18mu}^{\rule{18mu}{18mu}} & {\footnotesize\rule{1em}{1em}^{\rule{1em}{1em}}\rule{18mu}{18mu}^{\rule{18mu}{18mu}}} \end{array} -RlapBug: \frac{\rlap{x}}{2} +RlapBug: \frac{\mathrlap{x}}{2} Rule: \rule{1em}{0.5em}\rule{1ex}{2ex}\rule{1em}{1ex}\rule{1em}{0.431ex} SizingBaseline: tex: '{\tiny a+b}a+b{\Huge a+b}' @@ -198,6 +198,7 @@ SizingBaseline: post: M Sizing: | {\Huge x}{\LARGE y}{\normalsize z}{\scriptsize w} +Smash: \left( X^{\smash 2} \right) \sqrt{\smash[b]{y}} Spacing: ^3+[-1][1-1]1=1(=1)\lvert a\rvert~b Sqrt: | \sqrt{\sqrt{\sqrt{x}}}_{\sqrt{\sqrt{x}}}^{\sqrt{\sqrt{\sqrt{x}}}