diff --git a/docs/support_table.md b/docs/support_table.md
index 241a48ed..75b0fc24 100644
--- a/docs/support_table.md
+++ b/docs/support_table.md
@@ -928,7 +928,7 @@ table td {
|\small|$\small small$|`\small small`|
|\smallfrown|$\smallfrown$||
|\smallint|$\smallint$||
-|{smallmatrix}|Not supported||
+|{smallmatrix}|$\begin{smallmatrix} a & b \\ c & d \end{smallmatrix}$|`\begin{smallmatrix}`
`a & b \\`
`c & d`
`\end{smallmatrix}`|
|\smallsetminus|$\smallsetminus$||
|\smallsmile|$\smallsmile$||
|\smash|$\left(x^{\smash{2}}\right)$|`\left(x^{\smash{2}}\right)`|
@@ -965,7 +965,7 @@ table td {
|\subseteqq|$\subseteqq$||
|\subsetneq|$\subsetneq$||
|\subsetneqq|$\subsetneqq$||
-|\substack|Not supported||
+|\substack|$$\sum_{\substack{0 `a & b \\`
`c & d`
`\end{Bmatrix}`|$\def\arraystretch{1.5}\begin{array}{c:c:c} a & b & c \\ \hline d & e & f \\ \hdashline g & h & i \end{array}$|`\def\arraystretch{1.5}`
`\begin{array}{c:c:c}`
`a & b & c \\ \hline`
`d & e & f \\`
`\hdashline`
`g & h & i`
`\end{array}`
|$\begin{aligned} a&=b+c \\ d+e&=f \end{aligned}$ |`\begin{aligned}`
`a&=b+c \\`
`d+e&=f`
`\end{aligned}`|$\begin{alignedat}{2}10&x+&3&y=2\\3&x+&13&y=4\end{alignedat}$ |`\begin{alignedat}{2}`
`10&x+ &3&y = 2 \\`
` 3&x+&13&y = 4`
`\end{alignedat}`
|$\begin{gathered} a=b \\ e=b+c \end{gathered}$ |`\begin{gathered}`
`a=b \\ `
`e=b+c`
`\end{gathered}`|$x = \begin{cases} a &\text{if } b \\ c &\text{if } d \end{cases}$ |`x = \begin{cases}`
`a &\text{if } b \\`
`c &\text{if } d`
`\end{cases}`
+|$\begin{smallmatrix} a & b \\ c & d \end{smallmatrix}$ | `\begin{smallmatrix}`
`a & b \\`
`c & d`
`\end{smallmatrix}` | | |
@@ -205,7 +206,7 @@ In display math, KaTeX does not insert automatic line breaks. It ignores display
|:--------------|:----------------------------------------|:-----
|$x_n$ `x_n` |$\stackrel{!}{=}$ `\stackrel{!}{=}` |$a \atop b$ `a \atop b`
|$e^x$ `e^x` |$\overset{!}{=}$ `\overset{!}{=}` |$a\raisebox{0.25em}{b}c$ `a\raisebox{0.25em}{b}c`
-|$_u^o $ `_u^o `|$\underset{!}{=}$ `\underset{!}{=}`
+|$_u^o $ `_u^o `|$\underset{!}{=}$ `\underset{!}{=}` | $$\sum_{\substack{0 = function(group, options) {
// Horizontal spacing
const pt = 1 / options.fontMetrics().ptPerEm;
- const arraycolsep = 5 * pt; // \arraycolsep in article.cls
+ let arraycolsep = 5 * pt; // default value, i.e. \arraycolsep in article.cls
+ if (group.colSeparationType && group.colSeparationType === "small") {
+ // We're in a {smallmatrix}. Default column space is \thickspace,
+ // i.e. 5/18em = 0.2778em, per amsmath.dtx for {smallmatrix}.
+ // But that needs adjustment because LaTeX applies \scriptstyle to the
+ // entire array, including the colspace, but this function applies
+ // \scriptstyle only inside each element.
+ const localMultiplier = options.havingStyle(Style.SCRIPT).sizeMultiplier;
+ arraycolsep = 0.2778 * (localMultiplier / options.sizeMultiplier);
+ }
// Vertical spacing
const baselineskip = 12 * pt; // see size10.clo
@@ -379,7 +389,7 @@ const alignMap = {
};
const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
- const table = new mathMLTree.MathNode(
+ let table = new mathMLTree.MathNode(
"mtable", group.body.map(function(row) {
return new mathMLTree.MathNode(
"mtr", row.map(function(cell) {
@@ -401,7 +411,9 @@ const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
// The 0.16 and 0.09 values are found emprically. They produce an array
// similar to LaTeX and in which content does not interfere with \hines.
- const gap = 0.16 + group.arraystretch - 1 + (group.addJot ? 0.09 : 0);
+ const gap = (group.arraystretch === 0.5)
+ ? 0.1 // {smallmatrix}, {subarray}
+ : 0.16 + group.arraystretch - 1 + (group.addJot ? 0.09 : 0);
table.setAttribute("rowspacing", gap + "em");
// MathML table lines go only between cells.
@@ -463,6 +475,8 @@ const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
table.setAttribute("columnspacing", spacing.trim());
} else if (group.colSeparationType === "alignat") {
table.setAttribute("columnspacing", "0em");
+ } else if (group.colSeparationType === "small") {
+ table.setAttribute("columnspacing", "0.2778em");
} else {
table.setAttribute("columnspacing", "1em");
}
@@ -484,13 +498,18 @@ const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
table.setAttribute("rowlines", rowLines.trim());
}
- if (menclose === "") {
- return table;
- } else {
- const wrapper = new mathMLTree.MathNode("menclose", [table]);
- wrapper.setAttribute("notation", menclose.trim());
- return wrapper;
+ if (menclose !== "") {
+ table = new mathMLTree.MathNode("menclose", [table]);
+ table.setAttribute("notation", menclose.trim());
}
+
+ if (group.arraystretch && group.arraystretch < 1) {
+ // A small array. Wrap in scriptstyle so row gap is not too large.
+ table = new mathMLTree.MathNode("mstyle", [table]);
+ table.setAttribute("scriptlevel", "1");
+ }
+
+ return table;
};
// Convenience function for aligned and alignedat environments.
@@ -656,6 +675,63 @@ defineEnvironment({
mathmlBuilder,
});
+defineEnvironment({
+ type: "array",
+ names: ["smallmatrix"],
+ props: {
+ numArgs: 0,
+ },
+ handler(context) {
+ const payload = {arraystretch: 0.5};
+ const res = parseArray(context.parser, payload, "script");
+ res.colSeparationType = "small";
+ return res;
+ },
+ htmlBuilder,
+ mathmlBuilder,
+});
+
+defineEnvironment({
+ type: "array",
+ names: ["subarray"],
+ props: {
+ numArgs: 1,
+ },
+ handler(context, args) {
+ // Parsing of {subarray} is similar to {array}
+ const symNode = checkSymbolNodeType(args[0]);
+ const colalign: AnyParseNode[] =
+ symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").body;
+ const cols = colalign.map(function(nde) {
+ const node = assertSymbolNodeType(nde);
+ const ca = node.text;
+ // {subarray} only recognizes "l" & "c"
+ if ("lc".indexOf(ca) !== -1) {
+ return {
+ type: "align",
+ align: ca,
+ };
+ }
+ throw new ParseError("Unknown column alignment: " + ca, nde);
+ });
+ if (cols.length > 1) {
+ throw new ParseError("{subarray} can contain only one column");
+ }
+ let res = {
+ cols,
+ hskipBeforeAndAfter: false,
+ arraystretch: 0.5,
+ };
+ res = parseArray(context.parser, res, "script");
+ if (res.body[0].length > 1) {
+ throw new ParseError("{subarray} can contain only one column");
+ }
+ return res;
+ },
+ htmlBuilder,
+ mathmlBuilder,
+});
+
// A cases environment (in amsmath.sty) is almost equivalent to
// \def\arraystretch{1.2}%
// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right.
diff --git a/src/macros.js b/src/macros.js
index 5738958a..93cc3463 100644
--- a/src/macros.js
+++ b/src/macros.js
@@ -426,6 +426,9 @@ defineMacro("\\varPhi", "\\mathit{\\Phi}");
defineMacro("\\varPsi", "\\mathit{\\Psi}");
defineMacro("\\varOmega", "\\mathit{\\Omega}");
+//\newcommand{\substack}[1]{\subarray{c}#1\endsubarray}
+defineMacro("\\substack", "\\begin{subarray}{c}#1\\end{subarray}");
+
// \renewcommand{\colon}{\nobreak\mskip2mu\mathpunct{}\nonscript
// \mkern-\thinmuskip{:}\mskip6muplus1mu\relax}
defineMacro("\\colon", "\\nobreak\\mskip2mu\\mathpunct{}" +
diff --git a/test/katex-spec.js b/test/katex-spec.js
index 78852e75..a64d85d1 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -2582,6 +2582,43 @@ describe("An array environment", function() {
});
+describe("A subarray environment", function() {
+
+ it("should accept only a single alignment character", function() {
+ const parse = getParsed`\begin{subarray}{c}a \\ b\end{subarray}`;
+ expect(parse[0].type).toBe("array");
+ expect(parse[0].cols).toEqual([
+ {type: "align", align: "c"},
+ ]);
+ expect`\begin{subarray}{cc}a \\ b\end{subarray}`.not.toParse();
+ expect`\begin{subarray}{c}a & b \\ c & d\end{subarray}`.not.toParse();
+ expect`\begin{subarray}{c}a \\ b\end{subarray}`.toBuild();
+ });
+
+});
+
+describe("A substack function", function() {
+
+ it("should build", function() {
+ expect`\sum_{\substack{ 0