Add \smash, laps, spaces, and phantoms (#833)

* Add \smash, laps, spaces, and phantoms

1. Support `\smash`, including the optional argument from AMS.

2. Change `\llap` and `\rlap` so that they render in text style. Repeat: This *changes* KaTeX behavior.

3. Add `\mathllap` and `\mathrlap`. These will act as they do  in `mathtools` and as in previous KaTeX versions of `\llap` and `\rlap`.

4. Add `\mathclap` and `\clap`.

5. Add `\hphantom` and \vphantom`.

6. Add `\thinspace`, `\medspace`, `\thickspace`

7. Add `\hspace`.

This work will resolve issue #270 and parts of #50 and #164.

A. Perlis has written a [concise description](https://www.math.lsu.edu/~aperlis/publications/mathclap/perlis_mathclap_24Jun2003.pdf) of items 1 thru 5. Except for `\smash`'s optional argument. It's described in the [AMS User's Guide](http://texdoc.net/texmf-dist/doc/latex/amsmath/amsldoc.pdf).

Item 6 also comes from the AMS User's Guide.

* Fix test spec

* Exploit makeVList for smash

* update smash and phantom screenshots for chrome

* Pick up review comments

* Change test from \llap to \mathlap

\llap is fundamentally a text-mode function. We should not expect it to work correctly when given math-mode arguments. So test \mathllap instead.

* Correct \llap macro

A correction. The previous macro returned an error if given an argument with math-mode content, such as x^2.

The corrected macro will not return an error. It will instead return well rendered math, but letters are in `\mathrm` font.

* update \llap, \rlap, \clap macros to use \textrm

* update Lap screenshots
This commit is contained in:
Ron Kok
2017-09-02 11:04:30 -07:00
committed by Kevin Barabash
parent 211c86d39b
commit 092aa0c767
13 changed files with 244 additions and 39 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)) {

View File

@@ -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

View File

@@ -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;

View File

@@ -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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -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}}}