mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-10 21:48:41 +00:00
Support \tag, \tag*, and \gdef (#1309)
* Tag sketch * Drop objectAssign; already using Object.assign elsewhere * Basic \gdef support * Bug fix * Finish \tag * MathML numbers equations with <mlabeledtr> * Fix flow bugs * \gdef tests * Add basic \tag tests * Screenshot test for \tag * \tag* test * Add missing file * Bug fix screenshot * Major refactor * Represent tag at top level of parse tree, requiring less hackery * No more \@tag function; it was essentially just doing \text * Wrap tag in group so e.g. ( and ) are formatted the same * Add `feed` method to MacroExpander for multiple inputs (for tag) * Bug fixes in buildHTML, makeTextRow, _getBuilt (for display mode) * Remove excess <mrow> wrapper when unnecessary * Update screenshot from tag being wrapped in group * Add maxExpand limit
This commit is contained in:
committed by
Kevin Barabash
parent
99b2afa935
commit
a0ddad338e
@@ -54,7 +54,8 @@ Make sure to include the CSS and font files, but there is no need to include the
|
|||||||
|
|
||||||
Any HTML generated by KaTeX *should* be safe from `<script>` or other code
|
Any HTML generated by KaTeX *should* be safe from `<script>` or other code
|
||||||
injection attacks.
|
injection attacks.
|
||||||
(See `maxSize` below for preventing large width/height visual affronts.)
|
(See `maxSize` below for preventing large width/height visual affronts,
|
||||||
|
and see `maxExpand` below for preventing infinite macro loop attacks.)
|
||||||
Of course, it is always a good idea to sanitize the HTML, though you will need
|
Of course, it is always a good idea to sanitize the HTML, though you will need
|
||||||
a rather generous whitelist (including some of SVG and MathML) to support
|
a rather generous whitelist (including some of SVG and MathML) to support
|
||||||
all of KaTeX.
|
all of KaTeX.
|
||||||
@@ -82,7 +83,8 @@ You can provide an object of options as the last argument to `katex.render` and
|
|||||||
- `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`)
|
- `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. Single-character keys can also be included in which case the character will be redefined as the given macro (similar to TeX active characters).
|
- `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. Single-character keys can also be included in which case the character will be redefined as the given macro (similar to TeX active characters).
|
||||||
- `colorIsTextColor`: `boolean`. If `true`, `\color` will work like LaTeX's `\textcolor`, and take two arguments (e.g., `\color{blue}{hello}`), which restores the old behavior of KaTeX (pre-0.8.0). If `false` (the default), `\color` will work like LaTeX's `\color`, and take one argument (e.g., `\color{blue}hello`). In both cases, `\textcolor` works as in LaTeX (e.g., `\textcolor{blue}{hello}`).
|
- `colorIsTextColor`: `boolean`. If `true`, `\color` will work like LaTeX's `\textcolor`, and take two arguments (e.g., `\color{blue}{hello}`), which restores the old behavior of KaTeX (pre-0.8.0). If `false` (the default), `\color` will work like LaTeX's `\color`, and take one argument (e.g., `\color{blue}hello`). In both cases, `\textcolor` works as in LaTeX (e.g., `\textcolor{blue}{hello}`).
|
||||||
- `maxSize`: `number`. If non-zero, all user-specified sizes, e.g. in `\rule{500em}{500em}`, will be capped to `maxSize` ems. Otherwise, users can make elements and spaces arbitrarily large (the default behavior).
|
- `maxSize`: `number`. All user-specified sizes, e.g. in `\rule{500em}{500em}`, will be capped to `maxSize` ems. If set to `Infinity` (the default), users can make elements and spaces arbitrarily large.
|
||||||
|
- `maxExpand`: `number`. Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to `Infinity` (the default), the macro expander will try to fully expand as in LaTeX.
|
||||||
- `strict`: `boolean` or `string` or `function` (default: `"warn"`). If `false` or `"ignore`", allow features that make writing LaTeX convenient but are not actually supported by (Xe)LaTeX (similar to MathJax). If `true` or `"error"` (LaTeX faithfulness mode), throw an error for any such transgressions. If `"warn"` (the default), warn about such behavior via `console.warn`. Provide a custom function `handler(errorCode, errorMsg, token)` to customize behavior depending on the type of transgression (summarized by the string code `errorCode` and detailed in `errorMsg`); this function can also return `"ignore"`, `"error"`, or `"warn"` to use a built-in behavior. A list of such features and their `errorCode`s:
|
- `strict`: `boolean` or `string` or `function` (default: `"warn"`). If `false` or `"ignore`", allow features that make writing LaTeX convenient but are not actually supported by (Xe)LaTeX (similar to MathJax). If `true` or `"error"` (LaTeX faithfulness mode), throw an error for any such transgressions. If `"warn"` (the default), warn about such behavior via `console.warn`. Provide a custom function `handler(errorCode, errorMsg, token)` to customize behavior depending on the type of transgression (summarized by the string code `errorCode` and detailed in `errorMsg`); this function can also return `"ignore"`, `"error"`, or `"warn"` to use a built-in behavior. A list of such features and their `errorCode`s:
|
||||||
- `"unknownSymbol"`: Use of unknown Unicode symbol, which will likely also
|
- `"unknownSymbol"`: Use of unknown Unicode symbol, which will likely also
|
||||||
lead to warnings about missing character metrics, and layouts may be
|
lead to warnings about missing character metrics, and layouts may be
|
||||||
|
@@ -40,7 +40,6 @@
|
|||||||
"less": "~2.7.1",
|
"less": "~2.7.1",
|
||||||
"less-loader": "^4.0.5",
|
"less-loader": "^4.0.5",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"object-assign": "^4.1.0",
|
|
||||||
"pako": "1.0.4",
|
"pako": "1.0.4",
|
||||||
"pre-commit": "^1.2.2",
|
"pre-commit": "^1.2.2",
|
||||||
"query-string": "^5.1.0",
|
"query-string": "^5.1.0",
|
||||||
|
@@ -9,23 +9,33 @@ import {Token} from "./Token";
|
|||||||
import builtinMacros from "./macros";
|
import builtinMacros from "./macros";
|
||||||
import type {Mode} from "./types";
|
import type {Mode} from "./types";
|
||||||
import ParseError from "./ParseError";
|
import ParseError from "./ParseError";
|
||||||
import objectAssign from "object-assign";
|
|
||||||
|
|
||||||
import type {MacroContextInterface, MacroMap, MacroExpansion} from "./macros";
|
import type {MacroContextInterface, MacroMap, MacroExpansion} from "./macros";
|
||||||
|
import type Settings from "./Settings";
|
||||||
|
|
||||||
export default class MacroExpander implements MacroContextInterface {
|
export default class MacroExpander implements MacroContextInterface {
|
||||||
lexer: Lexer;
|
lexer: Lexer;
|
||||||
macros: MacroMap;
|
macros: MacroMap;
|
||||||
|
maxExpand: number;
|
||||||
stack: Token[];
|
stack: Token[];
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
|
|
||||||
constructor(input: string, macros: MacroMap, mode: Mode) {
|
constructor(input: string, settings: Settings, mode: Mode) {
|
||||||
this.lexer = new Lexer(input);
|
this.feed(input);
|
||||||
this.macros = objectAssign({}, builtinMacros, macros);
|
this.macros = Object.assign({}, builtinMacros, settings.macros);
|
||||||
|
this.maxExpand = settings.maxExpand;
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
this.stack = []; // contains tokens in REVERSE order
|
this.stack = []; // contains tokens in REVERSE order
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feed a new input string to the same MacroExpander
|
||||||
|
* (with existing macros etc.).
|
||||||
|
*/
|
||||||
|
feed(input: string) {
|
||||||
|
this.lexer = new Lexer(input);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches between "text" and "math" modes.
|
* Switches between "text" and "math" modes.
|
||||||
*/
|
*/
|
||||||
@@ -149,6 +159,13 @@ export default class MacroExpander implements MacroContextInterface {
|
|||||||
return topToken;
|
return topToken;
|
||||||
}
|
}
|
||||||
const {tokens, numArgs} = this._getExpansion(name);
|
const {tokens, numArgs} = this._getExpansion(name);
|
||||||
|
if (this.maxExpand !== Infinity) {
|
||||||
|
this.maxExpand--;
|
||||||
|
if (this.maxExpand < 0) {
|
||||||
|
throw new ParseError("Too many expansions: infinite loop or " +
|
||||||
|
"need to increase maxExpand setting");
|
||||||
|
}
|
||||||
|
}
|
||||||
let expansion = tokens;
|
let expansion = tokens;
|
||||||
if (numArgs) {
|
if (numArgs) {
|
||||||
const args = this.consumeArgs(numArgs);
|
const args = this.consumeArgs(numArgs);
|
||||||
|
@@ -103,6 +103,10 @@ type ParseNodeTypes = {
|
|||||||
sup?: ?ParseNode<*>,
|
sup?: ?ParseNode<*>,
|
||||||
sub?: ?ParseNode<*>,
|
sub?: ?ParseNode<*>,
|
||||||
|},
|
|},
|
||||||
|
"tag": {|
|
||||||
|
body: ParseNode<*>[],
|
||||||
|
tag: ParseNode<*>[],
|
||||||
|
|},
|
||||||
"text": {|
|
"text": {|
|
||||||
type: "text",
|
type: "text",
|
||||||
body: ParseNode<*>[],
|
body: ParseNode<*>[],
|
||||||
|
@@ -85,7 +85,7 @@ export default class Parser {
|
|||||||
this.mode = "math";
|
this.mode = "math";
|
||||||
// Create a new macro expander (gullet) and (indirectly via that) also a
|
// Create a new macro expander (gullet) and (indirectly via that) also a
|
||||||
// new lexer (mouth) for this parser (stomach, in the language of TeX)
|
// new lexer (mouth) for this parser (stomach, in the language of TeX)
|
||||||
this.gullet = new MacroExpander(input, settings.macros, this.mode);
|
this.gullet = new MacroExpander(input, settings, this.mode);
|
||||||
// Use old \color behavior (same as LaTeX's \textcolor) if requested.
|
// Use old \color behavior (same as LaTeX's \textcolor) if requested.
|
||||||
// We do this after the macros object has been copied by MacroExpander.
|
// We do this after the macros object has been copied by MacroExpander.
|
||||||
if (settings.colorIsTextColor) {
|
if (settings.colorIsTextColor) {
|
||||||
|
@@ -24,6 +24,7 @@ export type SettingsOptions = {
|
|||||||
colorIsTextColor?: boolean;
|
colorIsTextColor?: boolean;
|
||||||
strict?: boolean | "ignore" | "warn" | "error" | StrictFunction;
|
strict?: boolean | "ignore" | "warn" | "error" | StrictFunction;
|
||||||
maxSize?: number;
|
maxSize?: number;
|
||||||
|
maxExpand?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +45,7 @@ class Settings {
|
|||||||
colorIsTextColor: boolean;
|
colorIsTextColor: boolean;
|
||||||
strict: boolean | "ignore" | "warn" | "error" | StrictFunction;
|
strict: boolean | "ignore" | "warn" | "error" | StrictFunction;
|
||||||
maxSize: number;
|
maxSize: number;
|
||||||
|
maxExpand: number;
|
||||||
|
|
||||||
constructor(options: SettingsOptions) {
|
constructor(options: SettingsOptions) {
|
||||||
// allow null options
|
// allow null options
|
||||||
@@ -55,6 +57,7 @@ class Settings {
|
|||||||
this.colorIsTextColor = utils.deflt(options.colorIsTextColor, false);
|
this.colorIsTextColor = utils.deflt(options.colorIsTextColor, false);
|
||||||
this.strict = utils.deflt(options.strict, "warn");
|
this.strict = utils.deflt(options.strict, "warn");
|
||||||
this.maxSize = Math.max(0, utils.deflt(options.maxSize, Infinity));
|
this.maxSize = Math.max(0, utils.deflt(options.maxSize, Infinity));
|
||||||
|
this.maxExpand = Math.max(0, utils.deflt(options.maxExpand, Infinity));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -659,11 +659,17 @@ export default function buildHTML(tree, options) {
|
|||||||
// of the incoming tree so that it isn't accidentally changed
|
// of the incoming tree so that it isn't accidentally changed
|
||||||
tree = JSON.parse(JSON.stringify(tree));
|
tree = JSON.parse(JSON.stringify(tree));
|
||||||
|
|
||||||
|
// Strip off outer tag wrapper for processing below.
|
||||||
|
let tag = null;
|
||||||
|
if (tree.length === 1 && tree[0].type === "tag") {
|
||||||
|
tag = tree[0].value.tag;
|
||||||
|
tree = tree[0].value.body;
|
||||||
|
}
|
||||||
|
|
||||||
// Build the expression contained in the tree
|
// Build the expression contained in the tree
|
||||||
const expression = buildExpression(tree, options, true);
|
const expression = buildExpression(tree, options, true);
|
||||||
|
|
||||||
const htmlNode = makeSpan(["katex-html"], []);
|
const children = [];
|
||||||
htmlNode.setAttribute("aria-hidden", "true");
|
|
||||||
|
|
||||||
// Create one base node for each chunk between potential line breaks.
|
// Create one base node for each chunk between potential line breaks.
|
||||||
// The TeXBook [p.173] says "A formula will be broken only after a
|
// The TeXBook [p.173] says "A formula will be broken only after a
|
||||||
@@ -691,22 +697,43 @@ export default function buildHTML(tree, options) {
|
|||||||
}
|
}
|
||||||
// Don't allow break if \nobreak among the post-operator glue.
|
// Don't allow break if \nobreak among the post-operator glue.
|
||||||
if (!nobreak) {
|
if (!nobreak) {
|
||||||
htmlNode.children.push(buildHTMLUnbreakable(parts, options));
|
children.push(buildHTMLUnbreakable(parts, options));
|
||||||
parts = [];
|
parts = [];
|
||||||
}
|
}
|
||||||
} else if (expression[i].hasClass("newline")) {
|
} else if (expression[i].hasClass("newline")) {
|
||||||
// Write the line except the newline
|
// Write the line except the newline
|
||||||
parts.pop();
|
parts.pop();
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
htmlNode.children.push(buildHTMLUnbreakable(parts, options));
|
children.push(buildHTMLUnbreakable(parts, options));
|
||||||
parts = [];
|
parts = [];
|
||||||
}
|
}
|
||||||
// Put the newline at the top level
|
// Put the newline at the top level
|
||||||
htmlNode.children.push(expression[i]);
|
children.push(expression[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
htmlNode.children.push(buildHTMLUnbreakable(parts, options));
|
children.push(buildHTMLUnbreakable(parts, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, if there was a tag, build it too and append it as a final child.
|
||||||
|
let tagChild;
|
||||||
|
if (tag) {
|
||||||
|
tagChild = buildHTMLUnbreakable(
|
||||||
|
buildExpression(tag, options, true)
|
||||||
|
);
|
||||||
|
tagChild.classes = ["tag"];
|
||||||
|
children.push(tagChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlNode = makeSpan(["katex-html"], children);
|
||||||
|
htmlNode.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
|
// Adjust the strut of the tag to be the maximum height of all children
|
||||||
|
// (the height of the enclosing htmlNode) for proper vertical alignment.
|
||||||
|
if (tag) {
|
||||||
|
const strut = tagChild.children[0];
|
||||||
|
strut.style.height = (htmlNode.height + htmlNode.depth) + "em";
|
||||||
|
strut.style.verticalAlign = (-htmlNode.depth) + "em";
|
||||||
}
|
}
|
||||||
|
|
||||||
return htmlNode;
|
return htmlNode;
|
||||||
|
@@ -29,6 +29,35 @@ export const makeText = function(text, mode) {
|
|||||||
return new mathMLTree.TextNode(text);
|
return new mathMLTree.TextNode(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const makeTextRow = function(body, options) {
|
||||||
|
// Convert each element of the body into MathML, and combine consecutive
|
||||||
|
// <mtext> outputs into a single <mtext> tag. In this way, we don't
|
||||||
|
// nest non-text items (e.g., $nested-math$) within an <mtext>.
|
||||||
|
const inner = [];
|
||||||
|
let currentText = null;
|
||||||
|
for (let i = 0; i < body.length; i++) {
|
||||||
|
const group = buildGroup(body[i], options);
|
||||||
|
if (group.type === 'mtext' && currentText !== null) {
|
||||||
|
Array.prototype.push.apply(currentText.children, group.children);
|
||||||
|
} else {
|
||||||
|
inner.push(group);
|
||||||
|
if (group.type === 'mtext') {
|
||||||
|
currentText = group;
|
||||||
|
} else {
|
||||||
|
currentText = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a single tag in the end (presumably <mtext>),
|
||||||
|
// just return it. Otherwise, wrap them in an <mrow>.
|
||||||
|
if (inner.length === 1) {
|
||||||
|
return inner[0];
|
||||||
|
} else {
|
||||||
|
return new mathMLTree.MathNode("mrow", inner);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the math variant as a string or null if none is required.
|
* Returns the math variant as a string or null if none is required.
|
||||||
*/
|
*/
|
||||||
@@ -283,6 +312,21 @@ groupTypes.raisebox = function(group, options) {
|
|||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
groupTypes.tag = function(group, options) {
|
||||||
|
const table = new mathMLTree.MathNode("mtable", [
|
||||||
|
new mathMLTree.MathNode("mlabeledtr", [
|
||||||
|
new mathMLTree.MathNode("mtd",
|
||||||
|
buildExpression(group.value.tag, options)),
|
||||||
|
new mathMLTree.MathNode("mtd", [
|
||||||
|
new mathMLTree.MathNode("mrow",
|
||||||
|
buildExpression(group.value.body, options)),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
table.setAttribute("side", "right");
|
||||||
|
return table;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a list of nodes, builds them, and returns a list of the generated
|
* Takes a list of nodes, builds them, and returns a list of the generated
|
||||||
* MathML nodes. A little simpler than the HTML version because we don't do any
|
* MathML nodes. A little simpler than the HTML version because we don't do any
|
||||||
@@ -339,7 +383,13 @@ export default function buildMathML(tree, texExpression, options) {
|
|||||||
|
|
||||||
// Wrap up the expression in an mrow so it is presented in the semantics
|
// Wrap up the expression in an mrow so it is presented in the semantics
|
||||||
// tag correctly.
|
// tag correctly.
|
||||||
const wrapper = new mathMLTree.MathNode("mrow", expression);
|
let wrapper;
|
||||||
|
if (expression.length === 1 &&
|
||||||
|
utils.contains(["mrow", "mtable"], expression[0].type)) {
|
||||||
|
wrapper = expression[0];
|
||||||
|
} else {
|
||||||
|
wrapper = new mathMLTree.MathNode("mrow", expression);
|
||||||
|
}
|
||||||
|
|
||||||
// Build a TeX annotation of the source
|
// Build a TeX annotation of the source
|
||||||
const annotation = new mathMLTree.MathNode(
|
const annotation = new mathMLTree.MathNode(
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import defineFunction, {ordargument} from "../defineFunction";
|
import defineFunction, {ordargument} from "../defineFunction";
|
||||||
import buildCommon from "../buildCommon";
|
import buildCommon from "../buildCommon";
|
||||||
import mathMLTree from "../mathMLTree";
|
|
||||||
|
|
||||||
import * as html from "../buildHTML";
|
import * as html from "../buildHTML";
|
||||||
import * as mml from "../buildMathML";
|
import * as mml from "../buildMathML";
|
||||||
@@ -63,31 +62,6 @@ defineFunction({
|
|||||||
return buildCommon.makeSpan(["mord", "text"], inner, newOptions);
|
return buildCommon.makeSpan(["mord", "text"], inner, newOptions);
|
||||||
},
|
},
|
||||||
mathmlBuilder(group, options) {
|
mathmlBuilder(group, options) {
|
||||||
const body = group.value.body;
|
return mml.makeTextRow(group.value.body, options);
|
||||||
|
|
||||||
// Convert each element of the body into MathML, and combine consecutive
|
|
||||||
// <mtext> outputs into a single <mtext> tag. In this way, we don't
|
|
||||||
// nest non-text items (e.g., $nested-math$) within an <mtext>.
|
|
||||||
const inner = [];
|
|
||||||
let currentText = null;
|
|
||||||
for (let i = 0; i < body.length; i++) {
|
|
||||||
const group = mml.buildGroup(body[i], options);
|
|
||||||
if (group.type === 'mtext' && currentText != null) {
|
|
||||||
Array.prototype.push.apply(currentText.children, group.children);
|
|
||||||
} else {
|
|
||||||
inner.push(group);
|
|
||||||
if (group.type === 'mtext') {
|
|
||||||
currentText = group;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is a single tag in the end (presumably <mtext>),
|
|
||||||
// just return it. Otherwise, wrap them in an <mrow>.
|
|
||||||
if (inner.length === 1) {
|
|
||||||
return inner[0];
|
|
||||||
} else {
|
|
||||||
return new mathMLTree.MathNode("mrow", inner);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -24,12 +24,17 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
> .katex {
|
> .katex {
|
||||||
display: inline-block;
|
display: block;
|
||||||
text-align: initial;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
> .katex-html {
|
> .katex-html {
|
||||||
display: inline-block;
|
display: block;
|
||||||
|
|
||||||
|
> .tag {
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,12 +8,18 @@ import fontMetricsData from "../submodules/katex-fonts/fontMetricsData";
|
|||||||
import symbols from "./symbols";
|
import symbols from "./symbols";
|
||||||
import utils from "./utils";
|
import utils from "./utils";
|
||||||
import {Token} from "./Token";
|
import {Token} from "./Token";
|
||||||
|
import ParseError from "./ParseError";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides context to macros defined by functions. Implemented by
|
* Provides context to macros defined by functions. Implemented by
|
||||||
* MacroExpander.
|
* MacroExpander.
|
||||||
*/
|
*/
|
||||||
export interface MacroContextInterface {
|
export interface MacroContextInterface {
|
||||||
|
/**
|
||||||
|
* Object mapping macros to their expansions.
|
||||||
|
*/
|
||||||
|
macros: MacroMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the topmost token on the stack, without expanding it.
|
* Returns the topmost token on the stack, without expanding it.
|
||||||
* Similar in behavior to TeX's `\futurelet`.
|
* Similar in behavior to TeX's `\futurelet`.
|
||||||
@@ -95,6 +101,42 @@ defineMacro("\\TextOrMath", function(context) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Basic support for global macro definitions:
|
||||||
|
// \gdef\macro{expansion}
|
||||||
|
// \gdef\macro#1{expansion}
|
||||||
|
// \gdef\macro#1#2{expansion}
|
||||||
|
// \gdef\macro#1#2#3#4#5#6#7#8#9{expansion}
|
||||||
|
defineMacro("\\gdef", function(context) {
|
||||||
|
let arg = context.consumeArgs(1)[0];
|
||||||
|
if (arg.length !== 1) {
|
||||||
|
throw new ParseError("\\gdef's first argument must be a macro name");
|
||||||
|
}
|
||||||
|
const name = arg[0].text;
|
||||||
|
// Count argument specifiers, and check they are in the order #1 #2 ...
|
||||||
|
let numArgs = 0;
|
||||||
|
arg = context.consumeArgs(1)[0];
|
||||||
|
while (arg.length === 1 && arg[0].text === "#") {
|
||||||
|
arg = context.consumeArgs(1)[0];
|
||||||
|
if (arg.length !== 1) {
|
||||||
|
throw new ParseError(`Invalid argument number length "${arg.length}"`);
|
||||||
|
}
|
||||||
|
if (!(/^[1-9]$/.test(arg[0].text))) {
|
||||||
|
throw new ParseError(`Invalid argument number "${arg[0].text}"`);
|
||||||
|
}
|
||||||
|
numArgs++;
|
||||||
|
if (parseInt(arg[0].text) !== numArgs) {
|
||||||
|
throw new ParseError(`Argument number "${arg[0].text}" out of order`);
|
||||||
|
}
|
||||||
|
arg = context.consumeArgs(1)[0];
|
||||||
|
}
|
||||||
|
// Final arg is the expansion of the macro
|
||||||
|
context.macros[name] = {
|
||||||
|
tokens: arg,
|
||||||
|
numArgs,
|
||||||
|
};
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Grouping
|
// Grouping
|
||||||
// \let\bgroup={ \let\egroup=}
|
// \let\bgroup={ \let\egroup=}
|
||||||
@@ -354,6 +396,16 @@ defineMacro("\\thinspace", "\\,"); // \let\thinspace\,
|
|||||||
defineMacro("\\medspace", "\\:"); // \let\medspace\:
|
defineMacro("\\medspace", "\\:"); // \let\medspace\:
|
||||||
defineMacro("\\thickspace", "\\;"); // \let\thickspace\;
|
defineMacro("\\thickspace", "\\;"); // \let\thickspace\;
|
||||||
|
|
||||||
|
// \tag@in@display form of \tag
|
||||||
|
defineMacro("\\tag", "\\@ifstar\\tag@literal\\tag@paren");
|
||||||
|
defineMacro("\\tag@paren", "\\tag@literal{({#1})}");
|
||||||
|
defineMacro("\\tag@literal", (context) => {
|
||||||
|
if (context.macros["\\df@tag"]) {
|
||||||
|
throw new ParseError("Multiple \\tag");
|
||||||
|
}
|
||||||
|
return "\\gdef\\df@tag{\\text{#1}}";
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// LaTeX source2e
|
// LaTeX source2e
|
||||||
|
|
||||||
|
@@ -5,8 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Parser from "./Parser";
|
import Parser from "./Parser";
|
||||||
|
import ParseError from "./ParseError";
|
||||||
|
import ParseNode from "./ParseNode";
|
||||||
|
|
||||||
import type ParseNode from "./ParseNode";
|
|
||||||
import type Settings from "./Settings";
|
import type Settings from "./Settings";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,8 +18,22 @@ const parseTree = function(toParse: string, settings: Settings): ParseNode<*>[]
|
|||||||
throw new TypeError('KaTeX can only parse string typed expression');
|
throw new TypeError('KaTeX can only parse string typed expression');
|
||||||
}
|
}
|
||||||
const parser = new Parser(toParse, settings);
|
const parser = new Parser(toParse, settings);
|
||||||
|
let tree = parser.parse();
|
||||||
|
|
||||||
return parser.parse();
|
// If the input used \tag, it will set the \df@tag macro to the tag.
|
||||||
|
// In this case, we separately parse the tag and wrap the tree.
|
||||||
|
if (parser.gullet.macros["\\df@tag"]) {
|
||||||
|
if (!settings.displayMode) {
|
||||||
|
throw new ParseError("\\tag works only in display equations");
|
||||||
|
}
|
||||||
|
parser.gullet.feed("\\df@tag");
|
||||||
|
tree = [new ParseNode("tag", {
|
||||||
|
body: tree,
|
||||||
|
tag: parser.parse(),
|
||||||
|
}, "text")];
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default parseTree;
|
export default parseTree;
|
||||||
|
@@ -209,32 +209,30 @@ exports[`A MathML builder should render boldsymbol with the correct mathvariants
|
|||||||
<math>
|
<math>
|
||||||
<semantics>
|
<semantics>
|
||||||
<mrow>
|
<mrow>
|
||||||
<mrow>
|
<mi mathvariant="bold-italic">
|
||||||
<mi mathvariant="bold-italic">
|
A
|
||||||
A
|
</mi>
|
||||||
</mi>
|
<mi mathvariant="bold-italic">
|
||||||
<mi mathvariant="bold-italic">
|
x
|
||||||
x
|
</mi>
|
||||||
</mi>
|
<mn mathvariant="bold-italic">
|
||||||
<mn mathvariant="bold-italic">
|
2
|
||||||
2
|
</mn>
|
||||||
</mn>
|
<mi mathvariant="bold-italic">
|
||||||
<mi mathvariant="bold-italic">
|
k
|
||||||
k
|
</mi>
|
||||||
</mi>
|
<mi mathvariant="bold-italic">
|
||||||
<mi mathvariant="bold-italic">
|
ω
|
||||||
ω
|
</mi>
|
||||||
</mi>
|
<mi mathvariant="bold-italic">
|
||||||
<mi mathvariant="bold-italic">
|
Ω
|
||||||
Ω
|
</mi>
|
||||||
</mi>
|
<mi mathvariant="bold-italic">
|
||||||
<mi mathvariant="bold-italic">
|
ı
|
||||||
ı
|
</mi>
|
||||||
</mi>
|
<mo mathvariant="bold-italic">
|
||||||
<mo mathvariant="bold-italic">
|
+
|
||||||
+
|
</mo>
|
||||||
</mo>
|
|
||||||
</mrow>
|
|
||||||
</mrow>
|
</mrow>
|
||||||
<annotation encoding="application/x-tex">
|
<annotation encoding="application/x-tex">
|
||||||
\\boldsymbol{Ax2k\\omega\\Omega\\imath+}
|
\\boldsymbol{Ax2k\\omega\\Omega\\imath+}
|
||||||
@@ -296,35 +294,33 @@ exports[`A MathML builder should render mathchoice as if there was nothing 2`] =
|
|||||||
<math>
|
<math>
|
||||||
<semantics>
|
<semantics>
|
||||||
<mrow>
|
<mrow>
|
||||||
<mrow>
|
<msubsup>
|
||||||
<msubsup>
|
<mo>
|
||||||
<mo>
|
∑
|
||||||
∑
|
</mo>
|
||||||
</mo>
|
<mrow>
|
||||||
<mrow>
|
|
||||||
<mi>
|
|
||||||
k
|
|
||||||
</mi>
|
|
||||||
<mo>
|
|
||||||
=
|
|
||||||
</mo>
|
|
||||||
<mn>
|
|
||||||
0
|
|
||||||
</mn>
|
|
||||||
</mrow>
|
|
||||||
<mi mathvariant="normal">
|
|
||||||
∞
|
|
||||||
</mi>
|
|
||||||
</msubsup>
|
|
||||||
<msup>
|
|
||||||
<mi>
|
|
||||||
x
|
|
||||||
</mi>
|
|
||||||
<mi>
|
<mi>
|
||||||
k
|
k
|
||||||
</mi>
|
</mi>
|
||||||
</msup>
|
<mo>
|
||||||
</mrow>
|
=
|
||||||
|
</mo>
|
||||||
|
<mn>
|
||||||
|
0
|
||||||
|
</mn>
|
||||||
|
</mrow>
|
||||||
|
<mi mathvariant="normal">
|
||||||
|
∞
|
||||||
|
</mi>
|
||||||
|
</msubsup>
|
||||||
|
<msup>
|
||||||
|
<mi>
|
||||||
|
x
|
||||||
|
</mi>
|
||||||
|
<mi>
|
||||||
|
k
|
||||||
|
</mi>
|
||||||
|
</msup>
|
||||||
</mrow>
|
</mrow>
|
||||||
<annotation encoding="application/x-tex">
|
<annotation encoding="application/x-tex">
|
||||||
\\mathchoice{D}{\\sum_{k = 0}^{\\infty} x^k}{S}{SS}
|
\\mathchoice{D}{\\sum_{k = 0}^{\\infty} x^k}{S}{SS}
|
||||||
@@ -391,12 +387,10 @@ exports[`A MathML builder should set href attribute for href appropriately 1`] =
|
|||||||
|
|
||||||
<math>
|
<math>
|
||||||
<semantics>
|
<semantics>
|
||||||
<mrow>
|
<mrow href="http://example.org">
|
||||||
<mrow href="http://example.org">
|
<mi>
|
||||||
<mi>
|
α
|
||||||
α
|
</mi>
|
||||||
</mi>
|
|
||||||
</mrow>
|
|
||||||
</mrow>
|
</mrow>
|
||||||
<annotation encoding="application/x-tex">
|
<annotation encoding="application/x-tex">
|
||||||
\\href{http://example.org}{\\alpha}
|
\\href{http://example.org}{\\alpha}
|
||||||
@@ -505,3 +499,55 @@ exports[`A MathML builder should use <munderover> for large operators 1`] = `
|
|||||||
</math>
|
</math>
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`A MathML builder tags use <mlabeledtr> 1`] = `
|
||||||
|
|
||||||
|
<math>
|
||||||
|
<semantics>
|
||||||
|
<mtable side="right">
|
||||||
|
<mlabeledtr>
|
||||||
|
<mtd>
|
||||||
|
<mrow>
|
||||||
|
<mtext>
|
||||||
|
(
|
||||||
|
</mtext>
|
||||||
|
<mrow>
|
||||||
|
<mtext>
|
||||||
|
h
|
||||||
|
</mtext>
|
||||||
|
<mtext>
|
||||||
|
i
|
||||||
|
</mtext>
|
||||||
|
</mrow>
|
||||||
|
<mtext>
|
||||||
|
)
|
||||||
|
</mtext>
|
||||||
|
</mrow>
|
||||||
|
</mtd>
|
||||||
|
<mtd>
|
||||||
|
<mrow>
|
||||||
|
<mi>
|
||||||
|
x
|
||||||
|
</mi>
|
||||||
|
<mo>
|
||||||
|
+
|
||||||
|
</mo>
|
||||||
|
<msup>
|
||||||
|
<mi>
|
||||||
|
y
|
||||||
|
</mi>
|
||||||
|
<mn>
|
||||||
|
2
|
||||||
|
</mn>
|
||||||
|
</msup>
|
||||||
|
</mrow>
|
||||||
|
</mtd>
|
||||||
|
</mlabeledtr>
|
||||||
|
</mtable>
|
||||||
|
<annotation encoding="application/x-tex">
|
||||||
|
\\tag{hi} x+y^2
|
||||||
|
</annotation>
|
||||||
|
</semantics>
|
||||||
|
</math>
|
||||||
|
|
||||||
|
`;
|
||||||
|
@@ -11,13 +11,18 @@ export const defaultSettings = new Settings({
|
|||||||
export const strictSettings = new Settings({strict: true});
|
export const strictSettings = new Settings({strict: true});
|
||||||
|
|
||||||
export const _getBuilt = function(expr, settings = defaultSettings) {
|
export const _getBuilt = function(expr, settings = defaultSettings) {
|
||||||
const rootNode = katex.__renderToDomTree(expr, settings);
|
let rootNode = katex.__renderToDomTree(expr, settings);
|
||||||
|
|
||||||
if (rootNode.classes.indexOf('katex-error') >= 0) {
|
if (rootNode.classes.indexOf('katex-error') >= 0) {
|
||||||
return rootNode;
|
return rootNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rootNode.classes.indexOf('katex-display') >= 0) {
|
||||||
|
rootNode = rootNode.children[0];
|
||||||
|
}
|
||||||
|
|
||||||
// grab the root node of the HTML rendering
|
// grab the root node of the HTML rendering
|
||||||
|
// rootNode.children[0] is the MathML rendering
|
||||||
const builtHTML = rootNode.children[1];
|
const builtHTML = rootNode.children[1];
|
||||||
|
|
||||||
// combine the non-strut children of all base spans
|
// combine the non-strut children of all base spans
|
||||||
|
@@ -2663,6 +2663,24 @@ describe("A macro expander", function() {
|
|||||||
// {"\\mode": "\\TextOrMath{text}{math}"});
|
// {"\\mode": "\\TextOrMath{text}{math}"});
|
||||||
//});
|
//});
|
||||||
|
|
||||||
|
it("\\gdef defines macros", function() {
|
||||||
|
compareParseTree("\\gdef\\foo{x^2}\\foo+\\foo", "x^2+x^2");
|
||||||
|
compareParseTree("\\gdef{\\foo}{x^2}\\foo+\\foo", "x^2+x^2");
|
||||||
|
compareParseTree("\\gdef\\foo{hi}\\foo+\\text{\\foo}", "hi+\\text{hi}");
|
||||||
|
compareParseTree("\\gdef\\foo#1{hi #1}\\text{\\foo{Alice}, \\foo{Bob}}",
|
||||||
|
"\\text{hi Alice, hi Bob}");
|
||||||
|
compareParseTree("\\gdef\\foo#1#2{(#1,#2)}\\foo 1 2+\\foo 3 4",
|
||||||
|
"(1,2)+(3,4)");
|
||||||
|
expect("\\gdef\\foo#2{}").toNotParse();
|
||||||
|
expect("\\gdef\\foo#1#3{}").toNotParse();
|
||||||
|
expect("\\gdef\\foo#1#2#3#4#5#6#7#8#9{}").toParse();
|
||||||
|
expect("\\gdef\\foo#1#2#3#4#5#6#7#8#9#10{}").toNotParse();
|
||||||
|
expect("\\gdef\\foo#{}").toNotParse();
|
||||||
|
expect("\\gdef\\foo\\bar").toParse();
|
||||||
|
expect("\\gdef{\\foo\\bar}{}").toNotParse();
|
||||||
|
expect("\\gdef{}{}").toNotParse();
|
||||||
|
});
|
||||||
|
|
||||||
// This may change in the future, if we support the extra features of
|
// This may change in the future, if we support the extra features of
|
||||||
// \hspace.
|
// \hspace.
|
||||||
it("should treat \\hspace, \\hskip like \\kern", function() {
|
it("should treat \\hspace, \\hskip like \\kern", function() {
|
||||||
@@ -2681,6 +2699,30 @@ describe("A macro expander", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("\\tag support", function() {
|
||||||
|
const displayMode = new Settings({displayMode: true});
|
||||||
|
|
||||||
|
it("should fail outside display mode", () => {
|
||||||
|
expect("\\tag{hi}x+y").toNotParse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with multiple tags", () => {
|
||||||
|
expect("\\tag{1}\\tag{2}x+y").toNotParse(displayMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should build", () => {
|
||||||
|
expect("\\tag{hi}x+y").toBuild(displayMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore location of \\tag", () => {
|
||||||
|
expect("\\tag{hi}x+y").toParseLike("x+y\\tag{hi}", displayMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle \\tag* like \\tag", () => {
|
||||||
|
expect("\\tag{hi}x+y").toParseLike("\\tag*{({hi})}x+y", displayMode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("A parser taking String objects", function() {
|
describe("A parser taking String objects", function() {
|
||||||
it("should not fail on an empty String object", function() {
|
it("should not fail on an empty String object", function() {
|
||||||
expect(new String("")).toParse();
|
expect(new String("")).toParse();
|
||||||
@@ -2862,6 +2904,20 @@ describe("The maxSize setting", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("The maxExpand setting", () => {
|
||||||
|
it("should prevent expansion", () => {
|
||||||
|
expect("\\gdef\\foo{1}\\foo").toParse();
|
||||||
|
expect("\\gdef\\foo{1}\\foo").toParse(new Settings({maxExpand: 2}));
|
||||||
|
expect("\\gdef\\foo{1}\\foo").toNotParse(new Settings({maxExpand: 1}));
|
||||||
|
expect("\\gdef\\foo{1}\\foo").toNotParse(new Settings({maxExpand: 0}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent infinite loops", () => {
|
||||||
|
expect("\\gdef\\foo{\\foo}\\foo").toNotParse(
|
||||||
|
new Settings({maxExpand: 10}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("The \\mathchoice function", function() {
|
describe("The \\mathchoice function", function() {
|
||||||
const cmd = "\\sum_{k = 0}^{\\infty} x^k";
|
const cmd = "\\sum_{k = 0}^{\\infty} x^k";
|
||||||
|
|
||||||
|
@@ -94,8 +94,13 @@ describe("A MathML builder", function() {
|
|||||||
.toMatchSnapshot();
|
.toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accents turn into <mover accent="true"> in MathML', function() {
|
it('accents turn into <mover accent="true"> in MathML', () => {
|
||||||
expect(getMathML("über fiancée", {unicodeTextInMathMode: true}))
|
expect(getMathML("über fiancée", {unicodeTextInMathMode: true}))
|
||||||
.toMatchSnapshot();
|
.toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tags use <mlabeledtr>', () => {
|
||||||
|
expect(getMathML("\\tag{hi} x+y^2", {displayMode: true}))
|
||||||
|
.toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
BIN
test/screenshotter/images/Tag-chrome.png
Normal file
BIN
test/screenshotter/images/Tag-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
BIN
test/screenshotter/images/Tag-firefox.png
Normal file
BIN
test/screenshotter/images/Tag-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
@@ -332,6 +332,9 @@ SvgReset:
|
|||||||
Symbols1: |
|
Symbols1: |
|
||||||
\maltese\degree\pounds\$
|
\maltese\degree\pounds\$
|
||||||
\text{\maltese\degree\pounds\textdollar}
|
\text{\maltese\degree\pounds\textdollar}
|
||||||
|
Tag:
|
||||||
|
tex: \tag{$+$hi} \frac{x^2}{y}+x^{2^y}
|
||||||
|
display: 1
|
||||||
Text: \frac{a}{b}\text{c~ {ab} \ e}+fg
|
Text: \frac{a}{b}\text{c~ {ab} \ e}+fg
|
||||||
TextSpace:
|
TextSpace:
|
||||||
\begin{array}{l}
|
\begin{array}{l}
|
||||||
|
Reference in New Issue
Block a user