mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-08 12:38:39 +00:00
Refactor Parser (#1723)
* Move unsupported command (undefined control sequence) error to parseSymbol * Change parseGivenFunction and parseFunction to parse only function * Move \begin handling to environment.js * Remove ParsedFunc/Arg, move logics into parseGroup * Fix flow error * Remove parseFunction, rename parseGivenFunction to parseFunction * Minor fixes * Remove previously resolved TODO * Minor fixes * Update flow typing
This commit is contained in:
322
src/Parser.js
322
src/Parser.js
@@ -1,21 +1,20 @@
|
|||||||
// @flow
|
// @flow
|
||||||
/* eslint no-constant-condition:0 */
|
/* eslint no-constant-condition:0 */
|
||||||
import functions from "./functions";
|
import functions from "./functions";
|
||||||
import environments from "./environments";
|
import MacroExpander, {implicitCommands} from "./MacroExpander";
|
||||||
import MacroExpander from "./MacroExpander";
|
|
||||||
import symbols, {ATOMS, extraLatin} from "./symbols";
|
import symbols, {ATOMS, extraLatin} from "./symbols";
|
||||||
import {validUnit} from "./units";
|
import {validUnit} from "./units";
|
||||||
import {supportedCodepoint} from "./unicodeScripts";
|
import {supportedCodepoint} from "./unicodeScripts";
|
||||||
import unicodeAccents from "./unicodeAccents";
|
import unicodeAccents from "./unicodeAccents";
|
||||||
import unicodeSymbols from "./unicodeSymbols";
|
import unicodeSymbols from "./unicodeSymbols";
|
||||||
import utils from "./utils";
|
import utils from "./utils";
|
||||||
import {assertNodeType, checkNodeType} from "./parseNode";
|
import {checkNodeType} from "./parseNode";
|
||||||
import ParseError from "./ParseError";
|
import ParseError from "./ParseError";
|
||||||
import {combiningDiacriticalMarksEndRegex} from "./Lexer";
|
import {combiningDiacriticalMarksEndRegex} from "./Lexer";
|
||||||
import Settings from "./Settings";
|
import Settings from "./Settings";
|
||||||
import SourceLocation from "./SourceLocation";
|
import SourceLocation from "./SourceLocation";
|
||||||
import {Token} from "./Token";
|
import {Token} from "./Token";
|
||||||
import type {AnyParseNode, SymbolParseNode} from "./parseNode";
|
import type {ParseNode, AnyParseNode, SymbolParseNode} from "./parseNode";
|
||||||
import type {Atom, Group} from "./symbols";
|
import type {Atom, Group} from "./symbols";
|
||||||
import type {Mode, ArgType, BreakToken} from "./types";
|
import type {Mode, ArgType, BreakToken} from "./types";
|
||||||
import type {FunctionContext, FunctionSpec} from "./defineFunction";
|
import type {FunctionContext, FunctionSpec} from "./defineFunction";
|
||||||
@@ -48,33 +47,9 @@ import type {EnvSpec} from "./defineEnvironment";
|
|||||||
* There are also extra `.handle...` functions, which pull out some reused
|
* There are also extra `.handle...` functions, which pull out some reused
|
||||||
* functionality into self-contained functions.
|
* functionality into self-contained functions.
|
||||||
*
|
*
|
||||||
* The earlier functions return ParseNodes.
|
* The functions return ParseNodes.
|
||||||
* The later functions (which are called deeper in the parse) sometimes return
|
|
||||||
* ParsedFuncOrArg, which contain a ParseNode as well as some data about
|
|
||||||
* whether the parsed object is a function which is missing some arguments, or a
|
|
||||||
* standalone object which can be used as an argument to another function.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type ParsedFunc = {|
|
|
||||||
type: "fn",
|
|
||||||
result: string, // Function name defined via defineFunction (e.g. "\\frac").
|
|
||||||
token: Token,
|
|
||||||
|};
|
|
||||||
type ParsedArg = {|
|
|
||||||
type: "arg",
|
|
||||||
result: AnyParseNode,
|
|
||||||
token: Token,
|
|
||||||
|};
|
|
||||||
type ParsedFuncOrArg = ParsedFunc | ParsedArg;
|
|
||||||
|
|
||||||
function newArgument(result: AnyParseNode, token: Token): ParsedArg {
|
|
||||||
return {type: "arg", result, token};
|
|
||||||
}
|
|
||||||
|
|
||||||
function newFunction(token: Token): ParsedFunc {
|
|
||||||
return {type: "fn", result: token.text, token};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Parser {
|
export default class Parser {
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
gullet: MacroExpander;
|
gullet: MacroExpander;
|
||||||
@@ -190,12 +165,6 @@ export default class Parser {
|
|||||||
}
|
}
|
||||||
const atom = this.parseAtom(breakOnTokenText);
|
const atom = this.parseAtom(breakOnTokenText);
|
||||||
if (!atom) {
|
if (!atom) {
|
||||||
if (!this.settings.throwOnError && lex.text[0] === "\\") {
|
|
||||||
const errorNode = this.handleUnsupportedCmd();
|
|
||||||
body.push(errorNode);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
body.push(atom);
|
body.push(atom);
|
||||||
@@ -275,33 +244,16 @@ export default class Parser {
|
|||||||
const symbol = symbolToken.text;
|
const symbol = symbolToken.text;
|
||||||
this.consume();
|
this.consume();
|
||||||
this.consumeSpaces(); // ignore spaces before sup/subscript argument
|
this.consumeSpaces(); // ignore spaces before sup/subscript argument
|
||||||
const group = this.parseGroup();
|
const group = this.parseGroup(name, false, Parser.SUPSUB_GREEDINESS);
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") {
|
|
||||||
return this.handleUnsupportedCmd();
|
|
||||||
} else {
|
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Expected group after '" + symbol + "'",
|
"Expected group after '" + symbol + "'",
|
||||||
symbolToken
|
symbolToken
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (group.type === "fn") {
|
return group;
|
||||||
// ^ and _ have a greediness, so handle interactions with functions'
|
|
||||||
// greediness
|
|
||||||
const funcGreediness = functions[group.result].greediness;
|
|
||||||
if (funcGreediness > Parser.SUPSUB_GREEDINESS) {
|
|
||||||
return this.parseGivenFunction(group);
|
|
||||||
} else {
|
|
||||||
throw new ParseError(
|
|
||||||
"Got function '" + group.result + "' with no arguments " +
|
|
||||||
"as " + name, symbolToken);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return group.result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -339,7 +291,7 @@ export default class Parser {
|
|||||||
parseAtom(breakOnTokenText?: BreakToken): ?AnyParseNode {
|
parseAtom(breakOnTokenText?: BreakToken): ?AnyParseNode {
|
||||||
// The body of an atom is an implicit group, so that things like
|
// The body of an atom is an implicit group, so that things like
|
||||||
// \left(x\right)^2 work correctly.
|
// \left(x\right)^2 work correctly.
|
||||||
const base = this.parseImplicitGroup(breakOnTokenText);
|
const base = this.parseGroup("atom", false, null, breakOnTokenText);
|
||||||
|
|
||||||
// In text mode, we don't have superscripts or subscripts
|
// In text mode, we don't have superscripts or subscripts
|
||||||
if (this.mode === "text") {
|
if (this.mode === "text") {
|
||||||
@@ -430,96 +382,30 @@ export default class Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses an implicit group, which is a group that starts at the end of a
|
|
||||||
* specified, and ends right before a higher explicit group ends, or at EOL. It
|
|
||||||
* is used for functions that appear to affect the current style, like \Large or
|
|
||||||
* \textrm, where instead of keeping a style we just pretend that there is an
|
|
||||||
* implicit grouping after it until the end of the group. E.g.
|
|
||||||
* small text {\Large large text} small text again
|
|
||||||
*/
|
|
||||||
parseImplicitGroup(breakOnTokenText?: BreakToken): ?AnyParseNode {
|
|
||||||
const start = this.parseSymbol();
|
|
||||||
|
|
||||||
if (start == null) {
|
|
||||||
// If we didn't get anything we handle, fall back to parseFunction
|
|
||||||
return this.parseFunction();
|
|
||||||
} else if (start.type === "arg") {
|
|
||||||
// Defer to parseGivenFunction if it's not a function we handle
|
|
||||||
return this.parseGivenFunction(start);
|
|
||||||
}
|
|
||||||
|
|
||||||
const func = start.result;
|
|
||||||
|
|
||||||
if (func === "\\begin") {
|
|
||||||
// begin...end is similar to left...right
|
|
||||||
const begin =
|
|
||||||
assertNodeType(this.parseGivenFunction(start), "environment");
|
|
||||||
|
|
||||||
const envName = begin.name;
|
|
||||||
if (!environments.hasOwnProperty(envName)) {
|
|
||||||
throw new ParseError(
|
|
||||||
"No such environment: " + envName, begin.nameGroup);
|
|
||||||
}
|
|
||||||
// Build the environment object. Arguments and other information will
|
|
||||||
// be made available to the begin and end methods using properties.
|
|
||||||
const env = environments[envName];
|
|
||||||
const {args, optArgs} =
|
|
||||||
this.parseArguments("\\begin{" + envName + "}", env);
|
|
||||||
const context = {
|
|
||||||
mode: this.mode,
|
|
||||||
envName: envName,
|
|
||||||
parser: this,
|
|
||||||
};
|
|
||||||
const result = env.handler(context, args, optArgs);
|
|
||||||
this.expect("\\end", false);
|
|
||||||
const endNameToken = this.nextToken;
|
|
||||||
let end = this.parseFunction();
|
|
||||||
if (!end) {
|
|
||||||
throw new ParseError("failed to parse function after \\end");
|
|
||||||
}
|
|
||||||
end = assertNodeType(end, "environment");
|
|
||||||
if (end.name !== envName) {
|
|
||||||
throw new ParseError(
|
|
||||||
`Mismatch: \\begin{${envName}} matched by \\end{${end.name}}`,
|
|
||||||
endNameToken);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
// Defer to parseGivenFunction if it's not a function we handle
|
|
||||||
return this.parseGivenFunction(start, breakOnTokenText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses an entire function, including its base and all of its arguments.
|
* Parses an entire function, including its base and all of its arguments.
|
||||||
* It also handles the case where the parsed node is not a function.
|
|
||||||
*/
|
*/
|
||||||
parseFunction(): ?AnyParseNode {
|
parseFunction(
|
||||||
const baseGroup = this.parseGroup();
|
|
||||||
return baseGroup ? this.parseGivenFunction(baseGroup) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as parseFunction(), except that the base is provided, guaranteeing a
|
|
||||||
* non-nullable result.
|
|
||||||
*/
|
|
||||||
parseGivenFunction(
|
|
||||||
baseGroup: ParsedFuncOrArg,
|
|
||||||
breakOnTokenText?: BreakToken,
|
breakOnTokenText?: BreakToken,
|
||||||
): AnyParseNode {
|
name?: string, // For error reporting.
|
||||||
if (baseGroup.type === "fn") {
|
greediness?: ?number,
|
||||||
const func = baseGroup.result;
|
): ?AnyParseNode {
|
||||||
|
const token = this.nextToken;
|
||||||
|
const func = token.text;
|
||||||
const funcData = functions[func];
|
const funcData = functions[func];
|
||||||
if (this.mode === "text" && !funcData.allowedInText) {
|
if (!funcData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (greediness != null && funcData.greediness <= greediness) {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Can't use function '" + func + "' in text mode",
|
"Got function '" + func + "' with no arguments" +
|
||||||
baseGroup.token);
|
(name ? " as " + name : ""), token);
|
||||||
} else if (this.mode === "math" &&
|
} else if (this.mode === "text" && !funcData.allowedInText) {
|
||||||
funcData.allowedInMath === false) {
|
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Can't use function '" + func + "' in math mode",
|
"Can't use function '" + func + "' in text mode", token);
|
||||||
baseGroup.token);
|
} else if (this.mode === "math" && funcData.allowedInMath === false) {
|
||||||
|
throw new ParseError(
|
||||||
|
"Can't use function '" + func + "' in math mode", token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume the command token after possibly switching to the
|
// Consume the command token after possibly switching to the
|
||||||
@@ -534,12 +420,7 @@ export default class Parser {
|
|||||||
this.consume();
|
this.consume();
|
||||||
}
|
}
|
||||||
const {args, optArgs} = this.parseArguments(func, funcData);
|
const {args, optArgs} = this.parseArguments(func, funcData);
|
||||||
const token = baseGroup.token;
|
return this.callFunction(func, args, optArgs, token, breakOnTokenText);
|
||||||
return this.callFunction(
|
|
||||||
func, args, optArgs, token, breakOnTokenText);
|
|
||||||
} else {
|
|
||||||
return baseGroup.result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -605,37 +486,17 @@ export default class Parser {
|
|||||||
this.consumeSpaces();
|
this.consumeSpaces();
|
||||||
}
|
}
|
||||||
const nextToken = this.nextToken;
|
const nextToken = this.nextToken;
|
||||||
let arg = argType ?
|
const arg = this.parseGroupOfType("argument to '" + func + "'",
|
||||||
this.parseGroupOfType(argType, isOptional) :
|
argType, isOptional, baseGreediness);
|
||||||
this.parseGroup(isOptional);
|
|
||||||
if (!arg) {
|
if (!arg) {
|
||||||
if (isOptional) {
|
if (isOptional) {
|
||||||
optArgs.push(null);
|
optArgs.push(null);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!this.settings.throwOnError &&
|
|
||||||
this.nextToken.text[0] === "\\") {
|
|
||||||
arg = newArgument(this.handleUnsupportedCmd(), nextToken);
|
|
||||||
} else {
|
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
"Expected group after '" + func + "'", nextToken);
|
"Expected group after '" + func + "'", nextToken);
|
||||||
}
|
}
|
||||||
}
|
(isOptional ? optArgs : args).push(arg);
|
||||||
let argNode: AnyParseNode;
|
|
||||||
if (arg.type === "fn") {
|
|
||||||
const argGreediness =
|
|
||||||
functions[arg.result].greediness;
|
|
||||||
if (argGreediness > baseGreediness) {
|
|
||||||
argNode = this.parseGivenFunction(arg);
|
|
||||||
} else {
|
|
||||||
throw new ParseError(
|
|
||||||
"Got function '" + arg.result + "' as " +
|
|
||||||
"argument to '" + func + "'", nextToken);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
argNode = arg.result;
|
|
||||||
}
|
|
||||||
(isOptional ? optArgs : args).push(argNode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {args, optArgs};
|
return {args, optArgs};
|
||||||
@@ -645,35 +506,29 @@ export default class Parser {
|
|||||||
* Parses a group when the mode is changing.
|
* Parses a group when the mode is changing.
|
||||||
*/
|
*/
|
||||||
parseGroupOfType(
|
parseGroupOfType(
|
||||||
type: ArgType, // Used to describe the mode in error messages.
|
name: string,
|
||||||
|
type: ?ArgType,
|
||||||
optional: boolean,
|
optional: boolean,
|
||||||
): ?ParsedFuncOrArg {
|
greediness: ?number,
|
||||||
// Handle `original` argTypes
|
): ?AnyParseNode {
|
||||||
if (type === "original") {
|
switch (type) {
|
||||||
type = this.mode;
|
case "color":
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "color") {
|
|
||||||
return this.parseColorGroup(optional);
|
return this.parseColorGroup(optional);
|
||||||
}
|
case "size":
|
||||||
if (type === "size") {
|
|
||||||
return this.parseSizeGroup(optional);
|
return this.parseSizeGroup(optional);
|
||||||
}
|
case "url":
|
||||||
if (type === "url") {
|
|
||||||
return this.parseUrlGroup(optional);
|
return this.parseUrlGroup(optional);
|
||||||
|
case "math":
|
||||||
|
case "text":
|
||||||
|
return this.parseGroup(name, optional, greediness, undefined, type);
|
||||||
|
case "original":
|
||||||
|
case null:
|
||||||
|
case undefined:
|
||||||
|
return this.parseGroup(name, optional, greediness);
|
||||||
|
default:
|
||||||
|
throw new ParseError(
|
||||||
|
"Unknown group type as " + name, this.nextToken);
|
||||||
}
|
}
|
||||||
if (type === "raw") {
|
|
||||||
const token = this.parseStringGroup("raw", optional, true);
|
|
||||||
return token ? newArgument({
|
|
||||||
type: "raw",
|
|
||||||
mode: this.mode,
|
|
||||||
string: token.text,
|
|
||||||
}, token) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// By the time we get here, type is one of "text" or "math".
|
|
||||||
// Specify this as mode to parseGroup.
|
|
||||||
return this.parseGroup(optional, type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeSpaces() {
|
consumeSpaces() {
|
||||||
@@ -796,7 +651,7 @@ export default class Parser {
|
|||||||
/**
|
/**
|
||||||
* Parses a color description.
|
* Parses a color description.
|
||||||
*/
|
*/
|
||||||
parseColorGroup(optional: boolean): ?ParsedArg {
|
parseColorGroup(optional: boolean): ?ParseNode<"color-token"> {
|
||||||
const res = this.parseStringGroup("color", optional);
|
const res = this.parseStringGroup("color", optional);
|
||||||
if (!res) {
|
if (!res) {
|
||||||
return null;
|
return null;
|
||||||
@@ -812,17 +667,17 @@ export default class Parser {
|
|||||||
// Predefined color names are all missed by this RegEx pattern.
|
// Predefined color names are all missed by this RegEx pattern.
|
||||||
color = "#" + color;
|
color = "#" + color;
|
||||||
}
|
}
|
||||||
return newArgument({
|
return {
|
||||||
type: "color-token",
|
type: "color-token",
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
color,
|
color,
|
||||||
}, res);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a size specification, consisting of magnitude and unit.
|
* Parses a size specification, consisting of magnitude and unit.
|
||||||
*/
|
*/
|
||||||
parseSizeGroup(optional: boolean): ?ParsedArg {
|
parseSizeGroup(optional: boolean): ?ParseNode<"size"> {
|
||||||
let res;
|
let res;
|
||||||
let isBlank = false;
|
let isBlank = false;
|
||||||
if (!optional && this.nextToken.text !== "{") {
|
if (!optional && this.nextToken.text !== "{") {
|
||||||
@@ -852,18 +707,18 @@ export default class Parser {
|
|||||||
if (!validUnit(data)) {
|
if (!validUnit(data)) {
|
||||||
throw new ParseError("Invalid unit: '" + data.unit + "'", res);
|
throw new ParseError("Invalid unit: '" + data.unit + "'", res);
|
||||||
}
|
}
|
||||||
return newArgument({
|
return {
|
||||||
type: "size",
|
type: "size",
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
value: data,
|
value: data,
|
||||||
isBlank,
|
isBlank,
|
||||||
}, res);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses an URL, checking escaped letters and allowed protocols.
|
* Parses an URL, checking escaped letters and allowed protocols.
|
||||||
*/
|
*/
|
||||||
parseUrlGroup(optional: boolean): ?ParsedArg {
|
parseUrlGroup(optional: boolean): ?ParseNode<"url"> {
|
||||||
const res = this.parseStringGroup("url", optional, true); // get raw string
|
const res = this.parseStringGroup("url", optional, true); // get raw string
|
||||||
if (!res) {
|
if (!res) {
|
||||||
return null;
|
return null;
|
||||||
@@ -881,32 +736,43 @@ export default class Parser {
|
|||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
`Forbidden protocol '${protocol}'`, res);
|
`Forbidden protocol '${protocol}'`, res);
|
||||||
}
|
}
|
||||||
return newArgument({
|
return {
|
||||||
type: "url",
|
type: "url",
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
url,
|
url,
|
||||||
}, res);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If `optional` is false or absent, this parses an ordinary group,
|
* If `optional` is false or absent, this parses an ordinary group,
|
||||||
* which is either a single nucleus (like "x") or an expression
|
* which is either a single nucleus (like "x") or an expression
|
||||||
* in braces (like "{x+y}").
|
* 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
|
* If `optional` is true, it parses either a bracket-delimited expression
|
||||||
* (like "[x+y]") or returns null to indicate the absence of a
|
* (like "[x+y]") or returns null to indicate the absence of a
|
||||||
* bracket-enclosed group.
|
* bracket-enclosed group.
|
||||||
* If `mode` is present, switches to that mode while parsing the group,
|
* If `mode` is present, switches to that mode while parsing the group,
|
||||||
* and switches back after.
|
* and switches back after.
|
||||||
*/
|
*/
|
||||||
parseGroup(optional?: boolean, mode?: Mode): ?ParsedFuncOrArg {
|
parseGroup(
|
||||||
|
name: string, // For error reporting.
|
||||||
|
optional?: boolean,
|
||||||
|
greediness?: ?number,
|
||||||
|
breakOnTokenText?: BreakToken,
|
||||||
|
mode?: Mode,
|
||||||
|
): ?AnyParseNode {
|
||||||
const outerMode = this.mode;
|
const outerMode = this.mode;
|
||||||
const firstToken = this.nextToken;
|
const firstToken = this.nextToken;
|
||||||
// Try to parse an open brace
|
const text = firstToken.text;
|
||||||
if (this.nextToken.text === (optional ? "[" : "{")) {
|
// Switch to specified mode
|
||||||
// Switch to specified mode before we expand symbol after brace
|
|
||||||
if (mode) {
|
if (mode) {
|
||||||
this.switchMode(mode);
|
this.switchMode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
// Try to parse an open brace
|
||||||
|
if (text === (optional ? "[" : "{")) {
|
||||||
// Start a new group namespace
|
// Start a new group namespace
|
||||||
this.gullet.beginGroup();
|
this.gullet.beginGroup();
|
||||||
// If we get a brace, parse an expression
|
// If we get a brace, parse an expression
|
||||||
@@ -921,24 +787,36 @@ export default class Parser {
|
|||||||
this.gullet.endGroup();
|
this.gullet.endGroup();
|
||||||
// Make sure we get a close brace
|
// Make sure we get a close brace
|
||||||
this.expect(optional ? "]" : "}");
|
this.expect(optional ? "]" : "}");
|
||||||
return newArgument({
|
return {
|
||||||
type: "ordgroup",
|
type: "ordgroup",
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
loc: SourceLocation.range(firstToken, lastToken),
|
loc: SourceLocation.range(firstToken, lastToken),
|
||||||
body: expression,
|
body: expression,
|
||||||
}, firstToken.range(lastToken, firstToken.text));
|
};
|
||||||
|
} else if (optional) {
|
||||||
|
// Return nothing for an optional group
|
||||||
|
result = null;
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, just return a nucleus, or nothing for an optional group
|
// If there exists a function with this name, parse the function.
|
||||||
if (mode) {
|
// Otherwise, just return a nucleus
|
||||||
this.switchMode(mode);
|
result = this.parseFunction(breakOnTokenText, name, greediness) ||
|
||||||
|
this.parseSymbol();
|
||||||
|
if (result == null && text[0] === "\\" &&
|
||||||
|
!implicitCommands.hasOwnProperty(text)) {
|
||||||
|
if (this.settings.throwOnError) {
|
||||||
|
throw new ParseError(
|
||||||
|
"Undefined control sequence: " + text, firstToken);
|
||||||
}
|
}
|
||||||
const result = optional ? null : this.parseSymbol();
|
result = this.handleUnsupportedCmd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch mode back
|
||||||
if (mode) {
|
if (mode) {
|
||||||
this.switchMode(outerMode);
|
this.switchMode(outerMode);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form ligature-like combinations of characters for text mode.
|
* Form ligature-like combinations of characters for text mode.
|
||||||
@@ -986,20 +864,14 @@ export default class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a single symbol out of the string. Here, we handle both the functions
|
* Parse a single symbol out of the string. Here, we handle single character
|
||||||
* we have defined, as well as the single character symbols
|
* symbols and special functions like verbatim
|
||||||
*/
|
*/
|
||||||
parseSymbol(): ?ParsedFuncOrArg {
|
parseSymbol(): ?AnyParseNode {
|
||||||
const nucleus = this.nextToken;
|
const nucleus = this.nextToken;
|
||||||
let text = nucleus.text;
|
let text = nucleus.text;
|
||||||
|
|
||||||
if (functions[text]) {
|
if (/^\\verb[^a-zA-Z]/.test(text)) {
|
||||||
// If there exists a function with this name, we return the
|
|
||||||
// function and say that it is a function.
|
|
||||||
// The token will be consumed later in parseGivenFunction
|
|
||||||
// (after possibly switching modes).
|
|
||||||
return newFunction(nucleus);
|
|
||||||
} else if (/^\\verb[^a-zA-Z]/.test(text)) {
|
|
||||||
this.consume();
|
this.consume();
|
||||||
let arg = text.slice(5);
|
let arg = text.slice(5);
|
||||||
const star = (arg.charAt(0) === "*");
|
const star = (arg.charAt(0) === "*");
|
||||||
@@ -1013,12 +885,12 @@ export default class Parser {
|
|||||||
please report what input caused this bug`);
|
please report what input caused this bug`);
|
||||||
}
|
}
|
||||||
arg = arg.slice(1, -1); // remove first and last char
|
arg = arg.slice(1, -1); // remove first and last char
|
||||||
return newArgument({
|
return {
|
||||||
type: "verb",
|
type: "verb",
|
||||||
mode: "text",
|
mode: "text",
|
||||||
body: arg,
|
body: arg,
|
||||||
star,
|
star,
|
||||||
}, nucleus);
|
};
|
||||||
} else if (text === "%") {
|
} else if (text === "%") {
|
||||||
this.consumeComment();
|
this.consumeComment();
|
||||||
return this.parseSymbol();
|
return this.parseSymbol();
|
||||||
@@ -1123,6 +995,6 @@ export default class Parser {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newArgument(symbol, nucleus);
|
return symbol;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -114,11 +114,8 @@ function parseArray(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
} else if (next === "\\cr") {
|
} else if (next === "\\cr") {
|
||||||
const cr = parser.parseFunction();
|
const cr = assertNodeType(parser.parseFunction(), "cr");
|
||||||
if (!cr) {
|
rowGaps.push(cr.size);
|
||||||
throw new ParseError(`Failed to parse function after ${next}`);
|
|
||||||
}
|
|
||||||
rowGaps.push(assertNodeType(cr, "cr").size);
|
|
||||||
|
|
||||||
// check for \hline(s) following the row separator
|
// check for \hline(s) following the row separator
|
||||||
hLinesBeforeRow.push(getHLines(parser));
|
hLinesBeforeRow.push(getHLines(parser));
|
||||||
|
@@ -170,16 +170,13 @@ defineFunction({
|
|||||||
--parser.leftrightDepth;
|
--parser.leftrightDepth;
|
||||||
// Check the next token
|
// Check the next token
|
||||||
parser.expect("\\right", false);
|
parser.expect("\\right", false);
|
||||||
const right = parser.parseFunction();
|
const right = assertNodeType(parser.parseFunction(), "leftright-right");
|
||||||
if (!right) {
|
|
||||||
throw new ParseError('failed to parse function after \\right');
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
type: "leftright",
|
type: "leftright",
|
||||||
mode: parser.mode,
|
mode: parser.mode,
|
||||||
body,
|
body,
|
||||||
left: delim.text,
|
left: delim.text,
|
||||||
right: assertNodeType(right, "leftright-right").delim,
|
right: right.delim,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
htmlBuilder: (group, options) => {
|
htmlBuilder: (group, options) => {
|
||||||
|
@@ -2,9 +2,11 @@
|
|||||||
import defineFunction from "../defineFunction";
|
import defineFunction from "../defineFunction";
|
||||||
import ParseError from "../ParseError";
|
import ParseError from "../ParseError";
|
||||||
import {assertNodeType} from "../parseNode";
|
import {assertNodeType} from "../parseNode";
|
||||||
|
import environments from "../environments";
|
||||||
|
|
||||||
// Environment delimiters. HTML/MathML rendering is defined in the corresponding
|
// Environment delimiters. HTML/MathML rendering is defined in the corresponding
|
||||||
// defineEnvironment definitions.
|
// defineEnvironment definitions.
|
||||||
|
// $FlowFixMe, "environment" handler returns an environment ParseNode
|
||||||
defineFunction({
|
defineFunction({
|
||||||
type: "environment",
|
type: "environment",
|
||||||
names: ["\\begin", "\\end"],
|
names: ["\\begin", "\\end"],
|
||||||
@@ -12,19 +14,48 @@ defineFunction({
|
|||||||
numArgs: 1,
|
numArgs: 1,
|
||||||
argTypes: ["text"],
|
argTypes: ["text"],
|
||||||
},
|
},
|
||||||
handler({parser}, args) {
|
handler({parser, funcName}, args) {
|
||||||
const nameGroup = args[0];
|
const nameGroup = args[0];
|
||||||
if (nameGroup.type !== "ordgroup") {
|
if (nameGroup.type !== "ordgroup") {
|
||||||
throw new ParseError("Invalid environment name", nameGroup);
|
throw new ParseError("Invalid environment name", nameGroup);
|
||||||
}
|
}
|
||||||
let name = "";
|
let envName = "";
|
||||||
for (let i = 0; i < nameGroup.body.length; ++i) {
|
for (let i = 0; i < nameGroup.body.length; ++i) {
|
||||||
name += assertNodeType(nameGroup.body[i], "textord").text;
|
envName += assertNodeType(nameGroup.body[i], "textord").text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (funcName === "\\begin") {
|
||||||
|
// begin...end is similar to left...right
|
||||||
|
if (!environments.hasOwnProperty(envName)) {
|
||||||
|
throw new ParseError(
|
||||||
|
"No such environment: " + envName, nameGroup);
|
||||||
|
}
|
||||||
|
// Build the environment object. Arguments and other information will
|
||||||
|
// be made available to the begin and end methods using properties.
|
||||||
|
const env = environments[envName];
|
||||||
|
const {args, optArgs} =
|
||||||
|
parser.parseArguments("\\begin{" + envName + "}", env);
|
||||||
|
const context = {
|
||||||
|
mode: parser.mode,
|
||||||
|
envName,
|
||||||
|
parser,
|
||||||
|
};
|
||||||
|
const result = env.handler(context, args, optArgs);
|
||||||
|
parser.expect("\\end", false);
|
||||||
|
const endNameToken = parser.nextToken;
|
||||||
|
const end = assertNodeType(parser.parseFunction(), "environment");
|
||||||
|
if (end.name !== envName) {
|
||||||
|
throw new ParseError(
|
||||||
|
`Mismatch: \\begin{${envName}} matched by \\end{${end.name}}`,
|
||||||
|
endNameToken);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "environment",
|
type: "environment",
|
||||||
mode: parser.mode,
|
mode: parser.mode,
|
||||||
name,
|
name: envName,
|
||||||
nameGroup,
|
nameGroup,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@@ -29,7 +29,7 @@ describe("Parser:", function() {
|
|||||||
it("rejects \\sqrt as argument to ^", function() {
|
it("rejects \\sqrt as argument to ^", function() {
|
||||||
expect`1^\sqrt{2}`.toFailWithParseError(
|
expect`1^\sqrt{2}`.toFailWithParseError(
|
||||||
"Got function '\\sqrt' with no arguments as superscript" +
|
"Got function '\\sqrt' with no arguments as superscript" +
|
||||||
" at position 2: 1^̲\\sqrt{2}");
|
" at position 3: 1^\\̲s̲q̲r̲t̲{2}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,28 +106,17 @@ describe("Parser:", function() {
|
|||||||
" at position 10: 1^{2\\sqrt}̲");
|
" at position 10: 1^{2\\sqrt}̲");
|
||||||
});
|
});
|
||||||
it("complains about functions as arguments to others", function() {
|
it("complains about functions as arguments to others", function() {
|
||||||
// TODO: The position looks pretty wrong here
|
|
||||||
expect`\sqrt\over2`.toFailWithParseError(
|
expect`\sqrt\over2`.toFailWithParseError(
|
||||||
"Got function '\\over' as argument to '\\sqrt'" +
|
"Got function '\\over' with no arguments as argument to" +
|
||||||
" at position 6: \\sqrt\\̲o̲v̲e̲r̲2");
|
" '\\sqrt' at position 6: \\sqrt\\̲o̲v̲e̲r̲2");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#parseArguments", function() {
|
describe("#parseGroup", function() {
|
||||||
it("complains about missing argument at end of input", function() {
|
it("complains about undefined control sequence", function() {
|
||||||
expect`2\sqrt`.toFailWithParseError(
|
expect`\xyz`.toFailWithParseError(
|
||||||
"Expected group after '\\sqrt' at end of input: 2\\sqrt");
|
"Undefined control sequence: \\xyz" +
|
||||||
});
|
" at position 1: \\̲x̲y̲z̲");
|
||||||
it("complains about missing argument at end of group", function() {
|
|
||||||
expect`1^{2\sqrt}`.toFailWithParseError(
|
|
||||||
"Expected group after '\\sqrt'" +
|
|
||||||
" at position 10: 1^{2\\sqrt}̲");
|
|
||||||
});
|
|
||||||
it("complains about functions as arguments to others", function() {
|
|
||||||
// TODO: The position looks pretty wrong here
|
|
||||||
expect`\sqrt\over2`.toFailWithParseError(
|
|
||||||
"Got function '\\over' as argument to '\\sqrt'" +
|
|
||||||
" at position 6: \\sqrt\\̲o̲v̲e̲r̲2");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,7 +237,6 @@ describe("environments.js:", function() {
|
|||||||
|
|
||||||
describe("array environment", function() {
|
describe("array environment", function() {
|
||||||
it("rejects unknown column types", function() {
|
it("rejects unknown column types", function() {
|
||||||
// TODO: The error position here looks strange
|
|
||||||
expect`\begin{array}{cba}\end{array}`.toFailWithParseError(
|
expect`\begin{array}{cba}\end{array}`.toFailWithParseError(
|
||||||
"Unknown column alignment: b at position 16:" +
|
"Unknown column alignment: b at position 16:" +
|
||||||
" \\begin{array}{cb̲a}\\end{array}");
|
" \\begin{array}{cb̲a}\\end{array}");
|
||||||
|
Reference in New Issue
Block a user