mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-05 11:18:39 +00:00
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:
committed by
Kevin Barabash
parent
9d8195c793
commit
e5333ad04d
@@ -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);
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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:
|
||||
|
||||
|
@@ -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`|
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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<TrustContextTypes>;
|
||||
export type TrustFunction = (context: AnyTrustContext) => ?boolean;
|
||||
|
@@ -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<HtmlDomNode> | Anchor) {
|
||||
if (node instanceof DocumentFragment || node instanceof Anchor) {
|
||||
): ?(DocumentFragment<HtmlDomNode> | Anchor | DomSpan) {
|
||||
if (node instanceof DocumentFragment || node instanceof Anchor
|
||||
|| (node instanceof Span && node.hasClass("enclosing"))) {
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
|
@@ -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";
|
||||
|
102
src/functions/html.js
Normal file
102
src/functions/html.js
Normal 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);
|
||||
},
|
||||
});
|
@@ -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,
|
||||
|
@@ -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`] = `
|
||||
[
|
||||
{
|
||||
|
@@ -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`;
|
||||
|
BIN
test/screenshotter/images/HTML-chrome.png
Normal file
BIN
test/screenshotter/images/HTML-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
BIN
test/screenshotter/images/HTML-firefox.png
Normal file
BIN
test/screenshotter/images/HTML-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
@@ -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}}
|
||||
|
@@ -71,6 +71,7 @@
|
||||
var settings = {
|
||||
displayMode: !!query["display"],
|
||||
throwOnError: !query["noThrow"],
|
||||
strict: false,
|
||||
trust: true // trust test inputs
|
||||
};
|
||||
if (query["errorColor"]) {
|
||||
|
@@ -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});
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user