diff --git a/contrib/render-a11y-string/render-a11y-string.js b/contrib/render-a11y-string/render-a11y-string.js index 3cd7092e..fbc020ce 100644 --- a/contrib/render-a11y-string/render-a11y-string.js +++ b/contrib/render-a11y-string/render-a11y-string.js @@ -655,6 +655,11 @@ const handleObject = ( break; } + case "html": { + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + default: (tree.type: empty); throw new Error("KaTeX a11y un-recognized type: " + tree.type); diff --git a/docs/issues.md b/docs/issues.md index 2bc23731..b3ca0ee0 100644 --- a/docs/issues.md +++ b/docs/issues.md @@ -22,6 +22,8 @@ title: Common Issues - MathJax defines `\color` to be like `\textcolor` by default; set KaTeX's `colorIsTextColor` option to `true` for this behavior. KaTeX's default behavior matches MathJax with its `color.js` extension enabled. +- Equivalents of MathJax `\class`, `\cssId`, and `\style` are `\htmlClass`, + `\htmlId`, and `\htmlStyle`, respectively, to avoid ambiguity. ## Troubleshooting diff --git a/docs/options.md b/docs/options.md index 4abe4b8d..3677e12a 100644 --- a/docs/options.md +++ b/docs/options.md @@ -28,6 +28,8 @@ You can provide an object of options as the last argument to [`katex.render` and - `"commentAtEnd"`: Use of `%` comment without a terminating newline. LaTeX would thereby comment out the end of math mode (e.g. `$`), causing an error. + - `"htmlExtension"`: Use of HTML extension (`\html`-prefixed) commands, + which are provieded for HTML manipulation. A second category of `errorCode`s never throw errors, but their strictness affects the behavior of KaTeX: @@ -41,6 +43,10 @@ You can provide an object of options as the last argument to [`katex.render` and - `{command: "\\url", url, protocol}` - `{command: "\\href", url, protocol}` - `{command: "\\includegraphics", url, protocol}` + - `{command: "\\htmlClass", class}` + - `{command: "\\htmlId", id}` + - `{command: "\\htmlStyle", style}` + - `{command: "\\htmlData", attributes}` Here are some sample trust settings: diff --git a/docs/support_table.md b/docs/support_table.md index 8db08e45..fabe3835 100644 --- a/docs/support_table.md +++ b/docs/support_table.md @@ -458,6 +458,10 @@ use `\ce` instead| |\hskip|$w\hskip1em i\hskip2em d$|`w\hskip1em i\hskip2em d`| |\hslash|$\hslash$|| |\hspace|$s\hspace7ex k$|`s\hspace7ex k`| +|\htmlClass|$\htmlClass{foo}{x}$|`\htmlClass{foo}{x}` Must enable `trust` and disable `strict` [option](options.md)| +|\htmlData|$\htmlData{foo=a, bar=b}{x}$|`\htmlData{foo=a, bar=b}{x}` Must enable `trust` and disable `strict` [option](options.md)| +|\htmlId|$\htmlId{bar}{x}$|`\htmlId{bar}{x}` Must enable `trust` and disable `strict` [option](options.md)| +|\htmlStyle|$\htmlStyle{color: red;}{x}$|`\htmlStyle{color: red;}{x}` Must enable `trust` and disable `strict` [option](options.md)| |\huge|$\huge huge$|`\huge huge`| |\Huge|$\Huge Huge$|`\Huge Huge`| diff --git a/docs/supported.md b/docs/supported.md index 10870350..b18686f7 100644 --- a/docs/supported.md +++ b/docs/supported.md @@ -113,9 +113,15 @@ or for just some URLs via the `trust` [option](options.md). | $\href{https://katex.org/}{\KaTeX}$ | `\href{https://katex.org/}{\KaTeX}` | | $\url{https://katex.org/}$ | `\url{https://katex.org/}` | | $\includegraphics[height=0.8em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://katex.org/img/khan-academy.png}$ | `\includegraphics[height=0.8em, totalheight=0.9em, width=0.9em, alt=KA logo]{https://katex.org/img/khan-academy.png}` | +| $\htmlId{bar}{x}$ | `\htmlId{bar}{x}` | +| $\htmlClass{foo}{x}$ | `\htmlClass{foo}{x}` | +| $\htmlStyle{color: red;}{x}$ | `\htmlStyle{color: red;}{x}` | +| $\htmlData{foo=a, bar=b}{x}$ | `\htmlData{foo=a, bar=b}{x}` | `\includegraphics` supports `height`, `width`, `totalheight`, and `alt` in its first argument. `height` is required. +HTML extension (`\html`-prefixed) commands are non-standard, so loosening `strict` option for `htmlExtension` is required. + ## Letters and Unicode diff --git a/src/Settings.js b/src/Settings.js index e1707d71..4e0a2ec6 100644 --- a/src/Settings.js +++ b/src/Settings.js @@ -32,6 +32,22 @@ export type TrustContextTypes = { url: string, protocol?: string, |}, + "\\htmlClass": {| + command: "\\htmlClass", + class: string, + |}, + "\\htmlId": {| + command: "\\htmlId", + id: string, + |}, + "\\htmlStyle": {| + command: "\\htmlStyle", + style: string, + |}, + "\\htmlData": {| + command: "\\htmlData", + attributes: {[string]: string}, + |}, }; export type AnyTrustContext = $Values; export type TrustFunction = (context: AnyTrustContext) => ?boolean; diff --git a/src/buildHTML.js b/src/buildHTML.js index 541b46f7..56186e19 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -9,7 +9,7 @@ import ParseError from "./ParseError"; import Style from "./Style"; import buildCommon from "./buildCommon"; -import {Anchor} from "./domTree"; +import {Span, Anchor} from "./domTree"; import utils from "./utils"; import {spacings, tightSpacings} from "./spacingData"; import {_htmlGroupBuilders as groupBuilders} from "./defineFunction"; @@ -185,8 +185,9 @@ const traverseNonSpaceNodes = function( // Check if given node is a partial group, i.e., does not affect spacing around. const checkPartialGroup = function( node: HtmlDomNode, -): ?(DocumentFragment | Anchor) { - if (node instanceof DocumentFragment || node instanceof Anchor) { +): ?(DocumentFragment | Anchor | DomSpan) { + if (node instanceof DocumentFragment || node instanceof Anchor + || (node instanceof Span && node.hasClass("enclosing"))) { return node; } return null; diff --git a/src/functions.js b/src/functions.js index 5bfae0eb..acb273e9 100644 --- a/src/functions.js +++ b/src/functions.js @@ -20,6 +20,7 @@ import "./functions/font"; import "./functions/genfrac"; import "./functions/horizBrace"; import "./functions/href"; +import "./functions/html"; import "./functions/htmlmathml"; import "./functions/includegraphics"; import "./functions/kern"; diff --git a/src/functions/html.js b/src/functions/html.js new file mode 100644 index 00000000..c8d316aa --- /dev/null +++ b/src/functions/html.js @@ -0,0 +1,102 @@ +// @flow +import defineFunction, {ordargument} from "../defineFunction"; +import buildCommon from "../buildCommon"; +import {assertNodeType} from "../parseNode"; +import ParseError from "../ParseError"; + +import * as html from "../buildHTML"; +import * as mml from "../buildMathML"; + +defineFunction({ + type: "html", + names: ["\\htmlClass", "\\htmlId", "\\htmlStyle", "\\htmlData"], + props: { + numArgs: 2, + argTypes: ["raw", "original"], + allowedInText: true, + }, + handler: ({parser, funcName, token}, args) => { + const value = assertNodeType(args[0], "raw").string; + const body = args[1]; + + if (parser.settings.strict) { + parser.settings.reportNonstrict("htmlExtension", + "HTML extension is disabled on strict mode"); + } + + let trustContext; + const attributes = {}; + + switch (funcName) { + case "\\htmlClass": + attributes.class = value; + trustContext = { + command: "\\htmlClass", + class: value, + }; + break; + case "\\htmlId": + attributes.id = value; + trustContext = { + command: "\\htmlId", + id: value, + }; + break; + case "\\htmlStyle": + attributes.style = value; + trustContext = { + command: "\\htmlStyle", + style: value, + }; + break; + case "\\htmlData": { + const data = value.split(","); + for (let i = 0; i < data.length; i++) { + const keyVal = data[i].split("="); + if (keyVal.length !== 2) { + throw new ParseError( + "Error parsing key-value for \\htmlData"); + } + attributes["data-" + keyVal[0].trim()] = keyVal[1].trim(); + } + + trustContext = { + command: "\\htmlData", + attributes, + }; + break; + } + default: + throw new Error("Unrecognized html command"); + } + + if (!parser.settings.isTrusted(trustContext)) { + return parser.formatUnsupportedCmd(funcName); + } + return { + type: "html", + mode: parser.mode, + attributes, + body: ordargument(body), + }; + }, + htmlBuilder: (group, options) => { + const elements = html.buildExpression(group.body, options, false); + + const classes = ["enclosing"]; + if (group.attributes.class) { + classes.push(...group.attributes.class.trim().split(/\s+/)); + } + + const span = buildCommon.makeSpan(classes, elements, options); + for (const attr in group.attributes) { + if (attr !== "class" && group.attributes.hasOwnProperty(attr)) { + span.setAttribute(attr, group.attributes[attr]); + } + } + return span; + }, + mathmlBuilder: (group, options) => { + return mml.buildExpressionRow(group.body, options); + }, +}); diff --git a/src/parseNode.js b/src/parseNode.js index b83d3dde..74852dd1 100644 --- a/src/parseNode.js +++ b/src/parseNode.js @@ -270,6 +270,13 @@ type ParseNodeTypes = { href: string, body: AnyParseNode[], |}, + "html": {| + type: "html", + mode: Mode, + loc?: ?SourceLocation, + attributes: {[string]: string}, + body: AnyParseNode[], + |}, "htmlmathml": {| type: "htmlmathml", mode: Mode, diff --git a/test/__snapshots__/katex-spec.js.snap b/test/__snapshots__/katex-spec.js.snap index e3f30ccf..d8b2cd71 100755 --- a/test/__snapshots__/katex-spec.js.snap +++ b/test/__snapshots__/katex-spec.js.snap @@ -624,6 +624,226 @@ exports[`A parser that does not throw on unsupported commands should properly es `; +exports[`An HTML extension builder should not affect spacing 1`] = ` +[ + { + "attributes": { + "id": "a" + }, + "children": [ + { + "classes": [ + "mord", + "mathdefault" + ], + "depth": 0, + "height": 0.43056, + "italic": 0, + "maxFontSize": 1, + "skew": 0.02778, + "style": { + }, + "text": "x", + "width": 0.57153 + }, + { + "attributes": { + }, + "children": [ + ], + "classes": [ + "mspace" + ], + "depth": 0, + "height": 0, + "maxFontSize": 0, + "style": { + "marginRight": "0.2222222222222222em" + } + }, + { + "classes": [ + "mbin" + ], + "depth": 0.08333, + "height": 0.58333, + "italic": 0, + "maxFontSize": 1, + "skew": 0, + "style": { + }, + "text": "+", + "width": 0.77778 + }, + { + "attributes": { + }, + "children": [ + ], + "classes": [ + "mspace" + ], + "depth": 0, + "height": 0, + "maxFontSize": 0, + "style": { + "marginRight": "0.2222222222222222em" + } + } + ], + "classes": [ + "enclosing" + ], + "depth": 0.08333, + "height": 0.58333, + "maxFontSize": 1, + "style": { + } + }, + { + "classes": [ + "mord", + "mathdefault" + ], + "depth": 0.19444, + "height": 0.43056, + "italic": 0.03588, + "maxFontSize": 1, + "skew": 0.05556, + "style": { + }, + "text": "y", + "width": 0.49028 + } +] +`; + +exports[`An HTML extension builder should render with trust and strict setting 1`] = ` +[ + { + "attributes": { + "id": "bar" + }, + "children": [ + { + "classes": [ + "mord", + "mathdefault" + ], + "depth": 0, + "height": 0.43056, + "italic": 0, + "maxFontSize": 1, + "skew": 0.02778, + "style": { + }, + "text": "x", + "width": 0.57153 + } + ], + "classes": [ + "enclosing" + ], + "depth": 0, + "height": 0.43056, + "maxFontSize": 1, + "style": { + } + }, + { + "attributes": { + }, + "children": [ + { + "classes": [ + "mord", + "mathdefault" + ], + "depth": 0, + "height": 0.43056, + "italic": 0, + "maxFontSize": 1, + "skew": 0.02778, + "style": { + }, + "text": "x", + "width": 0.57153 + } + ], + "classes": [ + "enclosing", + "foo" + ], + "depth": 0, + "height": 0.43056, + "maxFontSize": 1, + "style": { + } + }, + { + "attributes": { + "style": "color: red;" + }, + "children": [ + { + "classes": [ + "mord", + "mathdefault" + ], + "depth": 0, + "height": 0.43056, + "italic": 0, + "maxFontSize": 1, + "skew": 0.02778, + "style": { + }, + "text": "x", + "width": 0.57153 + } + ], + "classes": [ + "enclosing" + ], + "depth": 0, + "height": 0.43056, + "maxFontSize": 1, + "style": { + } + }, + { + "attributes": { + "data-bar": "b", + "data-foo": "a" + }, + "children": [ + { + "classes": [ + "mord", + "mathdefault" + ], + "depth": 0, + "height": 0.43056, + "italic": 0, + "maxFontSize": 1, + "skew": 0.02778, + "style": { + }, + "text": "x", + "width": 0.57153 + } + ], + "classes": [ + "enclosing" + ], + "depth": 0, + "height": 0.43056, + "maxFontSize": 1, + "style": { + } + } +] +`; + exports[`An implicit group parser within optional groups should work style commands \\sqrt[\\textstyle 3]{x} 1`] = ` [ { diff --git a/test/katex-spec.js b/test/katex-spec.js index 7f474eb4..da1b6430 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -2011,6 +2011,36 @@ describe("An includegraphics builder", function() { }); }); +describe("An HTML extension builder", function() { + const html = + "\\htmlId{bar}{x}\\htmlClass{foo}{x}\\htmlStyle{color: red;}{x}\\htmlData{foo=a, bar=b}{x}"; + const trustNonStrictSettings = new Settings({trust: true, strict: false}); + it("should not fail", function() { + expect(html).toBuild(trustNonStrictSettings); + }); + + it("should set HTML attributes", function() { + const built = getBuilt(html, trustNonStrictSettings); + expect(built[0].attributes.id).toMatch("bar"); + expect(built[1].classes).toContain("foo"); + expect(built[2].attributes.style).toMatch("color: red"); + expect(built[3].attributes).toEqual({ + "data-bar": "b", + "data-foo": "a", + }); + }); + + it("should not affect spacing", function() { + const built = getBuilt("\\htmlId{a}{x+}y", trustNonStrictSettings); + expect(built).toMatchSnapshot(); + }); + + it("should render with trust and strict setting", function() { + const built = getBuilt(html, trustNonStrictSettings); + expect(built).toMatchSnapshot(); + }); +}); + describe("A bin builder", function() { it("should create mbins normally", function() { const built = getBuilt`x + y`; diff --git a/test/screenshotter/images/HTML-chrome.png b/test/screenshotter/images/HTML-chrome.png new file mode 100644 index 00000000..52afcd6e Binary files /dev/null and b/test/screenshotter/images/HTML-chrome.png differ diff --git a/test/screenshotter/images/HTML-firefox.png b/test/screenshotter/images/HTML-firefox.png new file mode 100644 index 00000000..e1646ea3 Binary files /dev/null and b/test/screenshotter/images/HTML-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 8144c36d..e33c3bc7 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -149,6 +149,9 @@ GroupMacros: \endExp: \egroup tex: \startExp a+b\endExp HorizontalBraces: \overbrace{\displaystyle{\oint_S{\vec E\cdot\hat n\,\mathrm d a}}}^\text{emf} = \underbrace{\frac{q_{\text{enc}}}{\varepsilon_0}}_{\text{charge}} +HTML: + tex: \htmlId{a}{a+}b\htmlStyle{color:red;}{+c} + nolatex: HTML extension not supported by LaTeX Includegraphics: | \def\logo{\includegraphics[height=0.8em, totalheight=0.9em, width=0.9em, alt=KA logo]{../../website/static/img/khan-academy.png}} \def\logoB{\includegraphics[height=0.4em, totalheight=0.9em, width=0.9em, alt=KA logo]{../../website/static/img/khan-academy.png}} diff --git a/test/screenshotter/test.html b/test/screenshotter/test.html index 6809558b..eb810b43 100644 --- a/test/screenshotter/test.html +++ b/test/screenshotter/test.html @@ -71,6 +71,7 @@ var settings = { displayMode: !!query["display"], throwOnError: !query["noThrow"], + strict: false, trust: true // trust test inputs }; if (query["errorColor"]) { diff --git a/website/lib/remarkable-katex.js b/website/lib/remarkable-katex.js index afd50bd2..d6007354 100644 --- a/website/lib/remarkable-katex.js +++ b/website/lib/remarkable-katex.js @@ -30,7 +30,7 @@ module.exports = function(md, options) { function renderKatex(source, displayMode) { return katex.renderToString(source, - {displayMode, throwOnError: false, trust: true}); + {displayMode, throwOnError: false, trust: true, strict: false}); } /**