Add HTML extension (#2082)

* Add html extension

* Fix flow error

* Update documentation

* Add tests

* Call buildA11yStrings for "html" node

* Throw ParseError when parsing \htmlData fails

* Improve documentation

* Add a screenshotter test

* Add dummy screenshot

* Update screenshots
This commit is contained in:
ylemkimon
2019-12-02 07:49:28 +09:00
committed by Kevin Barabash
parent 9d8195c793
commit e5333ad04d
17 changed files with 408 additions and 4 deletions

View File

@@ -655,6 +655,11 @@ const handleObject = (
break; break;
} }
case "html": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
default: default:
(tree.type: empty); (tree.type: empty);
throw new Error("KaTeX a11y un-recognized type: " + tree.type); throw new Error("KaTeX a11y un-recognized type: " + tree.type);

View File

@@ -22,6 +22,8 @@ title: Common Issues
- MathJax defines `\color` to be like `\textcolor` by default; set KaTeX's - MathJax defines `\color` to be like `\textcolor` by default; set KaTeX's
`colorIsTextColor` option to `true` for this behavior. KaTeX's default `colorIsTextColor` option to `true` for this behavior. KaTeX's default
behavior matches MathJax with its `color.js` extension enabled. 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 ## Troubleshooting

View File

@@ -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. - `"commentAtEnd"`: Use of `%` comment without a terminating newline.
LaTeX would thereby comment out the end of math mode (e.g. `$`), LaTeX would thereby comment out the end of math mode (e.g. `$`),
causing an error. 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 A second category of `errorCode`s never throw errors, but their strictness
affects the behavior of KaTeX: 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: "\\url", url, protocol}`
- `{command: "\\href", url, protocol}` - `{command: "\\href", url, protocol}`
- `{command: "\\includegraphics", url, protocol}` - `{command: "\\includegraphics", url, protocol}`
- `{command: "\\htmlClass", class}`
- `{command: "\\htmlId", id}`
- `{command: "\\htmlStyle", style}`
- `{command: "\\htmlData", attributes}`
Here are some sample trust settings: Here are some sample trust settings:

View File

@@ -458,6 +458,10 @@ use `\ce` instead|
|\hskip|$w\hskip1em i\hskip2em d$|`w\hskip1em i\hskip2em d`| |\hskip|$w\hskip1em i\hskip2em d$|`w\hskip1em i\hskip2em d`|
|\hslash|$\hslash$|| |\hslash|$\hslash$||
|\hspace|$s\hspace7ex k$|`s\hspace7ex k`| |\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`|
|\Huge|$\Huge Huge$|`\Huge Huge`| |\Huge|$\Huge Huge$|`\Huge Huge`|

View File

@@ -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}` | | $\href{https://katex.org/}{\KaTeX}$ | `\href{https://katex.org/}{\KaTeX}` |
| $\url{https://katex.org/}$ | `\url{https://katex.org/}` | | $\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}` | | $\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. `\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 ## Letters and Unicode

View File

@@ -32,6 +32,22 @@ export type TrustContextTypes = {
url: string, url: string,
protocol?: 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<TrustContextTypes>; export type AnyTrustContext = $Values<TrustContextTypes>;
export type TrustFunction = (context: AnyTrustContext) => ?boolean; export type TrustFunction = (context: AnyTrustContext) => ?boolean;

View File

@@ -9,7 +9,7 @@
import ParseError from "./ParseError"; import ParseError from "./ParseError";
import Style from "./Style"; import Style from "./Style";
import buildCommon from "./buildCommon"; import buildCommon from "./buildCommon";
import {Anchor} from "./domTree"; import {Span, Anchor} from "./domTree";
import utils from "./utils"; import utils from "./utils";
import {spacings, tightSpacings} from "./spacingData"; import {spacings, tightSpacings} from "./spacingData";
import {_htmlGroupBuilders as groupBuilders} from "./defineFunction"; 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. // Check if given node is a partial group, i.e., does not affect spacing around.
const checkPartialGroup = function( const checkPartialGroup = function(
node: HtmlDomNode, node: HtmlDomNode,
): ?(DocumentFragment<HtmlDomNode> | Anchor) { ): ?(DocumentFragment<HtmlDomNode> | Anchor | DomSpan) {
if (node instanceof DocumentFragment || node instanceof Anchor) { if (node instanceof DocumentFragment || node instanceof Anchor
|| (node instanceof Span && node.hasClass("enclosing"))) {
return node; return node;
} }
return null; return null;

View File

@@ -20,6 +20,7 @@ import "./functions/font";
import "./functions/genfrac"; import "./functions/genfrac";
import "./functions/horizBrace"; import "./functions/horizBrace";
import "./functions/href"; import "./functions/href";
import "./functions/html";
import "./functions/htmlmathml"; import "./functions/htmlmathml";
import "./functions/includegraphics"; import "./functions/includegraphics";
import "./functions/kern"; import "./functions/kern";

102
src/functions/html.js Normal file
View File

@@ -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);
},
});

View File

@@ -270,6 +270,13 @@ type ParseNodeTypes = {
href: string, href: string,
body: AnyParseNode[], body: AnyParseNode[],
|}, |},
"html": {|
type: "html",
mode: Mode,
loc?: ?SourceLocation,
attributes: {[string]: string},
body: AnyParseNode[],
|},
"htmlmathml": {| "htmlmathml": {|
type: "htmlmathml", type: "htmlmathml",
mode: Mode, mode: Mode,

View File

@@ -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`] = ` exports[`An implicit group parser within optional groups should work style commands \\sqrt[\\textstyle 3]{x} 1`] = `
[ [
{ {

View File

@@ -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() { describe("A bin builder", function() {
it("should create mbins normally", function() { it("should create mbins normally", function() {
const built = getBuilt`x + y`; const built = getBuilt`x + y`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -149,6 +149,9 @@ GroupMacros:
\endExp: \egroup \endExp: \egroup
tex: \startExp a+b\endExp 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}} 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: | Includegraphics: |
\def\logo{\includegraphics[height=0.8em, totalheight=0.9em, width=0.9em, alt=KA logo]{../../website/static/img/khan-academy.png}} \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}} \def\logoB{\includegraphics[height=0.4em, totalheight=0.9em, width=0.9em, alt=KA logo]{../../website/static/img/khan-academy.png}}

View File

@@ -71,6 +71,7 @@
var settings = { var settings = {
displayMode: !!query["display"], displayMode: !!query["display"],
throwOnError: !query["noThrow"], throwOnError: !query["noThrow"],
strict: false,
trust: true // trust test inputs trust: true // trust test inputs
}; };
if (query["errorColor"]) { if (query["errorColor"]) {

View File

@@ -30,7 +30,7 @@ module.exports = function(md, options) {
function renderKatex(source, displayMode) { function renderKatex(source, displayMode) {
return katex.renderToString(source, return katex.renderToString(source,
{displayMode, throwOnError: false, trust: true}); {displayMode, throwOnError: false, trust: true, strict: false});
} }
/** /**