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