feat(macro): improve argument parsing (#2085)
* Improve macro argument parsing
* Make \above a primitive command
* Fix screenshotter data
* Normalize argument where necessary
* Improve argument location info
* Update comments
* Minor refactor
* Modularize group parsers
* Allow braced and blank size argument
for non-strict mode and \genfrac, respectively.
* Minor refactor & update comments
* Remove raw option in parseStringGroup
* Update tests
* Fix { delimited parameter
* Update tests
* Update tests
* Normalize argument in \genfrac
* Update tests
* Consume space before scanning an optional argument
* Fix \\, \newline, and \cr behavior
* Fix flow error
* Update comments
* Remove unnecessary mode switching
Parser mode affects neither fetch nor consume.
* Allow single (active) character macro
* Add function property `primitive`
* Set \mathchoice and \*style primitive
* Separate size-related improvements out to #2139
* Fix flow error
* Update screenshots
* Update demo example
* Add a migration guide
* Fix capitalization
* Make a primitive function unexpandable
* Update screenshots
* Update screenshots
* Revert "Document \def doesn't support delimiters (#2288) (#2289)"
This reverts commit f96fba6f7f
.
* Update comments, errors, and tests
* Update screenshots
24
docs/migration.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
id: migration
|
||||
title: Migration Guide
|
||||
---
|
||||
|
||||
As of KaTeX 1.0, we've changed how MacroExpander and Parser work in order to close
|
||||
some gaps between KaTeX and LaTeX and therefore there may be breaking changes.
|
||||
|
||||
## Macro arguments
|
||||
Tokens will not be expanded while parsing a macro argument. For example, `\frac\foo\foo`,
|
||||
where the `\foo` is defined as `12`, will be parsed as `\frac{12}{12}`, not
|
||||
`\frac{1}{2}12`. <!--To expand the argument before parsing, `\expandafter` can
|
||||
be used` like `\expandafter\frac\foo\foo`.-->
|
||||
|
||||
## `\def`
|
||||
`\def` no longer accepts a control sequence enclosed in braces. For example,
|
||||
`\def{\foo}{}` no longer works and should be changed to `\def\foo{}`.
|
||||
|
||||
It also no longer accepts replacement text not enclosed in braces. For example,
|
||||
`\def\foo1` no longer works and should be changed to `\def\foo{1}`.
|
||||
|
||||
## `\newline` and `\cr`
|
||||
`\newline` and `\cr` no longer takes an optional size argument. To specify vertical
|
||||
spacing, `\\` should be used.
|
@@ -315,7 +315,6 @@ Direct Input: $∀ ∴ ∁ ∵ ∃ ∣ ∈ ∉ ∋ ⊂ ⊃ ∧ ∨ ↦ → ←
|
||||
Macros can also be defined in the KaTeX [rendering options](options.md).
|
||||
|
||||
Macros accept up to nine arguments: #1, #2, etc.
|
||||
Delimiters (such as `\def\add#1+#2{#1\oplus#2}`) are not currently supported.
|
||||
|
||||
`\gdef`, `\xdef`, `\global\def`, `\global\edef`, `\global\let`, and `\global\futurelet` will persist between math expressions.
|
||||
|
||||
|
@@ -13,7 +13,7 @@ import ParseError from "./ParseError";
|
||||
import Namespace from "./Namespace";
|
||||
import builtinMacros from "./macros";
|
||||
|
||||
import type {MacroContextInterface, MacroDefinition, MacroExpansion}
|
||||
import type {MacroContextInterface, MacroDefinition, MacroExpansion, MacroArg}
|
||||
from "./macros";
|
||||
import type Settings from "./Settings";
|
||||
|
||||
@@ -108,6 +108,32 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
this.stack.push(...tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an macro argument without expanding tokens and append the array of
|
||||
* tokens to the token stack. Uses Token as a container for the result.
|
||||
*/
|
||||
scanArgument(isOptional: boolean): ?Token {
|
||||
let start;
|
||||
let end;
|
||||
let tokens;
|
||||
if (isOptional) {
|
||||
this.consumeSpaces(); // \@ifnextchar gobbles any space following it
|
||||
if (this.future().text !== "[") {
|
||||
return null;
|
||||
}
|
||||
start = this.popToken(); // don't include [ in tokens
|
||||
({tokens, end} = this.consumeArg(["]"]));
|
||||
} else {
|
||||
({tokens, start, end} = this.consumeArg());
|
||||
}
|
||||
|
||||
// indicate the end of an argument
|
||||
this.pushToken(new Token("EOF", end.loc));
|
||||
|
||||
this.pushTokens(tokens);
|
||||
return start.range(end, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume all following space tokens, without expansion.
|
||||
*/
|
||||
@@ -123,40 +149,91 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the specified number of arguments from the token stream,
|
||||
* and return the resulting array of arguments.
|
||||
* Consume an argument from the token stream, and return the resulting array
|
||||
* of tokens and start/end token.
|
||||
*/
|
||||
consumeArgs(numArgs: number): Token[][] {
|
||||
const args: Token[][] = [];
|
||||
// obtain arguments, either single token or balanced {…} group
|
||||
for (let i = 0; i < numArgs; ++i) {
|
||||
this.consumeSpaces(); // ignore spaces before each argument
|
||||
const startOfArg = this.popToken();
|
||||
if (startOfArg.text === "{") {
|
||||
const arg: Token[] = [];
|
||||
let depth = 1;
|
||||
while (depth !== 0) {
|
||||
const tok = this.popToken();
|
||||
arg.push(tok);
|
||||
if (tok.text === "{") {
|
||||
++depth;
|
||||
} else if (tok.text === "}") {
|
||||
--depth;
|
||||
} else if (tok.text === "EOF") {
|
||||
throw new ParseError(
|
||||
"End of input in macro argument",
|
||||
startOfArg);
|
||||
}
|
||||
consumeArg(delims?: ?string[]): MacroArg {
|
||||
// The argument for a delimited parameter is the shortest (possibly
|
||||
// empty) sequence of tokens with properly nested {...} groups that is
|
||||
// followed ... by this particular list of non-parameter tokens.
|
||||
// The argument for an undelimited parameter is the next nonblank
|
||||
// token, unless that token is ‘{’, when the argument will be the
|
||||
// entire {...} group that follows.
|
||||
const tokens: Token[] = [];
|
||||
const isDelimited = delims && delims.length > 0;
|
||||
if (!isDelimited) {
|
||||
// Ignore spaces between arguments. As the TeXbook says:
|
||||
// "After you have said ‘\def\row#1#2{...}’, you are allowed to
|
||||
// put spaces between the arguments (e.g., ‘\row x n’), because
|
||||
// TeX doesn’t use single spaces as undelimited arguments."
|
||||
this.consumeSpaces();
|
||||
}
|
||||
const start = this.future();
|
||||
let tok;
|
||||
let depth = 0;
|
||||
let match = 0;
|
||||
do {
|
||||
tok = this.popToken();
|
||||
tokens.push(tok);
|
||||
if (tok.text === "{") {
|
||||
++depth;
|
||||
} else if (tok.text === "}") {
|
||||
--depth;
|
||||
if (depth === -1) {
|
||||
throw new ParseError("Extra }", tok);
|
||||
}
|
||||
arg.pop(); // remove last }
|
||||
arg.reverse(); // like above, to fit in with stack order
|
||||
args[i] = arg;
|
||||
} else if (startOfArg.text === "EOF") {
|
||||
throw new ParseError(
|
||||
"End of input expecting macro argument");
|
||||
} else {
|
||||
args[i] = [startOfArg];
|
||||
} else if (tok.text === "EOF") {
|
||||
throw new ParseError("Unexpected end of input in a macro argument" +
|
||||
", expected '" + (delims && isDelimited ? delims[match] : "}") +
|
||||
"'", tok);
|
||||
}
|
||||
if (delims && isDelimited) {
|
||||
if ((depth === 0 || (depth === 1 && delims[match] === "{")) &&
|
||||
tok.text === delims[match]) {
|
||||
++match;
|
||||
if (match === delims.length) {
|
||||
// don't include delims in tokens
|
||||
tokens.splice(-match, match);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
match = 0;
|
||||
}
|
||||
}
|
||||
} while (depth !== 0 || isDelimited);
|
||||
// If the argument found ... has the form ‘{<nested tokens>}’,
|
||||
// ... the outermost braces enclosing the argument are removed
|
||||
if (start.text === "{" && tokens[tokens.length - 1].text === "}") {
|
||||
tokens.pop();
|
||||
tokens.shift();
|
||||
}
|
||||
tokens.reverse(); // to fit in with stack order
|
||||
return {tokens, start, end: tok};
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the specified number of (delimited) arguments from the token
|
||||
* stream and return the resulting array of arguments.
|
||||
*/
|
||||
consumeArgs(numArgs: number, delimiters?: string[][]): Token[][] {
|
||||
if (delimiters) {
|
||||
if (delimiters.length !== numArgs + 1) {
|
||||
throw new ParseError(
|
||||
"The length of delimiters doesn't match the number of args!");
|
||||
}
|
||||
const delims = delimiters[0];
|
||||
for (let i = 0; i < delims.length; i++) {
|
||||
const tok = this.popToken();
|
||||
if (delims[i] !== tok.text) {
|
||||
throw new ParseError(
|
||||
"Use of the macro doesn't match its definition", tok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const args: Token[][] = [];
|
||||
for (let i = 0; i < numArgs; i++) {
|
||||
args.push(this.consumeArg(delimiters && delimiters[i + 1]).tokens);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
@@ -177,10 +254,6 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
*
|
||||
* Used to implement `expandAfterFuture` and `expandNextToken`.
|
||||
*
|
||||
* At the moment, macro expansion doesn't handle delimited macros,
|
||||
* i.e. things like those defined by \def\foo#1\end{…}.
|
||||
* See the TeX book page 202ff. for details on how those should behave.
|
||||
*
|
||||
* If expandableOnly, only expandable tokens are expanded and
|
||||
* an undefined control sequence results in an error.
|
||||
*/
|
||||
@@ -202,8 +275,8 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
"need to increase maxExpand setting");
|
||||
}
|
||||
let tokens = expansion.tokens;
|
||||
const args = this.consumeArgs(expansion.numArgs, expansion.delimiters);
|
||||
if (expansion.numArgs) {
|
||||
const args = this.consumeArgs(expansion.numArgs);
|
||||
// paste arguments in place of the placeholders
|
||||
tokens = tokens.slice(); // make a shallow copy
|
||||
for (let i = tokens.length - 1; i >= 0; --i) {
|
||||
@@ -368,7 +441,6 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
const macro = this.macros.get(name);
|
||||
return macro != null ? typeof macro === "string"
|
||||
|| typeof macro === "function" || !macro.unexpandable
|
||||
// TODO(ylem): #2085
|
||||
: functions.hasOwnProperty(name)/* && !functions[name].primitive*/;
|
||||
: functions.hasOwnProperty(name) && !functions[name].primitive;
|
||||
}
|
||||
}
|
||||
|
241
src/Parser.js
@@ -145,12 +145,6 @@ export default class Parser {
|
||||
|
||||
static endOfExpression = ["}", "\\endgroup", "\\end", "\\right", "&"];
|
||||
|
||||
static endOfGroup = {
|
||||
"[": "]",
|
||||
"{": "}",
|
||||
"\\begingroup": "\\endgroup",
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an "expression", which is a list of atoms.
|
||||
*
|
||||
@@ -265,9 +259,8 @@ export default class Parser {
|
||||
const symbolToken = this.fetch();
|
||||
const symbol = symbolToken.text;
|
||||
this.consume();
|
||||
const group = this.parseGroup(name, false, Parser.SUPSUB_GREEDINESS,
|
||||
undefined, undefined, true);
|
||||
// ignore spaces before sup/subscript argument
|
||||
this.consumeSpaces(); // ignore spaces before sup/subscript argument
|
||||
const group = this.parseGroup(name, Parser.SUPSUB_GREEDINESS);
|
||||
|
||||
if (!group) {
|
||||
throw new ParseError(
|
||||
@@ -312,7 +305,7 @@ export default class Parser {
|
||||
parseAtom(breakOnTokenText?: BreakToken): ?AnyParseNode {
|
||||
// The body of an atom is an implicit group, so that things like
|
||||
// \left(x\right)^2 work correctly.
|
||||
const base = this.parseGroup("atom", false, null, breakOnTokenText);
|
||||
const base = this.parseGroup("atom", null, breakOnTokenText);
|
||||
|
||||
// In text mode, we don't have superscripts or subscripts
|
||||
if (this.mode === "text") {
|
||||
@@ -480,31 +473,24 @@ export default class Parser {
|
||||
const optArgs = [];
|
||||
|
||||
for (let i = 0; i < totalArgs; i++) {
|
||||
const argType = funcData.argTypes && funcData.argTypes[i];
|
||||
let argType = funcData.argTypes && funcData.argTypes[i];
|
||||
const isOptional = i < funcData.numOptionalArgs;
|
||||
// Ignore spaces between arguments. As the TeXbook says:
|
||||
// "After you have said ‘\def\row#1#2{...}’, you are allowed to
|
||||
// put spaces between the arguments (e.g., ‘\row x n’), because
|
||||
// TeX doesn’t use single spaces as undelimited arguments."
|
||||
const consumeSpaces = (i > 0 && !isOptional) ||
|
||||
// Also consume leading spaces in math mode, as parseSymbol
|
||||
// won't know what to do with them. This can only happen with
|
||||
// macros, e.g. \frac\foo\foo where \foo expands to a space symbol.
|
||||
// In LaTeX, the \foo's get treated as (blank) arguments.
|
||||
// In KaTeX, for now, both spaces will get consumed.
|
||||
// TODO(edemaine)
|
||||
(i === 0 && !isOptional && this.mode === "math");
|
||||
const arg = this.parseGroupOfType(`argument to '${func}'`,
|
||||
argType, isOptional, baseGreediness, consumeSpaces);
|
||||
if (!arg) {
|
||||
if (isOptional) {
|
||||
optArgs.push(null);
|
||||
continue;
|
||||
}
|
||||
throw new ParseError(
|
||||
`Expected group after '${func}'`, this.fetch());
|
||||
|
||||
if ((funcData.primitive && argType == null) ||
|
||||
// \sqrt expands into primitive if optional argument doesn't exist
|
||||
(funcData.type === "sqrt" && i === 1 && optArgs[0] == null)) {
|
||||
argType = "primitive";
|
||||
}
|
||||
|
||||
const arg = this.parseGroupOfType(`argument to '${func}'`,
|
||||
argType, isOptional, baseGreediness);
|
||||
if (isOptional) {
|
||||
optArgs.push(arg);
|
||||
} else if (arg != null) {
|
||||
args.push(arg);
|
||||
} else { // should be unreachable
|
||||
throw new ParseError("Null argument, please report this as a bug");
|
||||
}
|
||||
(isOptional ? optArgs : args).push(arg);
|
||||
}
|
||||
|
||||
return {args, optArgs};
|
||||
@@ -518,64 +504,50 @@ export default class Parser {
|
||||
type: ?ArgType,
|
||||
optional: boolean,
|
||||
greediness: ?number,
|
||||
consumeSpaces: boolean,
|
||||
): ?AnyParseNode {
|
||||
switch (type) {
|
||||
case "color":
|
||||
if (consumeSpaces) {
|
||||
this.consumeSpaces();
|
||||
}
|
||||
return this.parseColorGroup(optional);
|
||||
case "size":
|
||||
if (consumeSpaces) {
|
||||
this.consumeSpaces();
|
||||
}
|
||||
return this.parseSizeGroup(optional);
|
||||
case "url":
|
||||
return this.parseUrlGroup(optional, consumeSpaces);
|
||||
return this.parseUrlGroup(optional);
|
||||
case "math":
|
||||
case "text":
|
||||
return this.parseGroup(
|
||||
name, optional, greediness, undefined, type, consumeSpaces);
|
||||
return this.parseArgumentGroup(optional, type);
|
||||
case "hbox": {
|
||||
// hbox argument type wraps the argument in the equivalent of
|
||||
// \hbox, which is like \text but switching to \textstyle size.
|
||||
const group = this.parseGroup(name, optional, greediness,
|
||||
undefined, "text", consumeSpaces);
|
||||
if (!group) {
|
||||
return group;
|
||||
}
|
||||
const styledGroup = {
|
||||
const group = this.parseArgumentGroup(optional, "text");
|
||||
return group != null ? {
|
||||
type: "styling",
|
||||
mode: group.mode,
|
||||
body: [group],
|
||||
style: "text", // simulate \textstyle
|
||||
};
|
||||
return styledGroup;
|
||||
} : null;
|
||||
}
|
||||
case "raw": {
|
||||
if (consumeSpaces) {
|
||||
this.consumeSpaces();
|
||||
const token = this.parseStringGroup("raw", optional);
|
||||
return token != null ? {
|
||||
type: "raw",
|
||||
mode: "text",
|
||||
string: token.text,
|
||||
} : null;
|
||||
}
|
||||
case "primitive": {
|
||||
if (optional) {
|
||||
throw new ParseError("A primitive argument cannot be optional");
|
||||
}
|
||||
if (optional && this.fetch().text === "{") {
|
||||
return null;
|
||||
}
|
||||
const token = this.parseStringGroup("raw", optional, true);
|
||||
if (token) {
|
||||
return {
|
||||
type: "raw",
|
||||
mode: "text",
|
||||
string: token.text,
|
||||
};
|
||||
} else {
|
||||
throw new ParseError("Expected raw group", this.fetch());
|
||||
const group = this.parseGroup(name, greediness);
|
||||
if (group == null) {
|
||||
throw new ParseError("Expected group as " + name, this.fetch());
|
||||
}
|
||||
return group;
|
||||
}
|
||||
case "original":
|
||||
case null:
|
||||
case undefined:
|
||||
return this.parseGroup(name, optional, greediness,
|
||||
undefined, undefined, consumeSpaces);
|
||||
return this.parseArgumentGroup(optional);
|
||||
default:
|
||||
throw new ParseError(
|
||||
"Unknown group type as " + name, this.fetch());
|
||||
@@ -598,49 +570,20 @@ export default class Parser {
|
||||
parseStringGroup(
|
||||
modeName: ArgType, // Used to describe the mode in error messages.
|
||||
optional: boolean,
|
||||
raw?: boolean,
|
||||
): ?Token {
|
||||
const groupBegin = optional ? "[" : "{";
|
||||
const groupEnd = optional ? "]" : "}";
|
||||
const beginToken = this.fetch();
|
||||
if (beginToken.text !== groupBegin) {
|
||||
if (optional) {
|
||||
return null;
|
||||
} else if (raw && beginToken.text !== "EOF" &&
|
||||
/[^{}[\]]/.test(beginToken.text)) {
|
||||
this.consume();
|
||||
return beginToken;
|
||||
}
|
||||
const argToken = this.gullet.scanArgument(optional);
|
||||
if (argToken == null) {
|
||||
return null;
|
||||
}
|
||||
const outerMode = this.mode;
|
||||
this.mode = "text";
|
||||
this.expect(groupBegin);
|
||||
let str = "";
|
||||
const firstToken = this.fetch();
|
||||
let nested = 0; // allow nested braces in raw string group
|
||||
let lastToken = firstToken;
|
||||
let nextToken;
|
||||
while ((nextToken = this.fetch()).text !== groupEnd ||
|
||||
(raw && nested > 0)) {
|
||||
switch (nextToken.text) {
|
||||
case "EOF":
|
||||
throw new ParseError(
|
||||
"Unexpected end of input in " + modeName,
|
||||
firstToken.range(lastToken, str));
|
||||
case groupBegin:
|
||||
nested++;
|
||||
break;
|
||||
case groupEnd:
|
||||
nested--;
|
||||
break;
|
||||
}
|
||||
lastToken = nextToken;
|
||||
str += lastToken.text;
|
||||
while ((nextToken = this.fetch()).text !== "EOF") {
|
||||
str += nextToken.text;
|
||||
this.consume();
|
||||
}
|
||||
this.expect(groupEnd);
|
||||
this.mode = outerMode;
|
||||
return firstToken.range(lastToken, str);
|
||||
this.consume(); // consume the end of the argument
|
||||
argToken.text = str;
|
||||
return argToken;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -652,8 +595,6 @@ export default class Parser {
|
||||
regex: RegExp,
|
||||
modeName: string, // Used to describe the mode in error messages.
|
||||
): Token {
|
||||
const outerMode = this.mode;
|
||||
this.mode = "text";
|
||||
const firstToken = this.fetch();
|
||||
let lastToken = firstToken;
|
||||
let str = "";
|
||||
@@ -669,7 +610,6 @@ export default class Parser {
|
||||
"Invalid " + modeName + ": '" + firstToken.text + "'",
|
||||
firstToken);
|
||||
}
|
||||
this.mode = outerMode;
|
||||
return firstToken.range(lastToken, str);
|
||||
}
|
||||
|
||||
@@ -678,7 +618,7 @@ export default class Parser {
|
||||
*/
|
||||
parseColorGroup(optional: boolean): ?ParseNode<"color-token"> {
|
||||
const res = this.parseStringGroup("color", optional);
|
||||
if (!res) {
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
const match = (/^(#[a-f0-9]{3}|#?[a-f0-9]{6}|[a-z]+)$/i).exec(res.text);
|
||||
@@ -705,7 +645,9 @@ export default class Parser {
|
||||
parseSizeGroup(optional: boolean): ?ParseNode<"size"> {
|
||||
let res;
|
||||
let isBlank = false;
|
||||
if (!optional && this.fetch().text !== "{") {
|
||||
// don't expand before parseStringGroup
|
||||
this.gullet.consumeSpaces();
|
||||
if (!optional && this.gullet.future().text !== "{") {
|
||||
res = this.parseRegexGroup(
|
||||
/^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/, "size");
|
||||
} else {
|
||||
@@ -744,11 +686,11 @@ export default class Parser {
|
||||
* Parses an URL, checking escaped letters and allowed protocols,
|
||||
* and setting the catcode of % as an active character (as in \hyperref).
|
||||
*/
|
||||
parseUrlGroup(optional: boolean, consumeSpaces: boolean): ?ParseNode<"url"> {
|
||||
parseUrlGroup(optional: boolean): ?ParseNode<"url"> {
|
||||
this.gullet.lexer.setCatcode("%", 13); // active character
|
||||
const res = this.parseStringGroup("url", optional, true); // get raw string
|
||||
const res = this.parseStringGroup("url", optional);
|
||||
this.gullet.lexer.setCatcode("%", 14); // comment character
|
||||
if (!res) {
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
// hyperref package allows backslashes alone in href, but doesn't
|
||||
@@ -764,52 +706,61 @@ export default class Parser {
|
||||
}
|
||||
|
||||
/**
|
||||
* If `optional` is false or absent, this parses an ordinary group,
|
||||
* which is either a single nucleus (like "x") or an expression
|
||||
* in braces (like "{x+y}") or an implicit group, a group that starts
|
||||
* at the current position, and ends right before a higher explicit
|
||||
* Parses an argument with the mode specified.
|
||||
*/
|
||||
parseArgumentGroup(optional: boolean, mode?: Mode): ?ParseNode<"ordgroup"> {
|
||||
const argToken = this.gullet.scanArgument(optional);
|
||||
if (argToken == null) {
|
||||
return null;
|
||||
}
|
||||
const outerMode = this.mode;
|
||||
if (mode) { // Switch to specified mode
|
||||
this.switchMode(mode);
|
||||
}
|
||||
|
||||
this.gullet.beginGroup();
|
||||
const expression = this.parseExpression(false, "EOF");
|
||||
// TODO: find an alternative way to denote the end
|
||||
this.expect("EOF"); // expect the end of the argument
|
||||
this.gullet.endGroup();
|
||||
const result = {
|
||||
type: "ordgroup",
|
||||
mode: this.mode,
|
||||
loc: argToken.loc,
|
||||
body: expression,
|
||||
};
|
||||
|
||||
if (mode) { // Switch mode back
|
||||
this.switchMode(outerMode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an ordinary group, which is either a single nucleus (like "x")
|
||||
* or an expression in braces (like "{x+y}") or an implicit group, a group
|
||||
* that starts at the current position, and ends right before a higher explicit
|
||||
* group ends, or at EOF.
|
||||
* If `optional` is true, it parses either a bracket-delimited expression
|
||||
* (like "[x+y]") or returns null to indicate the absence of a
|
||||
* bracket-enclosed group.
|
||||
* If `mode` is present, switches to that mode while parsing the group,
|
||||
* and switches back after.
|
||||
*/
|
||||
parseGroup(
|
||||
name: string, // For error reporting.
|
||||
optional?: boolean,
|
||||
greediness?: ?number,
|
||||
breakOnTokenText?: BreakToken,
|
||||
mode?: Mode,
|
||||
consumeSpaces?: boolean,
|
||||
): ?AnyParseNode {
|
||||
// Switch to specified mode
|
||||
const outerMode = this.mode;
|
||||
if (mode) {
|
||||
this.switchMode(mode);
|
||||
}
|
||||
// Consume spaces if requested, crucially *after* we switch modes,
|
||||
// so that the next non-space token is parsed in the correct mode.
|
||||
if (consumeSpaces) {
|
||||
this.consumeSpaces();
|
||||
}
|
||||
// Get first token
|
||||
const firstToken = this.fetch();
|
||||
const text = firstToken.text;
|
||||
|
||||
let result;
|
||||
// Try to parse an open brace or \begingroup
|
||||
if (optional ? text === "[" : text === "{" || text === "\\begingroup") {
|
||||
if (text === "{" || text === "\\begingroup") {
|
||||
this.consume();
|
||||
const groupEnd = Parser.endOfGroup[text];
|
||||
// Start a new group namespace
|
||||
const groupEnd = text === "{" ? "}" : "\\endgroup";
|
||||
|
||||
this.gullet.beginGroup();
|
||||
// If we get a brace, parse an expression
|
||||
const expression = this.parseExpression(false, groupEnd);
|
||||
const lastToken = this.fetch();
|
||||
// Check that we got a matching closing brace
|
||||
this.expect(groupEnd);
|
||||
// End group namespace
|
||||
this.expect(groupEnd); // Check that we got a matching closing brace
|
||||
this.gullet.endGroup();
|
||||
result = {
|
||||
type: "ordgroup",
|
||||
@@ -822,9 +773,6 @@ export default class Parser {
|
||||
// use-begingroup-instead-of-bgroup
|
||||
semisimple: text === "\\begingroup" || undefined,
|
||||
};
|
||||
} else if (optional) {
|
||||
// Return nothing for an optional group
|
||||
result = null;
|
||||
} else {
|
||||
// If there exists a function with this name, parse the function.
|
||||
// Otherwise, just return a nucleus
|
||||
@@ -840,11 +788,6 @@ export default class Parser {
|
||||
this.consume();
|
||||
}
|
||||
}
|
||||
|
||||
// Switch mode back
|
||||
if (mode) {
|
||||
this.switchMode(outerMode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@@ -83,6 +83,9 @@ export type FunctionPropSpec = {
|
||||
|
||||
// Must be true if the function is an infix operator.
|
||||
infix?: boolean,
|
||||
|
||||
// Whether or not the function is a TeX primitive.
|
||||
primitive?: boolean,
|
||||
};
|
||||
|
||||
type FunctionDefSpec<NODETYPE: NodeType> = {|
|
||||
@@ -128,6 +131,7 @@ export type FunctionSpec<NODETYPE: NodeType> = {|
|
||||
allowedInMath: boolean,
|
||||
numOptionalArgs: number,
|
||||
infix: boolean,
|
||||
primitive: boolean,
|
||||
|
||||
// FLOW TYPE NOTES: Doing either one of the following two
|
||||
//
|
||||
@@ -186,6 +190,7 @@ export default function defineFunction<NODETYPE: NodeType>({
|
||||
: props.allowedInMath,
|
||||
numOptionalArgs: props.numOptionalArgs || 0,
|
||||
infix: !!props.infix,
|
||||
primitive: !!props.primitive,
|
||||
handler: handler,
|
||||
};
|
||||
for (let i = 0; i < names.length; ++i) {
|
||||
@@ -223,6 +228,10 @@ export function defineFunctionBuilders<NODETYPE: NodeType>({
|
||||
});
|
||||
}
|
||||
|
||||
export const normalizeArgument = function(arg: AnyParseNode): AnyParseNode {
|
||||
return arg.type === "ordgroup" && arg.body.length === 1 ? arg.body[0] : arg;
|
||||
};
|
||||
|
||||
// Since the corresponding buildHTML/buildMathML function expects a
|
||||
// list of elements, we normalize for different kinds of arguments
|
||||
export const ordargument = function(arg: AnyParseNode): AnyParseNode[] {
|
||||
|
@@ -86,12 +86,11 @@ function parseArray(
|
||||
|},
|
||||
style: StyleStr,
|
||||
): ParseNode<"array"> {
|
||||
// Parse body of array with \\ temporarily mapped to \cr
|
||||
parser.gullet.beginGroup();
|
||||
if (singleRow) {
|
||||
parser.gullet.macros.set("\\\\", ""); // {equation} acts this way.
|
||||
} else {
|
||||
parser.gullet.macros.set("\\\\", "\\cr");
|
||||
if (!singleRow) {
|
||||
// \cr is equivalent to \\ without the optional size argument (see below)
|
||||
// TODO: provide helpful error when \cr is used outside array environment
|
||||
parser.gullet.macros.set("\\cr", "\\\\\\relax");
|
||||
}
|
||||
|
||||
// Get current arraystretch if it's not set by the environment
|
||||
@@ -121,7 +120,7 @@ function parseArray(
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
// Parse each cell in its own group (namespace)
|
||||
let cell = parser.parseExpression(false, "\\cr");
|
||||
let cell = parser.parseExpression(false, singleRow ? "\\end" : "\\\\");
|
||||
parser.gullet.endGroup();
|
||||
parser.gullet.beginGroup();
|
||||
|
||||
@@ -165,12 +164,18 @@ function parseArray(
|
||||
hLinesBeforeRow.push([]);
|
||||
}
|
||||
break;
|
||||
} else if (next === "\\cr") {
|
||||
if (singleRow) {
|
||||
throw new ParseError("Misplaced \\cr.", parser.nextToken);
|
||||
} else if (next === "\\\\") {
|
||||
parser.consume();
|
||||
let size;
|
||||
// \def\Let@{\let\\\math@cr}
|
||||
// \def\math@cr{...\math@cr@}
|
||||
// \def\math@cr@{\new@ifnextchar[\math@cr@@{\math@cr@@[\z@]}}
|
||||
// \def\math@cr@@[#1]{...\math@cr@@@...}
|
||||
// \def\math@cr@@@{\cr}
|
||||
if (parser.gullet.future().text !== " ") {
|
||||
size = parser.parseSizeGroup(true);
|
||||
}
|
||||
const cr = assertNodeType(parser.parseFunction(), "cr");
|
||||
rowGaps.push(cr.size);
|
||||
rowGaps.push(size ? size.value : null);
|
||||
|
||||
// check for \hline(s) following the row separator
|
||||
hLinesBeforeRow.push(getHLines(parser));
|
||||
@@ -185,7 +190,7 @@ function parseArray(
|
||||
|
||||
// End cell group
|
||||
parser.gullet.endGroup();
|
||||
// End array group defining \\
|
||||
// End array group defining \cr
|
||||
parser.gullet.endGroup();
|
||||
|
||||
return {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import defineFunction from "../defineFunction";
|
||||
import defineFunction, {normalizeArgument} from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
import utils from "../utils";
|
||||
@@ -218,7 +218,7 @@ defineFunction({
|
||||
numArgs: 1,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
const base = args[0];
|
||||
const base = normalizeArgument(args[0]);
|
||||
|
||||
const isStretchy = !NON_STRETCHY_ACCENT_REGEX.test(context.funcName);
|
||||
const isShifty = !isStretchy ||
|
||||
@@ -250,6 +250,7 @@ defineFunction({
|
||||
numArgs: 1,
|
||||
allowedInText: true,
|
||||
allowedInMath: false,
|
||||
argTypes: ["primitive"],
|
||||
},
|
||||
handler: (context, args) => {
|
||||
const base = args[0];
|
||||
|
@@ -5,17 +5,12 @@ import defineFunction from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
import {calculateSize} from "../units";
|
||||
import ParseError from "../ParseError";
|
||||
import {assertNodeType} from "../parseNode";
|
||||
|
||||
// \\ is a macro mapping to either \cr or \newline. Because they have the
|
||||
// same signature, we implement them as one megafunction, with newRow
|
||||
// indicating whether we're in the \cr case, and newLine indicating whether
|
||||
// to break the line in the \newline case.
|
||||
|
||||
// \DeclareRobustCommand\\{...\@xnewline}
|
||||
defineFunction({
|
||||
type: "cr",
|
||||
names: ["\\cr", "\\newline"],
|
||||
names: ["\\\\"],
|
||||
props: {
|
||||
numArgs: 0,
|
||||
numOptionalArgs: 1,
|
||||
@@ -23,25 +18,16 @@ defineFunction({
|
||||
allowedInText: true,
|
||||
},
|
||||
|
||||
handler({parser, funcName}, args, optArgs) {
|
||||
handler({parser}, args, optArgs) {
|
||||
const size = optArgs[0];
|
||||
const newRow = (funcName === "\\cr");
|
||||
let newLine = false;
|
||||
if (!newRow) {
|
||||
if (parser.settings.displayMode &&
|
||||
parser.settings.useStrictBehavior(
|
||||
"newLineInDisplayMode", "In LaTeX, \\\\ or \\newline " +
|
||||
"does nothing in display mode")) {
|
||||
newLine = false;
|
||||
} else {
|
||||
newLine = true;
|
||||
}
|
||||
}
|
||||
const newLine = !parser.settings.displayMode ||
|
||||
!parser.settings.useStrictBehavior(
|
||||
"newLineInDisplayMode", "In LaTeX, \\\\ or \\newline " +
|
||||
"does nothing in display mode");
|
||||
return {
|
||||
type: "cr",
|
||||
mode: parser.mode,
|
||||
newLine,
|
||||
newRow,
|
||||
size: size && assertNodeType(size, "size").value,
|
||||
};
|
||||
},
|
||||
@@ -50,10 +36,6 @@ defineFunction({
|
||||
// not within tabular/array environments.
|
||||
|
||||
htmlBuilder(group, options) {
|
||||
if (group.newRow) {
|
||||
throw new ParseError(
|
||||
"\\cr valid only within a tabular/array environment");
|
||||
}
|
||||
const span = buildCommon.makeSpan(["mspace"], [], options);
|
||||
if (group.newLine) {
|
||||
span.classes.push("newline");
|
||||
|
@@ -88,41 +88,65 @@ defineFunction({
|
||||
props: {
|
||||
numArgs: 0,
|
||||
allowedInText: true,
|
||||
primitive: true,
|
||||
},
|
||||
handler({parser, funcName}) {
|
||||
let arg = parser.gullet.consumeArgs(1)[0];
|
||||
if (arg.length !== 1) {
|
||||
throw new ParseError("\\gdef's first argument must be a macro name");
|
||||
let tok = parser.gullet.popToken();
|
||||
const name = tok.text;
|
||||
if (/^(?:[\\{}$&#^_]|EOF)$/.test(name)) {
|
||||
throw new ParseError("Expected a control sequence", tok);
|
||||
}
|
||||
const name = arg[0].text;
|
||||
// Count argument specifiers, and check they are in the order #1 #2 ...
|
||||
|
||||
let numArgs = 0;
|
||||
arg = parser.gullet.consumeArgs(1)[0];
|
||||
while (arg.length === 1 && arg[0].text === "#") {
|
||||
arg = parser.gullet.consumeArgs(1)[0];
|
||||
if (arg.length !== 1) {
|
||||
throw new ParseError(
|
||||
`Invalid argument number length "${arg.length}"`);
|
||||
let insert;
|
||||
const delimiters = [[]];
|
||||
// <parameter text> contains no braces
|
||||
while (parser.gullet.future().text !== "{") {
|
||||
tok = parser.gullet.popToken();
|
||||
if (tok.text === "#") {
|
||||
// If the very last character of the <parameter text> is #, so that
|
||||
// this # is immediately followed by {, TeX will behave as if the {
|
||||
// had been inserted at the right end of both the parameter text
|
||||
// and the replacement text.
|
||||
if (parser.gullet.future().text === "{") {
|
||||
insert = parser.gullet.future();
|
||||
delimiters[numArgs].push("{");
|
||||
break;
|
||||
}
|
||||
|
||||
// A parameter, the first appearance of # must be followed by 1,
|
||||
// the next by 2, and so on; up to nine #’s are allowed
|
||||
tok = parser.gullet.popToken();
|
||||
if (!(/^[1-9]$/.test(tok.text))) {
|
||||
throw new ParseError(`Invalid argument number "${tok.text}"`);
|
||||
}
|
||||
if (parseInt(tok.text) !== numArgs + 1) {
|
||||
throw new ParseError(
|
||||
`Argument number "${tok.text}" out of order`);
|
||||
}
|
||||
numArgs++;
|
||||
delimiters.push([]);
|
||||
} else if (tok.text === "EOF") {
|
||||
throw new ParseError("Expected a macro definition");
|
||||
} else {
|
||||
delimiters[numArgs].push(tok.text);
|
||||
}
|
||||
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 = parser.gullet.consumeArgs(1)[0];
|
||||
}
|
||||
// replacement text, enclosed in '{' and '}' and properly nested
|
||||
let {tokens} = parser.gullet.consumeArg();
|
||||
if (insert) {
|
||||
tokens.unshift(insert);
|
||||
}
|
||||
|
||||
if (funcName === "\\edef" || funcName === "\\xdef") {
|
||||
arg = parser.gullet.expandTokens(arg);
|
||||
arg.reverse(); // to fit in with stack order
|
||||
tokens = parser.gullet.expandTokens(tokens);
|
||||
tokens.reverse(); // to fit in with stack order
|
||||
}
|
||||
// Final arg is the expansion of the macro
|
||||
parser.gullet.macros.set(name, {
|
||||
tokens: arg,
|
||||
tokens,
|
||||
numArgs,
|
||||
delimiters,
|
||||
}, funcName === globalMap[funcName]);
|
||||
|
||||
return {
|
||||
@@ -145,6 +169,7 @@ defineFunction({
|
||||
props: {
|
||||
numArgs: 0,
|
||||
allowedInText: true,
|
||||
primitive: true,
|
||||
},
|
||||
handler({parser, funcName}) {
|
||||
const name = checkControlSequence(parser.gullet.popToken());
|
||||
@@ -168,6 +193,7 @@ defineFunction({
|
||||
props: {
|
||||
numArgs: 0,
|
||||
allowedInText: true,
|
||||
primitive: true,
|
||||
},
|
||||
handler({parser, funcName}) {
|
||||
const name = checkControlSequence(parser.gullet.popToken());
|
||||
|
@@ -81,6 +81,7 @@ defineFunction({
|
||||
],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
argTypes: ["primitive"],
|
||||
},
|
||||
handler: (context, args) => {
|
||||
const delim = checkDelimiter(args[0], context);
|
||||
@@ -145,6 +146,7 @@ defineFunction({
|
||||
names: ["\\right"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
primitive: true,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
// \left case below triggers parsing of \right in
|
||||
@@ -170,6 +172,7 @@ defineFunction({
|
||||
names: ["\\left"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
primitive: true,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
const delim = checkDelimiter(args[0], context);
|
||||
@@ -303,6 +306,7 @@ defineFunction({
|
||||
names: ["\\middle"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
primitive: true,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
const delim = checkDelimiter(args[0], context);
|
||||
|
@@ -2,7 +2,7 @@
|
||||
// TODO(kevinb): implement \\sl and \\sc
|
||||
|
||||
import {binrelClass} from "./mclass";
|
||||
import defineFunction from "../defineFunction";
|
||||
import defineFunction, {normalizeArgument} from "../defineFunction";
|
||||
import utils from "../utils";
|
||||
|
||||
import * as html from "../buildHTML";
|
||||
@@ -47,7 +47,7 @@ defineFunction({
|
||||
greediness: 2,
|
||||
},
|
||||
handler: ({parser, funcName}, args) => {
|
||||
const body = args[0];
|
||||
const body = normalizeArgument(args[0]);
|
||||
let func = funcName;
|
||||
if (func in fontAliases) {
|
||||
func = fontAliases[func];
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import defineFunction from "../defineFunction";
|
||||
import defineFunction, {normalizeArgument} from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import delimiter from "../delimiter";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
@@ -382,10 +382,12 @@ defineFunction({
|
||||
const denom = args[5];
|
||||
|
||||
// Look into the parse nodes to get the desired delimiters.
|
||||
const leftDelim = args[0].type === "atom" && args[0].family === "open"
|
||||
? delimFromValue(args[0].text) : null;
|
||||
const rightDelim = args[1].type === "atom" && args[1].family === "close"
|
||||
? delimFromValue(args[1].text) : null;
|
||||
const leftNode = normalizeArgument(args[0]);
|
||||
const leftDelim = leftNode.type === "atom" && leftNode.family === "open"
|
||||
? delimFromValue(leftNode.text) : null;
|
||||
const rightNode = normalizeArgument(args[1]);
|
||||
const rightDelim = rightNode.type === "atom" && rightNode.family === "close"
|
||||
? delimFromValue(rightNode.text) : null;
|
||||
|
||||
const barNode = assertNodeType(args[2], "size");
|
||||
let hasBarLine;
|
||||
|
@@ -15,6 +15,7 @@ defineFunction({
|
||||
props: {
|
||||
numArgs: 1,
|
||||
argTypes: ["size"],
|
||||
primitive: true,
|
||||
allowedInText: true,
|
||||
},
|
||||
handler({parser, funcName}, args) {
|
||||
|
@@ -23,6 +23,7 @@ defineFunction({
|
||||
names: ["\\mathchoice"],
|
||||
props: {
|
||||
numArgs: 4,
|
||||
primitive: true,
|
||||
},
|
||||
handler: ({parser}, args) => {
|
||||
return {
|
||||
|
@@ -65,6 +65,7 @@ defineFunction({
|
||||
],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
primitive: true,
|
||||
},
|
||||
handler({parser, funcName}, args) {
|
||||
const body = args[0];
|
||||
@@ -106,7 +107,7 @@ defineFunction({
|
||||
type: "mclass",
|
||||
mode: parser.mode,
|
||||
mclass: binrelClass(args[0]),
|
||||
body: [args[1]],
|
||||
body: ordargument(args[1]),
|
||||
isCharacterBox: utils.isCharacterBox(args[1]),
|
||||
};
|
||||
},
|
||||
|
@@ -225,6 +225,7 @@ defineFunction({
|
||||
names: ["\\mathop"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
primitive: true,
|
||||
},
|
||||
handler: ({parser}, args) => {
|
||||
const body = args[0];
|
||||
|
@@ -22,6 +22,7 @@ defineFunction({
|
||||
props: {
|
||||
numArgs: 0,
|
||||
allowedInText: true,
|
||||
primitive: true,
|
||||
},
|
||||
handler({breakOnTokenText, funcName, parser}, args) {
|
||||
// parse out the implicit body
|
||||
|
@@ -71,6 +71,12 @@ export interface MacroContextInterface {
|
||||
*/
|
||||
expandMacroAsText(name: string): string | void;
|
||||
|
||||
/**
|
||||
* Consume an argument from the token stream, and return the resulting array
|
||||
* of tokens and start/end token.
|
||||
*/
|
||||
consumeArg(delims?: ?string[]): MacroArg;
|
||||
|
||||
/**
|
||||
* Consume the specified number of arguments from the token stream,
|
||||
* and return the resulting array of arguments.
|
||||
@@ -91,10 +97,17 @@ export interface MacroContextInterface {
|
||||
isExpandable(name: string): boolean;
|
||||
}
|
||||
|
||||
export type MacroArg = {
|
||||
tokens: Token[],
|
||||
start: Token,
|
||||
end: Token
|
||||
};
|
||||
|
||||
/** Macro tokens (in reverse order). */
|
||||
export type MacroExpansion = {
|
||||
tokens: Token[],
|
||||
numArgs: number,
|
||||
delimiters?: string[][],
|
||||
unexpandable?: boolean, // used in \let
|
||||
};
|
||||
|
||||
@@ -240,7 +253,7 @@ defineMacro("\\char", function(context) {
|
||||
// \renewcommand{\macro}[args]{definition}
|
||||
// TODO: Optional arguments: \newcommand{\macro}[args][default]{definition}
|
||||
const newcommand = (context, existsOK: boolean, nonexistsOK: boolean) => {
|
||||
let arg = context.consumeArgs(1)[0];
|
||||
let arg = context.consumeArg().tokens;
|
||||
if (arg.length !== 1) {
|
||||
throw new ParseError(
|
||||
"\\newcommand's first argument must be a macro name");
|
||||
@@ -258,7 +271,7 @@ const newcommand = (context, existsOK: boolean, nonexistsOK: boolean) => {
|
||||
}
|
||||
|
||||
let numArgs = 0;
|
||||
arg = context.consumeArgs(1)[0];
|
||||
arg = context.consumeArg().tokens;
|
||||
if (arg.length === 1 && arg[0].text === "[") {
|
||||
let argText = '';
|
||||
let token = context.expandNextToken();
|
||||
@@ -271,7 +284,7 @@ const newcommand = (context, existsOK: boolean, nonexistsOK: boolean) => {
|
||||
throw new ParseError(`Invalid number of arguments: ${argText}`);
|
||||
}
|
||||
numArgs = parseInt(argText);
|
||||
arg = context.consumeArgs(1)[0];
|
||||
arg = context.consumeArg().tokens;
|
||||
}
|
||||
|
||||
// Final arg is the expansion of the macro
|
||||
@@ -696,8 +709,10 @@ defineMacro("\\pmb", "\\html@mathml{" +
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// LaTeX source2e
|
||||
|
||||
// \\ defaults to \newline, but changes to \cr within array environment
|
||||
defineMacro("\\\\", "\\newline");
|
||||
// \expandafter\let\expandafter\@normalcr
|
||||
// \csname\expandafter\@gobble\string\\ \endcsname
|
||||
// \DeclareRobustCommand\newline{\@normalcr\relax}
|
||||
defineMacro("\\newline", "\\\\\\relax");
|
||||
|
||||
// \def\TeX{T\kern-.1667em\lower.5ex\hbox{E}\kern-.125emX\@}
|
||||
// TODO: Doesn't normally work in math mode because \@ fails. KaTeX doesn't
|
||||
|
@@ -209,7 +209,6 @@ type ParseNodeTypes = {
|
||||
type: "cr",
|
||||
mode: Mode,
|
||||
loc?: ?SourceLocation,
|
||||
newRow: boolean,
|
||||
newLine: boolean,
|
||||
size: ?Measurement,
|
||||
|},
|
||||
|
@@ -21,13 +21,14 @@ export type Mode = "math" | "text";
|
||||
// argument is parsed normally)
|
||||
// - Mode: Node group parsed in given mode.
|
||||
export type ArgType = "color" | "size" | "url" | "raw" | "original" | "hbox" |
|
||||
Mode;
|
||||
"primitive" | Mode;
|
||||
|
||||
// LaTeX display style.
|
||||
export type StyleStr = "text" | "display" | "script" | "scriptscript";
|
||||
|
||||
// Allowable token text for "break" arguments in parser.
|
||||
export type BreakToken = "]" | "}" | "\\endgroup" | "$" | "\\)" | "\\cr";
|
||||
export type BreakToken = "]" | "}" | "\\endgroup" | "$" | "\\)" | "\\\\" | "\\end" |
|
||||
"EOF";
|
||||
|
||||
// Math font variants.
|
||||
export type FontVariant = "bold" | "bold-italic" | "bold-sans-serif" |
|
||||
|
@@ -94,11 +94,12 @@ describe("Parser:", function() {
|
||||
describe("#parseArguments", function() {
|
||||
it("complains about missing argument at end of input", function() {
|
||||
expect`2\sqrt`.toFailWithParseError(
|
||||
"Expected group after '\\sqrt' at end of input: 2\\sqrt");
|
||||
"Expected group as argument to '\\sqrt'" +
|
||||
" at end of input: 2\\sqrt");
|
||||
});
|
||||
it("complains about missing argument at end of group", function() {
|
||||
expect`1^{2\sqrt}`.toFailWithParseError(
|
||||
"Expected group after '\\sqrt'" +
|
||||
"Expected group as argument to '\\sqrt'" +
|
||||
" at position 10: 1^{2\\sqrt}̲");
|
||||
});
|
||||
it("complains about functions as arguments to others", function() {
|
||||
@@ -166,7 +167,7 @@ describe("Parser.expect calls:", function() {
|
||||
describe("#parseSpecialGroup expecting braces", function() {
|
||||
it("complains about missing { for color", function() {
|
||||
expect`\textcolor#ffffff{text}`.toFailWithParseError(
|
||||
"Expected '{', got '#' at position 11:" +
|
||||
"Invalid color: '#' at position 11:" +
|
||||
" \\textcolor#̲ffffff{text}");
|
||||
});
|
||||
it("complains about missing { for size", function() {
|
||||
@@ -176,23 +177,23 @@ describe("Parser.expect calls:", function() {
|
||||
// Can't test for the [ of an optional group since it's optional
|
||||
it("complains about missing } for color", function() {
|
||||
expect`\textcolor{#ffffff{text}`.toFailWithParseError(
|
||||
"Invalid color: '#ffffff{text' at position 12:" +
|
||||
" \\textcolor{#̲f̲f̲f̲f̲f̲f̲{̲t̲e̲x̲t̲}");
|
||||
"Unexpected end of input in a macro argument," +
|
||||
" expected '}' at end of input: …r{#ffffff{text}");
|
||||
});
|
||||
it("complains about missing ] for size", function() {
|
||||
expect`\rule[1em{2em}{3em}`.toFailWithParseError(
|
||||
"Unexpected end of input in size" +
|
||||
" at position 7: \\rule[1̲e̲m̲{̲2̲e̲m̲}̲{̲3̲e̲m̲}̲");
|
||||
"Unexpected end of input in a macro argument," +
|
||||
" expected ']' at end of input: …e[1em{2em}{3em}");
|
||||
});
|
||||
it("complains about missing ] for size at end of input", function() {
|
||||
expect`\rule[1em`.toFailWithParseError(
|
||||
"Unexpected end of input in size" +
|
||||
" at position 7: \\rule[1̲e̲m̲");
|
||||
"Unexpected end of input in a macro argument," +
|
||||
" expected ']' at end of input: \\rule[1em");
|
||||
});
|
||||
it("complains about missing } for color at end of input", function() {
|
||||
expect`\textcolor{#123456`.toFailWithParseError(
|
||||
"Unexpected end of input in color" +
|
||||
" at position 12: \\textcolor{#̲1̲2̲3̲4̲5̲6̲");
|
||||
"Unexpected end of input in a macro argument," +
|
||||
" expected '}' at end of input: …xtcolor{#123456");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -206,11 +207,13 @@ describe("Parser.expect calls:", function() {
|
||||
describe("#parseOptionalGroup expecting ]", function() {
|
||||
it("at end of file", function() {
|
||||
expect`\sqrt[3`.toFailWithParseError(
|
||||
"Expected ']', got 'EOF' at end of input: \\sqrt[3");
|
||||
"Unexpected end of input in a macro argument," +
|
||||
" expected ']' at end of input: \\sqrt[3");
|
||||
});
|
||||
it("before group", function() {
|
||||
expect`\sqrt[3{2}`.toFailWithParseError(
|
||||
"Expected ']', got 'EOF' at end of input: \\sqrt[3{2}");
|
||||
"Unexpected end of input in a macro argument," +
|
||||
" expected ']' at end of input: \\sqrt[3{2}");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -269,7 +272,7 @@ describe("functions.js:", function() {
|
||||
describe("\\begin and \\end", function() {
|
||||
it("reject invalid environment names", function() {
|
||||
expect`\begin x\end y`.toFailWithParseError(
|
||||
"Invalid environment name at position 8: \\begin x̲\\end y");
|
||||
"No such environment: x at position 8: \\begin x̲\\end y");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -293,22 +296,22 @@ describe("Lexer:", function() {
|
||||
it("reject 3-digit hex notation without #", function() {
|
||||
expect`\textcolor{1a2}{foo}`.toFailWithParseError(
|
||||
"Invalid color: '1a2'" +
|
||||
" at position 12: \\textcolor{1̲a̲2̲}{foo}");
|
||||
" at position 11: \\textcolor{̲1̲a̲2̲}̲{foo}");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#_innerLexSize", function() {
|
||||
it("reject size without unit", function() {
|
||||
expect`\rule{0}{2em}`.toFailWithParseError(
|
||||
"Invalid size: '0' at position 7: \\rule{0̲}{2em}");
|
||||
"Invalid size: '0' at position 6: \\rule{̲0̲}̲{2em}");
|
||||
});
|
||||
it("reject size with bogus unit", function() {
|
||||
expect`\rule{1au}{2em}`.toFailWithParseError(
|
||||
"Invalid unit: 'au' at position 7: \\rule{1̲a̲u̲}{2em}");
|
||||
"Invalid unit: 'au' at position 6: \\rule{̲1̲a̲u̲}̲{2em}");
|
||||
});
|
||||
it("reject size without number", function() {
|
||||
expect`\rule{em}{2em}`.toFailWithParseError(
|
||||
"Invalid size: 'em' at position 7: \\rule{e̲m̲}{2em}");
|
||||
"Invalid size: 'em' at position 6: \\rule{̲e̲m̲}̲{2em}");
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1282,8 +1282,13 @@ describe("A begin/end parser", function() {
|
||||
expect(m2).toParse();
|
||||
});
|
||||
|
||||
it("should allow \\cr as a line terminator", function() {
|
||||
it("should allow \\cr and \\\\ as a line terminator", function() {
|
||||
expect`\begin{matrix}a&b\cr c&d\end{matrix}`.toParse();
|
||||
expect`\begin{matrix}a&b\\c&d\end{matrix}`.toParse();
|
||||
});
|
||||
|
||||
it("should not allow \\cr to scan for an optional size argument", function() {
|
||||
expect`\begin{matrix}a&b\cr[c]&d\end{matrix}`.toParse();
|
||||
});
|
||||
|
||||
it("should eat a final newline", function() {
|
||||
@@ -1318,6 +1323,16 @@ describe("A sqrt parser", function() {
|
||||
it("should build sized square roots", function() {
|
||||
expect("\\Large\\sqrt[3]{x}").toBuild();
|
||||
});
|
||||
|
||||
it("should expand argument if optional argument doesn't exist", function() {
|
||||
expect("\\sqrt\\foo").toParseLike("\\sqrt123",
|
||||
new Settings({macros: {"\\foo": "123"}}));
|
||||
});
|
||||
|
||||
it("should not expand argument if optional argument exists", function() {
|
||||
expect("\\sqrt[2]\\foo").toParseLike("\\sqrt[2]{123}",
|
||||
new Settings({macros: {"\\foo": "123"}}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("A TeX-compliant parser", function() {
|
||||
@@ -2576,23 +2591,6 @@ describe("A smash builder", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("A document fragment", function() {
|
||||
it("should have paddings applied inside an extensible arrow", function() {
|
||||
const markup = katex.renderToString("\\tiny\\xrightarrow\\textcolor{red}{x}");
|
||||
expect(markup).toContain("x-arrow-pad");
|
||||
});
|
||||
|
||||
it("should have paddings applied inside an enclose", function() {
|
||||
const markup = katex.renderToString(r`\fbox\textcolor{red}{x}`);
|
||||
expect(markup).toContain("boxpad");
|
||||
});
|
||||
|
||||
it("should have paddings applied inside a square root", function() {
|
||||
const markup = katex.renderToString(r`\sqrt\textcolor{red}{x}`);
|
||||
expect(markup).toContain("padding-left");
|
||||
});
|
||||
});
|
||||
|
||||
describe("A parser error", function() {
|
||||
it("should report the position of an error", function() {
|
||||
try {
|
||||
@@ -2884,11 +2882,6 @@ describe("href and url commands", function() {
|
||||
});
|
||||
|
||||
describe("A raw text parser", function() {
|
||||
it("should not not parse a mal-formed string", function() {
|
||||
// In the next line, the first character passed to \includegraphics is a
|
||||
// Unicode combining character. So this is a test that the parser will catch a bad string.
|
||||
expect("\\includegraphics[\u030aheight=0.8em, totalheight=0.9em, width=0.9em]{" + "https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}").not.toParse();
|
||||
});
|
||||
it("should return null for a omitted optional string", function() {
|
||||
expect("\\includegraphics{https://cdn.kastatic.org/images/apple-touch-icon-57x57-precomposed.new.png}").toParse();
|
||||
});
|
||||
@@ -3039,12 +3032,17 @@ describe("A macro expander", function() {
|
||||
});
|
||||
|
||||
it("should allow for macro argument", function() {
|
||||
expect`\foo\bar`.toParseLike("(x)", new Settings({macros: {
|
||||
expect`\foo\bar`.toParseLike("(xyz)", new Settings({macros: {
|
||||
"\\foo": "(#1)",
|
||||
"\\bar": "x",
|
||||
"\\bar": "xyz",
|
||||
}}));
|
||||
});
|
||||
|
||||
it("should allow properly nested group for macro argument", function() {
|
||||
expect`\foo{e^{x_{12}+3}}`.toParseLike("(e^{x_{12}+3})",
|
||||
new Settings({macros: {"\\foo": "(#1)"}}));
|
||||
});
|
||||
|
||||
it("should delay expansion if preceded by \\expandafter", function() {
|
||||
expect`\expandafter\foo\bar`.toParseLike("x+y", new Settings({macros: {
|
||||
"\\foo": "#1+#2",
|
||||
@@ -3064,9 +3062,8 @@ describe("A macro expander", function() {
|
||||
new Settings({macros: {"\\foo": "x"}}));
|
||||
// \frac is a macro and therefore expandable
|
||||
expect`\noexpand\frac xy`.toParseLike`xy`;
|
||||
// TODO(ylem): #2085
|
||||
// \def is not expandable, so is not affected by \noexpand
|
||||
// expect`\noexpand\def\foo{xy}\foo`.toParseLike`xy`;
|
||||
expect`\noexpand\def\foo{xy}\foo`.toParseLike`xy`;
|
||||
});
|
||||
|
||||
it("should allow for space macro argument (text version)", function() {
|
||||
@@ -3104,15 +3101,11 @@ describe("A macro expander", function() {
|
||||
}}));
|
||||
});
|
||||
|
||||
// TODO: The following is not currently possible to get working, given that
|
||||
// functions and macros are dealt with separately.
|
||||
/*
|
||||
it("should allow for space function arguments", function() {
|
||||
expect`\frac\bar\bar`.toParseLike(r`\frac{}{}`, new Settings({macros: {
|
||||
"\\bar": " ",
|
||||
}}));
|
||||
});
|
||||
*/
|
||||
|
||||
it("should build \\overset and \\underset", function() {
|
||||
expect`\overset{f}{\rightarrow} Y`.toBuild();
|
||||
@@ -3222,32 +3215,36 @@ describe("A macro expander", function() {
|
||||
expect`\varsubsetneqq\varsupsetneq\varsupsetneqq`.toBuild();
|
||||
});
|
||||
|
||||
// TODO(edemaine): This doesn't work yet. Parses like `\text text`,
|
||||
// which doesn't treat all four letters as an argument.
|
||||
//it("\\TextOrMath should work in a macro passed to \\text", function() {
|
||||
// expect`\text\mode`.toParseLike(r`\text{text}`, new Settings({macros:
|
||||
// {"\\mode": "\\TextOrMath{text}{math}"}});
|
||||
//});
|
||||
it("\\TextOrMath should work in a macro passed to \\text", function() {
|
||||
expect`\text\mode`.toParseLike(r`\text{text}`, new Settings({macros:
|
||||
{"\\mode": "\\TextOrMath{text}{math}"}}));
|
||||
});
|
||||
|
||||
it("\\gdef defines macros", function() {
|
||||
expect`\gdef\foo{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
|
||||
expect`\gdef{\foo}{x^2}\foo+\foo`.toParseLike`x^2+x^2`;
|
||||
expect`\gdef\foo{hi}\foo+\text{\foo}`.toParseLike`hi+\text{hi}`;
|
||||
expect`\gdef\foo{hi}\foo+\text\foo`.toParseLike`hi+\text{hi}`;
|
||||
expect`\gdef\foo#1{hi #1}\text{\foo{Alice}, \foo{Bob}}`
|
||||
.toParseLike`\text{hi Alice, hi Bob}`;
|
||||
expect`\gdef\foo#1#2{(#1,#2)}\foo 1 2+\foo 3 4`.toParseLike`(1,2)+(3,4)`;
|
||||
expect`\gdef\foo#2{}`.not.toParse();
|
||||
expect`\gdef\foo#a{}`.not.toParse();
|
||||
expect`\gdef\foo#1#3{}`.not.toParse();
|
||||
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{}`.not.toParse();
|
||||
expect`\gdef\foo#{}`.not.toParse();
|
||||
expect`\gdef\foo\bar`.toParse();
|
||||
expect`\gdef\foo1`.not.toParse();
|
||||
expect`\gdef{\foo}{}`.not.toParse();
|
||||
expect`\gdef\foo\bar`.not.toParse();
|
||||
expect`\gdef{\foo\bar}{}`.not.toParse();
|
||||
expect`\gdef{}{}`.not.toParse();
|
||||
// TODO: These shouldn't work, but `1` and `{1}` are currently treated
|
||||
// the same, as are `\foo` and `{\foo}`.
|
||||
//expect`\gdef\foo1`.not.toParse();
|
||||
//expect`\gdef{\foo}{}`.not.toParse();
|
||||
});
|
||||
|
||||
it("\\gdef defines macros with delimited parameter", function() {
|
||||
expect`\gdef\foo|#1||{#1}\text{\foo| x y ||}`.toParseLike`\text{ x y }`;
|
||||
expect`\gdef\foo#1|#2{#1+#2}\foo 1 2 |34`.toParseLike`12+34`;
|
||||
expect`\gdef\foo#1#{#1}\foo1^{23}`.toParseLike`1^{23}`;
|
||||
expect`\gdef\foo|{}\foo`.not.toParse();
|
||||
expect`\gdef\foo#1|{#1}\foo1`.not.toParse();
|
||||
expect`\gdef\foo#1|{#1}\foo1}|`.not.toParse();
|
||||
});
|
||||
|
||||
it("\\xdef should expand definition", function() {
|
||||
@@ -3344,7 +3341,7 @@ describe("A macro expander", function() {
|
||||
expect`\def\foo{1}\let\bar\foo\def\foo{2}\bar`.toParseLike`1`;
|
||||
expect`\let\foo=\kern\edef\bar{\foo1em}\let\kern=\relax\bar`.toParseLike`\kern1em`;
|
||||
// \foo = { (left brace)
|
||||
expect`\let\foo{\frac\foo1}{2}`.toParseLike`\frac{1}{2}`;
|
||||
expect`\let\foo{\sqrt\foo1}`.toParseLike`\sqrt{1}`;
|
||||
// \equals = = (equal sign)
|
||||
expect`\let\equals==a\equals b`.toParseLike`a=b`;
|
||||
// \foo should not be expandable and not affected by \noexpand or \edef
|
||||
@@ -3534,9 +3531,9 @@ describe("\\@binrel automatic bin/rel/ord", () => {
|
||||
expect("L\\@binrel+xR").toParseLike("L\\mathbin xR");
|
||||
expect("L\\@binrel=xR").toParseLike("L\\mathrel xR");
|
||||
expect("L\\@binrel xxR").toParseLike("L\\mathord xR");
|
||||
expect("L\\@binrel{+}{x}R").toParseLike("L\\mathbin{{x}}R");
|
||||
expect("L\\@binrel{=}{x}R").toParseLike("L\\mathrel{{x}}R");
|
||||
expect("L\\@binrel{x}{x}R").toParseLike("L\\mathord{{x}}R");
|
||||
expect("L\\@binrel{+}{x}R").toParseLike("L\\mathbin{x}R");
|
||||
expect("L\\@binrel{=}{x}R").toParseLike("L\\mathrel{x}R");
|
||||
expect("L\\@binrel{x}{x}R").toParseLike("L\\mathord{x}R");
|
||||
});
|
||||
|
||||
it("should base on just first character in group", () => {
|
||||
@@ -3772,21 +3769,18 @@ describe("The \\mathchoice function", function() {
|
||||
});
|
||||
|
||||
describe("Newlines via \\\\ and \\newline", function() {
|
||||
it("should build \\\\ and \\newline the same", () => {
|
||||
it("should build \\\\ without the optional argument and \\newline the same", () => {
|
||||
expect`hello \\ world`.toBuildLike`hello \newline world`;
|
||||
expect`hello \\[1ex] world`.toBuildLike(
|
||||
"hello \\newline[1ex] world");
|
||||
});
|
||||
|
||||
it("should not allow \\newline to scan for an optional size argument", () => {
|
||||
expect`hello \newline[w]orld`.toBuild();
|
||||
});
|
||||
|
||||
it("should not allow \\cr at top level", () => {
|
||||
expect`hello \cr world`.not.toBuild();
|
||||
});
|
||||
|
||||
it("array redefines and resets \\\\", () => {
|
||||
expect`a\\b\begin{matrix}x&y\\z&w\end{matrix}\\c`
|
||||
.toParseLike`a\newline b\begin{matrix}x&y\cr z&w\end{matrix}\newline c`;
|
||||
});
|
||||
|
||||
it("\\\\ causes newline, even after mrel and mop", () => {
|
||||
const markup = katex.renderToString(r`M = \\ a + \\ b \\ c`);
|
||||
// Ensure newlines appear outside base spans (because, in this regexp,
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
@@ -62,9 +62,9 @@
|
||||
</a>
|
||||
</div>
|
||||
<textarea id="demo-input" spellcheck="false">
|
||||
% \f is defined as f(#1) using the macro
|
||||
\f{x} = \int_{-\infty}^\infty
|
||||
\hat \f\xi\,e^{2 \pi i \xi x}
|
||||
% \f is defined as #1f(#2) using the macro
|
||||
\f\relax{x} = \int_{-\infty}^\infty
|
||||
\f\hat\xi,e^{2 \pi i \xi x}
|
||||
\,d\xi</textarea>
|
||||
</div>
|
||||
<div class="demo-right">
|
||||
@@ -181,7 +181,7 @@
|
||||
<h4><label for="macros">macros</label></h4>
|
||||
<textarea id="macros" placeholder="JSON">
|
||||
{
|
||||
"\\f": "f(#1)"
|
||||
"\\f": "#1f(#2)"
|
||||
}</textarea>
|
||||
|
||||
<h3>Editor Options</h3>
|
||||
|
@@ -3,6 +3,6 @@
|
||||
"Installation": ["node", "browser"],
|
||||
"Usage": ["api", "cli", "autorender", "libs"],
|
||||
"Configuring KaTeX": ["options", "security", "error", "font"],
|
||||
"Misc": ["supported", "support_table", "issues"]
|
||||
"Misc": ["supported", "support_table", "issues", "migration"]
|
||||
}
|
||||
}
|
||||
|