Repository: KaTeX/KaTeX (js)

Issue:
Support for leqno and fleqn rendering options
In katex 0.10+ is it possible to render a left-justified equation on its own line and include a \tag{} or \tag*{}?



Pull request description:
leqno and fleqn support
Fix #1776 by adding two boolean display-math rendering options to the Settings object:
* `leqno`: Put tags on the left instead of right
* `fleqn`: Flush equations left instead of center

Both are implemented by a CSS class on the `.katex-display` element. Settings seemed to be the right place for these, but feel free to suggest alternatives (e.g. a list of classes to apply to display elements? that seemed a bit weird...).

See [amsmath.pdf](https://mirror.hmc.edu/ctan/macros/latex/required/amsmath/amsmath.pdf) for some documentation of these features.

Patch:
diff --git a/cli.js b/cli.js
index d885abcf..1fe0de48 100755
--- a/cli.js
+++ b/cli.js
@@ -26,6 +26,10 @@ const program = require("commander")
         "Render math in display mode, which puts the math in display style " +
         "(so \\int and \\sum are large, for example), and centers the math " +
         "on the page on its own line.")
+    .option("--leqno",
+        "Render display math in leqno style (left-justified tags).")
+    .option("--fleqn",
+        "Render display math flush left.")
     .option("-t, --no-throw-on-error",
         "Render errors (in the color given by --error-color) instead of " +
         "throwing a ParseError exception when encountering an error.")
diff --git a/docs/options.md b/docs/options.md
index a740d940..86e87915 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -5,6 +5,8 @@ title: Options
 You can provide an object of options as the last argument to [`katex.render` and `katex.renderToString`](api.md). Available options are:
 
 - `displayMode`: `boolean`. If `true` the math will be rendered in display mode, which will put the math in display style (so `\int` and `\sum` are large, for example), and will center the math on the page on its own line. If `false` the math will be rendered in inline mode. (default: `false`)
+- `leqno`: `boolean`. If `true`, display math has `\tag`s rendered on the left instead of the right, like `\usepackage[leqno]{amsmath}` in LaTeX.
+- `fleqn`: `boolean`. If `true`, display math renders flush left, like `\documentclass[fleqn]` in LaTeX.
 - `throwOnError`: `boolean`. If `true` (the default), KaTeX will throw a `ParseError` when it encounters an unsupported command or invalid LaTeX. If `false`, KaTeX will render unsupported commands as text, and render invalid LaTeX as its source code with hover text giving the error, in the color given by `errorColor`.
 - `errorColor`: `string`. A color string given in the format `"#XXX"` or `"#XXXXXX"`. This option determines the color that unsupported commands and invalid LaTeX are rendered in when `throwOnError` is set to `false`. (default: `#cc0000`)
 - `macros`: `object`. A collection of custom macros. Each macro is a property with a name like `\name` (written `"\\name"` in JavaScript) which maps to a string that describes the expansion of the macro, or a function that accepts an instance of `MacroExpander` as first argument and returns the expansion as a string. `MacroExpander` is an internal API and subject to non-backwards compatible changes. See [`src/macros.js`](https://github.com/KaTeX/KaTeX/blob/master/src/macros.js) for its usage. Single-character keys can also be included in which case the character will be redefined as the given macro (similar to TeX active characters). *This object will be modified* if the LaTeX code defines its own macros via `\gdef`, which enables consecutive calls to KaTeX to share state.
diff --git a/src/Settings.js b/src/Settings.js
index a0c69b2e..5ecc1d8b 100644
--- a/src/Settings.js
+++ b/src/Settings.js
@@ -18,6 +18,8 @@ export type StrictFunction =
 
 export type SettingsOptions = {
     displayMode?: boolean;
+    leqno?: boolean;
+    fleqn?: boolean;
     throwOnError?: boolean;
     errorColor?: string;
     macros?: MacroMap;
@@ -40,6 +42,8 @@ export type SettingsOptions = {
  */
 class Settings {
     displayMode: boolean;
+    leqno: boolean;
+    fleqn: boolean;
     throwOnError: boolean;
     errorColor: string;
     macros: MacroMap;
@@ -53,6 +57,8 @@ class Settings {
         // allow null options
         options = options || {};
         this.displayMode = utils.deflt(options.displayMode, false);
+        this.leqno = utils.deflt(options.leqno, false);
+        this.fleqn = utils.deflt(options.fleqn, false);
         this.throwOnError = utils.deflt(options.throwOnError, true);
         this.errorColor = utils.deflt(options.errorColor, "#cc0000");
         this.macros = options.macros || {};
diff --git a/src/buildTree.js b/src/buildTree.js
index 7bd6ba8c..d012513e 100644
--- a/src/buildTree.js
+++ b/src/buildTree.js
@@ -16,6 +16,20 @@ const optionsFromSettings = function(settings: Settings) {
     });
 };
 
+const displayWrap = function(node: DomSpan, settings: Settings): DomSpan {
+    if (settings.displayMode) {
+        const classes = ["katex-display"];
+        if (settings.leqno) {
+            classes.push("leqno");
+        }
+        if (settings.fleqn) {
+            classes.push("fleqn");
+        }
+        node = buildCommon.makeSpan(classes, [node]);
+    }
+    return node;
+};
+
 export const buildTree = function(
     tree: AnyParseNode[],
     expression: string,
@@ -29,11 +43,7 @@ export const buildTree = function(
         mathMLNode, htmlNode,
     ]);
 
-    if (settings.displayMode) {
-        return buildCommon.makeSpan(["katex-display"], [katexNode]);
-    } else {
-        return katexNode;
-    }
+    return displayWrap(katexNode, settings);
 };
 
 export const buildHTMLTree = function(
@@ -44,11 +54,7 @@ export const buildHTMLTree = function(
     const options = optionsFromSettings(settings);
     const htmlNode = buildHTML(tree, options);
     const katexNode = buildCommon.makeSpan(["katex"], [htmlNode]);
-    if (settings.displayMode) {
-        return buildCommon.makeSpan(["katex-display"], [katexNode]);
-    } else {
-        return katexNode;
-    }
+    return displayWrap(katexNode, settings);
 };
 
 export default buildTree;
diff --git a/src/katex.less b/src/katex.less
index aaebebf9..546dc7c8 100644
--- a/src/katex.less
+++ b/src/katex.less
@@ -588,3 +588,14 @@
         }
     }
 }
