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