Repository: KaTeX/KaTeX (js)

Issue:
Empty top-level environment crashes
**Describe the bug:**
Empty `align` environments (and `equation` and probably other top-level environments) cause the following JavaScript error:
```
Cannot read property 'shift' of undefined
```

**To Reproduce:**
Steps to reproduce the behavior:
1. Go to https://katex.org
2. Enter
   ```latex
   \begin{align}
   \end{align}
   ```

[Direct link](https://katex.org/?data=%7B%22displayMode%22%3Atrue%2C%22leqno%22%3Afalse%2C%22fleqn%22%3Afalse%2C%22throwOnError%22%3Atrue%2C%22errorColor%22%3A%22%23cc0000%22%2C%22strict%22%3A%22warn%22%2C%22output%22%3A%22htmlAndMathml%22%2C%22trust%22%3Afalse%2C%22macros%22%3A%7B%7D%2C%22code%22%3A%22%5C%5Cbegin%7Balign%7D%5Cn%5C%5Cend%7Balign%7D%22%7D)

**Expected behavior:**
I expect an empty math expression.

Curiously, `\begin{aligned} \end{aligned}` works fine.

**Screenshots:**
![image](https://user-images.githubusercontent.com/2218736/115302167-210e0080-a130-11eb-9e34-33153b138dbd.png)



Pull request description:
fix(array): Keep single empty row in AMS environments
I experimented in LaTeX, and it seems that `{align}`, `{align*}`, and even `{aligned}` environments (basically, all AMS environments) keep a single empty row if there is one, but remove a trailing empty row otherwise.  This PR implements this behavior.

This in particular prevents empty equation-numbered environment (e.g. `{align}`) from crashing (fix #2944).


Patch:
diff --git a/src/environments/array.js b/src/environments/array.js
index bb44fd06..6474b0b4 100644
--- a/src/environments/array.js
+++ b/src/environments/array.js
@@ -70,6 +70,7 @@ function parseArray(
         colSeparationType,
         addEqnNum,
         singleRow,
+        emptySingleRow,
         maxNumCols,
         leqno,
     }: {|
@@ -80,6 +81,7 @@ function parseArray(
         colSeparationType?: ColSeparationType,
         addEqnNum?: boolean,
         singleRow?: boolean,
+        emptySingleRow?: boolean,
         maxNumCols?: number,
         leqno?: boolean,
     |},
@@ -153,10 +155,12 @@ function parseArray(
             parser.consume();
         } else if (next === "\\end") {
             // Arrays terminate newlines with `\crcr` which consumes a `\cr` if
-            // the last line is empty.
+            // the last line is empty.  However, AMS environments keep the
+            // empty row if it's the only one.
             // NOTE: Currently, `cell` is the last item added into `row`.
             if (row.length === 1 && cell.type === "styling" &&
-                cell.body[0].body.length === 0) {
+                cell.body[0].body.length === 0 &&
+                (body.length > 1 || !emptySingleRow)) {
                 body.pop();
             }
             if (hLinesBeforeRow.length < body.length + 1) {
@@ -630,6 +634,7 @@ const alignedHandler = function(context, args) {
             cols,
             addJot: true,
             addEqnNum: context.envName === "align" || context.envName === "alignat",
+            emptySingleRow: true,
             colSeparationType: separationType,
             maxNumCols: context.envName === "split" ? 2 : undefined,
             leqno: context.parser.settings.leqno,
@@ -977,6 +982,7 @@ defineEnvironment({
             addJot: true,
             colSeparationType: "gather",
             addEqnNum: context.envName === "gather",
+            emptySingleRow: true,
             leqno: context.parser.settings.leqno,
         };
         return parseArray(context.parser, res, "display");
@@ -1009,6 +1015,7 @@ defineEnvironment({
         validateAmsEnvironmentContext(context);
         const res = {
             addEqnNum: context.envName === "equation",
+            emptySingleRow: true,
             singleRow: true,
             maxNumCols: 1,
             leqno: context.parser.settings.leqno,


Test patch:
diff --git a/test/katex-spec.js b/test/katex-spec.js
index 1c337f56..d3df44f9 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -2797,6 +2797,18 @@ describe("AMS environments", function() {
         expect`\begin{CD}A @<a<< B @>>b> C @>>> D\\@. @| @AcAA @VVdV \\@. E @= F @>>> G\end{CD}`.toBuild(displayMode);
     });
 
+    it("should build an empty environment", () => {
+        expect`\begin{gather}\end{gather}`.toBuild(displayMode);
+        expect`\begin{gather*}\end{gather*}`.toBuild(displayMode);
+        expect`\begin{align}\end{align}`.toBuild(displayMode);
+        expect`\begin{align*}\end{align*}`.toBuild(displayMode);
+        expect`\begin{alignat}{2}\end{alignat}`.toBuild(displayMode);
+        expect`\begin{alignat*}{2}\end{alignat*}`.toBuild(displayMode);
+        expect`\begin{equation}\end{equation}`.toBuild(displayMode);
+        expect`\begin{split}\end{split}`.toBuild(displayMode);
+        expect`\begin{CD}\end{CD}`.toBuild(displayMode);
+    });
+
     it("{equation} should fail if argument contains two rows.", () => {
         expect`\begin{equation}a=\cr b+c\end{equation}`.not.toParse(displayMode);
     });