Repository: KaTeX/KaTeX (js)

Issue:
\@ifnextchar should consume spaces
**Describe the bug:**
As [reported](https://github.com/KaTeX/KaTeX/issues/2114#issuecomment-540431307) by @JohnTasto, `\@ifnextchar` should ignore spaces that follow it, but our implementation doesn't.  I think this will just involve adding a `context.consumeSpaces()` line, but I haven't tested yet.

**To Reproduce:**
```latex
\def\x#1{\@ifnextchar x{yes}{no}}
\x{}x \\
\x{} x
```
![image](https://user-images.githubusercontent.com/2218736/66590245-52cb0200-eb5e-11e9-8d17-311a1e940292.png)

**Expected behavior:**
LaTeX produces:
![image](https://user-images.githubusercontent.com/2218736/66590309-7beb9280-eb5e-11e9-96fa-5b313a6bb007.png)

**Additional context:**
Here's the relevant LaTeX implementation of `\@ifnextchar`:

```latex
\long\def\@ifnextchar#1#2#3{%
  \let\reserved@d=#1%
  \def\reserved@a{#2}%
  \def\reserved@b{#3}%
  \futurelet\@let@token\@ifnch}
\let\kernel@ifnextchar\@ifnextchar
\def\@ifnch{%
  \ifx\@let@token\@sptoken
    \let\reserved@c\@xifnch
  \else
    \ifx\@let@token\reserved@d
      \let\reserved@c\reserved@a
    \else
      \let\reserved@c\reserved@b
    \fi
  \fi
  \reserved@c}
\def\:{\let\@sptoken= } \:  % this makes \@sptoken a space token
\def\:{\@xifnch} \expandafter\def\: {\futurelet\@let@token\@ifnch}
```

This is difficult code to read (because dealing with spaces in TeX is hard, and `\futurelet` is tricky), but here's a translation:

`\@ifnextchar{CHAR}{THEN}{ELSE}`:
* Stores its arguments
* Get next character
* Call `\@ifnch`

`\@ifnch`:
* If next character is a space, call `\@xifnch` which does the following:
  * Consumes a space
  * Get next character
  * Call `\@ifnch` (i.e. check for space again, etc.)
* Otherwise, check the character against CHAR and expand to THEN or ELSE accordingly.

In other words, **consume** spaces and check the next character without consuming it.  Our current implementation does the latter but not the former.

Pull request:
\@ifnextchar consumes spaces
Fix #2117 by fixing our implementation of \@ifnextchar to match LaTeX's: consuming spaces before checking the next character.  See #2117 for details, including confirmation that LaTeX behaves this way (consuming spaces, not just skipping over them).

Patch:
diff --git a/src/macros.js b/src/macros.js
index 4554a42f..c4b7f7a0 100644
--- a/src/macros.js
+++ b/src/macros.js
@@ -36,6 +36,11 @@ export interface MacroContextInterface {
      */
     popToken(): Token;
 
+    /**
+     * Consume all following space tokens, without expansion.
+     */
+    consumeSpaces(): void;
+
     /**
      * Expand the next token only once (if possible), and return the resulting
      * top token on the stack (without removing anything from the stack).
@@ -108,10 +113,12 @@ defineMacro("\\@secondoftwo", function(context) {
 });
 
 // LaTeX's \@ifnextchar{#1}{#2}{#3} looks ahead to the next (unexpanded)
-// symbol.  If it matches #1, then the macro expands to #2; otherwise, #3.
-// Note, however, that it does not consume the next symbol in either case.
+// symbol that isn't a space, consuming any spaces but not consuming the
+// first nonspace character.  If that nonspace character matches #1, then
+// the macro expands to #2; otherwise, it expands to #3.
 defineMacro("\\@ifnextchar", function(context) {
     const args = context.consumeArgs(3);  // symbol, if, else
+    context.consumeSpaces();
     const nextToken = context.future();
     if (args[0].length === 1 && args[0][0].text === nextToken.text) {
         return {tokens: args[1], numArgs: 0};


Test patch:
diff --git a/test/katex-spec.js b/test/katex-spec.js
index a418a133..f4637f10 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -3008,11 +3008,16 @@ describe("A macro expander", function() {
         expect`\@ifstar{yes}{no}?!`.toParseLike`no?!`;
     });
 
-    it("\\@ifnextchar should not consume anything", function() {
+    it("\\@ifnextchar should not consume nonspaces", function() {
         expect`\@ifnextchar!{yes}{no}!!`.toParseLike`yes!!`;
         expect`\@ifnextchar!{yes}{no}?!`.toParseLike`no?!`;
     });
 
+    it("\\@ifnextchar should consume spaces", function() {
+        expect`\def\x#1{\@ifnextchar x{yes}{no}}\x{}x\x{} x`
+            .toParseLike`yesxyesx`;
+    });
+
     it("\\@ifstar should consume star but nothing else", function() {
         expect`\@ifstar{yes}{no}*!`.toParseLike`yes!`;
         expect`\@ifstar{yes}{no}?!`.toParseLike`no?!`;