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:
Erik Demaine
2018-05-19 16:19:21 -04:00
committed by Kevin Barabash
parent 99b2afa935
commit a0ddad338e
19 changed files with 370 additions and 107 deletions

View File

@@ -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

View File

@@ -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",

View File

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

View File

@@ -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<*>[],

View File

@@ -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) {

View File

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

View File

@@ -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;

View File

@@ -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(

View File

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

View File

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

View File

@@ -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

View File

@@ -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;

View File

@@ -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>
`;

View File

@@ -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

View File

@@ -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";

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -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}