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