From 7f778d1543fc1a9021609fa29067073658fb0d4b Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Tue, 29 Jan 2019 04:40:17 -0500 Subject: [PATCH] leqno and fleqn support (#1814) * leqno support * Add fleqn support * Add tests * Lint fix * Add leqno and fleqn to website demo --- cli.js | 4 ++++ docs/options.md | 2 ++ src/Settings.js | 6 ++++++ src/buildTree.js | 26 ++++++++++++++++---------- src/katex.less | 11 +++++++++++ static/main.js | 10 ++++++++++ test/katex-spec.js | 23 +++++++++++++++++++++++ website/pages/index.html | 8 ++++++++ website/static/js/index.js | 4 ++-- 9 files changed, 82 insertions(+), 12 deletions(-) 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/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"); 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 @@ + + + + + + + + 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") {