+
+// Left-justified tags (default is right-justified)
+.katex-display.leqno > .katex > .katex-html > .tag {
+    left: 0;
+    right: auto;
+}
+
+// Flush-left display math
+.katex-display.fleqn > .katex {
+    text-align: left;
+}
diff --git a/static/main.js b/static/main.js
index 2c311350..262412e7 100644
--- a/static/main.js
+++ b/static/main.js
@@ -29,6 +29,16 @@ function init() {
         options.displayMode = false;
     }
 
+    // Use `leqno=1` (or `=t`/`=true`/`=y`/`=yes`) to put tags on left side.
+    if (query.leqno && query.leqno.match(/^(1|t|y)/)) {
+        options.leqno = true;
+    }
+
+    // Use `fleqn=1` (or `=t`/`=true`/`=y`/`=yes`) to put tags on left side.
+    if (query.fleqn && query.fleqn.match(/^(1|t|y)/)) {
+        options.fleqn = true;
+    }
+
     // Use `strict=warn` for warning strict mode or `strict=error`
     // (or `=1`/`=t`/`=true`/`=y`/`=yes`)
     // to turn off displayMode (which is on by default).
diff --git a/website/pages/index.html b/website/pages/index.html
index 9ff1f57f..9fcc4e3f 100644
--- a/website/pages/index.html
+++ b/website/pages/index.html
@@ -131,6 +131,14 @@
           <td><label for="displayMode">displayMode</label></td>
           <td><input type="checkbox" id="displayMode" checked/></td>
         </tr>
+        <tr>
+          <td><label for="leqno">leqno</label></td>
+          <td><input type="checkbox" id="leqno"/></td>
+        </tr>
+        <tr>
+          <td><label for="fleqn">fleqn</label></td>
+          <td><input type="checkbox" id="fleqn"/></td>
+        </tr>
         <tr>
           <td><label for="throwOnError">throwOnError</label></td>
           <td><input type="checkbox" id="throwOnError" checked/></td>
diff --git a/website/static/js/index.js b/website/static/js/index.js
index ba7392e2..d4a4498f 100644
--- a/website/static/js/index.js
+++ b/website/static/js/index.js
@@ -36,8 +36,8 @@
         demoInput.value = data.code;
     }
 
-    var katexOptions = ["displayMode", "throwOnError", "errorColor", "strict",
-        "macros"].map(function(id) {
+    var katexOptions = ["displayMode", "leqno", "fleqn", "throwOnError",
+        "errorColor", "strict", "macros"].map(function(id) {
             var el = document.getElementById(id);
             if (el.type === "checkbox") {
                 if (typeof data[id] === "boolean") {


Test patch:
diff --git a/test/katex-spec.js b/test/katex-spec.js
index 27134de6..b254fcc3 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -3174,6 +3174,29 @@ describe("\\tag support", function() {
     });
 });
 
+describe("leqno and fleqn rendering options", () => {
+    const expr = r`\tag{hi}x+y`;
+    for (const opt of ["leqno", "fleqn"]) {
+        it(`should not add ${opt} class by default`, () => {
+            const settings = new Settings({displayMode: true});
+            const built = katex.__renderToDomTree(expr, settings);
+            expect(built.classes).not.toContain(opt);
+        });
+        it(`should not add ${opt} class when false`, () => {
+            const settings = new Settings({displayMode: true});
+            settings[opt] = false;
+            const built = katex.__renderToDomTree(expr, settings);
+            expect(built.classes).not.toContain(opt);
+        });
+        it(`should add ${opt} class when true`, () => {
+            const settings = new Settings({displayMode: true});
+            settings[opt] = true;
+            const built = katex.__renderToDomTree(expr, settings);
+            expect(built.classes).toContain(opt);
+        });
+    }
+});
+
 describe("\\@binrel automatic bin/rel/ord", () => {
     it("should generate proper class", () => {
         expect("L\\@binrel+xR").toParseLike("L\\mathbin xR");