Repository: KaTeX/KaTeX (js)

Issue:
\providecommand overwrites the existing macro
> Before reporting a bug
- [x] Check [common issues](https://katex.org/docs/issues.html).
- [x] Check the bug is reproducible in [the demo](https://katex.org). If not, check KaTeX is up-to-date and installed correctly.
- [x] Search for [existing issues](https://github.com/KaTeX/KaTeX/issues).

**Describe the bug:**
`\providecommand` overwrites the existing macro if already defined.

**(La)TeX code:**
```latex
\newcommand{\foo}{a}
\providecommand{\foo}{b}
\foo
```

**Expected behavior:**
`\providecommand` should do nothing if the macro already exists.

QuickLaTeX screenshot:
![ss2](https://github.com/KaTeX/KaTeX/assets/2741668/efbbb2cd-9437-4fdc-a979-18fa5f784b8b)

**Screenshots:**
![ss1](https://github.com/KaTeX/KaTeX/assets/2741668/11b27021-4cab-41ee-85ed-0985ebda5fa9)


**Environment (please complete the following information):**
 - KaTeX Version: 0.16.9
 - Device: Desktop
 - OS: macOS 14.2.1
 - Browser: Firefox
 - Version: 122.0.1


Pull request description:
fix: \providecommand does not overwrite existing macro
<!-- PR title should follow Angular Commit Message Conventions (https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) -->

**What is the previous behavior before this PR?**
`\providecommand` acted like `\newcommand` if the command was new, or `\renewcommand` if the command was old. In other words, it defined the command no matter what. But this is not how LaTeX's `\providecommand` works.

**What is the new behavior after this PR?**
`\providecommand` acts like `\newcommand` if the command was new, and a no-op if the command was old. This matches LaTeX's behavior.

Fixes #3928

Patch:
diff --git a/src/macros.js b/src/macros.js
index 3c5c9658..182efa69 100644
--- a/src/macros.js
+++ b/src/macros.js
@@ -146,7 +146,9 @@ defineMacro("\\char", function(context) {
 // \newcommand{\macro}[args]{definition}
 // \renewcommand{\macro}[args]{definition}
 // TODO: Optional arguments: \newcommand{\macro}[args][default]{definition}
-const newcommand = (context, existsOK: boolean, nonexistsOK: boolean) => {
+const newcommand = (
+    context, existsOK: boolean, nonexistsOK: boolean, skipIfExists: boolean
+) => {
     let arg = context.consumeArg().tokens;
     if (arg.length !== 1) {
         throw new ParseError(
@@ -181,16 +183,21 @@ const newcommand = (context, existsOK: boolean, nonexistsOK: boolean) => {
         arg = context.consumeArg().tokens;
     }
 
-    // Final arg is the expansion of the macro
-    context.macros.set(name, {
-        tokens: arg,
-        numArgs,
-    });
+    if (!(exists && skipIfExists)) {
+        // Final arg is the expansion of the macro
+        context.macros.set(name, {
+            tokens: arg,
+            numArgs,
+        });
+    }
     return '';
 };
-defineMacro("\\newcommand", (context) => newcommand(context, false, true));
-defineMacro("\\renewcommand", (context) => newcommand(context, true, false));
-defineMacro("\\providecommand", (context) => newcommand(context, true, true));
+defineMacro("\\newcommand",
+    (context) => newcommand(context, false, true, false));
+defineMacro("\\renewcommand",
+    (context) => newcommand(context, true, false, false));
+defineMacro("\\providecommand",
+    (context) => newcommand(context, true, true, true));
 
 // terminal (console) tools
 defineMacro("\\message", (context) => {


Test patch:
diff --git a/test/katex-spec.js b/test/katex-spec.js
index ceb76dd5..70e587f9 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -3659,17 +3659,15 @@ describe("A macro expander", function() {
         expect`\newcommand{\foo}{1}\foo\renewcommand{\foo}{2}\foo`.toParseLike`12`;
     });
 
-    it("\\providecommand (re)defines macros", () => {
+    it("\\providecommand defines but does not redefine macros", () => {
         expect`\providecommand\foo{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
         expect`\providecommand{\foo}{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
-        expect`\providecommand\bar{x^2}\bar+\bar`.toParseLike`x^2+x^2`;
-        expect`\providecommand{\bar}{x^2}\bar+\bar`.toParseLike`x^2+x^2`;
         expect`\newcommand{\foo}{1}\foo\providecommand{\foo}{2}\foo`
-            .toParseLike`12`;
+            .toParseLike`11`;
         expect`\providecommand{\foo}{1}\foo\renewcommand{\foo}{2}\foo`
             .toParseLike`12`;
         expect`\providecommand{\foo}{1}\foo\providecommand{\foo}{2}\foo`
-            .toParseLike`12`;
+            .toParseLike`11`;
     });
 
     it("\\newcommand is local", () => {