Repository: Khan/KaTeX
Pull request #1381 (2018-05-29 18:42:38)

Support \arraystretch as a macro definition
With this PR, `\arraystretch` can be set via `settings.macros` or using `\def` or `\gdef`.  This was pretty easy now that we have `\def` and `\gdef`, as `arraystretch` was already supported internally.

This partially addresses #1320 (which is really asking for `\baselinestretch`).  `\arraystretch` was also mentioned in #687, though that issue is more about actual lengths, whereas `\arraystretch` is a macro not a length.

I did need to write two helpers: `expandMacro` and `expandMacroAsText` in `MacroExpander`.  These let a function or environment like `{array}` get the current value of `\arraystretch`, fully expanded, and use it to compute a number.  A cool example:

![image](https://user-images.githubusercontent.com/2218736/40678403-7948c0be-634e-11e8-97cb-759297ba9724.png)


codecov[bot]: # [Codecov](https://codecov.io/gh/Khan/KaTeX/pull/1381?src=pr&el=h1) Report
> Merging [#1381](https://codecov.io/gh/Khan/KaTeX/pull/1381?src=pr&el=desc) into [master](https://codecov.io/gh/Khan/KaTeX/commit/3796faaf5e17424aa4655958afa39221180e4db0?src=pr&el=desc) will **decrease** coverage by `0.19%`.
> The diff coverage is `42.85%`.


[![Impacted file tree graph](https://codecov.io/gh/Khan/KaTeX/pull/1381/graphs/tree.svg?src=pr&token=vfDnj2P77o&width=650&height=150)](https://codecov.io/gh/Khan/KaTeX/pull/1381?src=pr&el=tree)

```diff
@@           Coverage Diff            @@
##           master   #1381     +/-   ##
========================================
- Coverage   82.29%   82.1%   -0.2%     
========================================
  Files          77      77             
  Lines        4242    4263     +21     
  Branches      732     738      +6     
========================================
+ Hits         3491    3500      +9     
- Misses        649     659     +10     
- Partials      102     104      +2
```


| [Impacted Files](https://codecov.io/gh/Khan/KaTeX/pull/1381?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/macros.js](https://codecov.io/gh/Khan/KaTeX/pull/1381/diff?src=pr&el=tree#diff-c3JjL21hY3Jvcy5qcw==) | `87.9% <ø> (ø)` | :arrow_up: |
| [src/MacroExpander.js](https://codecov.io/gh/Khan/KaTeX/pull/1381/diff?src=pr&el=tree#diff-c3JjL01hY3JvRXhwYW5kZXIuanM=) | `85.96% <35.71%> (-7.04%)` | :arrow_down: |
| [src/environments/array.js](https://codecov.io/gh/Khan/KaTeX/pull/1381/diff?src=pr&el=tree#diff-c3JjL2Vudmlyb25tZW50cy9hcnJheS5qcw==) | `43.96% <57.14%> (+0.4%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Khan/KaTeX/pull/1381?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Khan/KaTeX/pull/1381?src=pr&el=footer). Last update [3796faa...506f88e](https://codecov.io/gh/Khan/KaTeX/pull/1381?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/MacroExpander.js b/src/MacroExpander.js
index 67a8dec8..01d703be 100644
--- a/src/MacroExpander.js
+++ b/src/MacroExpander.js
@@ -249,6 +249,40 @@ export default class MacroExpander implements MacroContextInterface {
         throw new Error(); // eslint-disable-line no-unreachable
     }
 
+    /**
+     * Fully expand the given macro name and return the resulting list of
+     * tokens, or return `undefined` if no such macro is defined.
+     */
+    expandMacro(name: string): Token[] | void {
+        if (!this.macros.get(name)) {
+            return undefined;
+        }
+        const output = [];
+        const oldStackLength = this.stack.length;
+        this.pushToken(new Token(name));
+        while (this.stack.length > oldStackLength) {
+            const expanded = this.expandOnce();
+            // expandOnce returns Token if and only if it's fully expanded.
+            if (expanded instanceof Token) {
+                output.push(this.stack.pop());
+            }
+        }
+        return output;
+    }
+
+    /**
+     * Fully expand the given macro name and return the result as a string,
+     * or return `undefined` if no such macro is defined.
+     */
+    expandMacroAsText(name: string): string | void {
+        const tokens = this.expandMacro(name);
+        if (tokens) {
+            return tokens.map((token) => token.text).join("");
+        } else {
+            return tokens;
+        }
+    }
+
     /**
      * Returns the expanded macro as a reversed array of tokens and a macro
      * argument count.  Or returns `null` if no such macro.
diff --git a/src/environments/array.js b/src/environments/array.js
index 8cdf3a4c..13bedce7 100644
--- a/src/environments/array.js
+++ b/src/environments/array.js
@@ -25,7 +25,7 @@ type AlignSpec = { type: "separator", separator: string } | {
 export type ArrayEnvNodeData = {|
     type: "array",
     hskipBeforeAndAfter?: boolean,
-    arraystretch?: number,
+    arraystretch: number,
     addJot?: boolean,
     cols?: AlignSpec[],
     body: ParseNode<*>[][], // List of rows in the (2D) array.
@@ -71,6 +71,20 @@ function parseArray(
     parser.gullet.beginGroup();
     parser.gullet.macros.set("\\\\", "\\cr");
 
+    // Get current arraystretch if it's not set by the environment
+    if (!result.arraystretch) {
+        const arraystretch = parser.gullet.expandMacroAsText("\\arraystretch");
+        if (arraystretch == null) {
+            // Default \arraystretch from lttab.dtx
+            result.arraystretch = 1;
+        } else {
+            result.arraystretch = parseFloat(arraystretch);
+            if (!result.arraystretch || result.arraystretch < 0) {
+                throw new ParseError(`Invalid \\arraystretch: ${arraystretch}`);
+            }
+        }
+    }
+
     let row = [];
     const body = [row];
     const rowGaps = [];
@@ -166,10 +180,7 @@ const htmlBuilder = function(group, options) {
     // Default \jot from ltmath.dtx
     // TODO(edemaine): allow overriding \jot via \setlength (#687)
     const jot = 3 * pt;
-    // Default \arraystretch from lttab.dtx
-    // TODO(gagern): may get redefined once we have user-defined macros
-    const arraystretch = utils.deflt(groupValue.arraystretch, 1);
-    const arrayskip = arraystretch * baselineskip;
+    const arrayskip = groupValue.arraystretch * baselineskip;
     const arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and
     const arstrutDepth = 0.3 * arrayskip;  // \@arstrutbox in lttab.dtx
 
@@ -358,7 +369,7 @@ const mathmlBuilder = function(group, options) {
         }));
 };
 
-// Convinient function for aligned and alignedat environments.
+// Convenience function for aligned and alignedat environments.
 const alignedHandler = function(context, args) {
     const cols = [];
     let res = {
diff --git a/src/macros.js b/src/macros.js
index c331daad..38d506fb 100644
--- a/src/macros.js
+++ b/src/macros.js
@@ -38,6 +38,18 @@ export interface MacroContextInterface {
      */
     expandAfterFuture(): Token;
 
+    /**
+     * Fully expand the given macro name and return the resulting list of
+     * tokens, or return `undefined` if no such macro is defined.
+     */
+    expandMacro(name: string): Token[] | void;
+
+    /**
+     * Fully expand the given macro name and return the result as a string,
+     * or return `undefined` if no such macro is defined.
+     */
+    expandMacroAsText(name: string): string | void;
+
     /**
      * Consume the specified number of arguments from the token stream,
      * and return the resulting array of arguments.


Test patch:
diff --git a/test/__snapshots__/katex-spec.js.snap b/test/__snapshots__/katex-spec.js.snap
index 930b7b17..627988d5 100644
--- a/test/__snapshots__/katex-spec.js.snap
+++ b/test/__snapshots__/katex-spec.js.snap
@@ -1,5 +1,148 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`A begin/end parser should grab \\arraystretch 1`] = `
+[
+  {
+    "type": "array",
+    "mode": "math",
+    "value": {
+      "type": "array",
+      "arraystretch": 1.5,
+      "body": [
+        [
+          {
+            "type": "styling",
+            "mode": "math",
+            "value": {
+              "type": "styling",
+              "style": "text",
+              "value": [
+                {
+                  "type": "ordgroup",
+                  "mode": "math",
+                  "value": [
+                    {
+                      "type": "mathord",
+                      "loc": {
+                        "end": 37,
+                        "lexer": {
+                          "input": "\\\\def\\\\arraystretch{1.5}\\\\begin{matrix}a&b\\\\\\\\c&d\\\\end{matrix}",
+                          "pos": 56
+                        },
+                        "start": 36
+                      },
+                      "mode": "math",
+                      "value": "a"
+                    }
+                  ]
+                }
+              ]
+            }
+          },
+          {
+            "type": "styling",
+            "mode": "math",
+            "value": {
+              "type": "styling",
+              "style": "text",
+              "value": [
+                {
+                  "type": "ordgroup",
+                  "mode": "math",
+                  "value": [
+                    {
+                      "type": "mathord",
+                      "loc": {
+                        "end": 39,
+                        "lexer": {
+                          "input": "\\\\def\\\\arraystretch{1.5}\\\\begin{matrix}a&b\\\\\\\\c&d\\\\end{matrix}",
+                          "pos": 56
+                        },
+                        "start": 38
+                      },
+                      "mode": "math",
+                      "value": "b"
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        ],
+        [
+          {
+            "type": "styling",
+            "mode": "math",
+            "value": {
+              "type": "styling",
+              "style": "text",
+              "value": [
+                {
+                  "type": "ordgroup",
+                  "mode": "math",
+                  "value": [
+                    {
+                      "type": "mathord",
+                      "loc": {
+                        "end": 42,
+                        "lexer": {
+                          "input": "\\\\def\\\\arraystretch{1.5}\\\\begin{matrix}a&b\\\\\\\\c&d\\\\end{matrix}",
+                          "pos": 56
+                        },
+                        "start": 41
+                      },
+                      "mode": "math",
+                      "value": "c"
+                    }
+                  ]
+                }
+              ]
+            }
+          },
+          {
+            "type": "styling",
+            "mode": "math",
+            "value": {
+              "type": "styling",
+              "style": "text",
+              "value": [
+                {
+                  "type": "ordgroup",
+                  "mode": "math",
+                  "value": [
+                    {
+                      "type": "mathord",
+                      "loc": {
+                        "end": 44,
+                        "lexer": {
+                          "input": "\\\\def\\\\arraystretch{1.5}\\\\begin{matrix}a&b\\\\\\\\c&d\\\\end{matrix}",
+                          "pos": 56
+                        },
+                        "start": 43
+                      },
+                      "mode": "math",
+                      "value": "d"
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        ]
+      ],
+      "hskipBeforeAndAfter": false,
+      "numHLinesBeforeRow": [
+        0,
+        0
+      ],
+      "rowGaps": [
+        null
+      ]
+    }
+  }
+]
+`;
+
 exports[`A parser that does not throw on unsupported commands should build katex-error span for other type of KaTeX error 1`] = `
 {
   "attributes": {
diff --git a/test/katex-spec.js b/test/katex-spec.js
index a7136609..cfb92eb9 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -1173,6 +1173,12 @@ describe("A begin/end parser", function() {
         const m3 = getParsed("\\begin{matrix}a&b\\\\ c&d \\\\ \\end{matrix}")[0];
         expect(m3.value.body.length).toBe(2);
     });
+
+    it("should grab \\arraystretch", function() {
+        const parse = getParsed("\\def\\arraystretch{1.5}" +
+            "\\begin{matrix}a&b\\\\c&d\\end{matrix}");
+        expect(parse).toMatchSnapshot();
+    });
 });
 
 describe("A sqrt parser", function() {