flow typings for parser.js (#1041)

flow typings for parser.js
This commit is contained in:
Kevin Barabash
2017-12-29 11:26:22 -07:00
committed by Ashish Myles
parent 63733f796a
commit 5cc795eaaa
6 changed files with 136 additions and 163 deletions

View File

@@ -1,5 +1,4 @@
// @flow
import {Token} from "./Token";
import SourceLocation from "./SourceLocation";
import type {Mode} from "./types";
@@ -21,14 +20,14 @@ export default class ParseNode {
type: string, // type of node, like e.g. "ordgroup"
value: mixed, // type-specific representation of the node
mode: Mode, // parse mode in action for this node, "math" or "text"
firstToken?: Token, // first token of the input for this node,
// will omit position information if unset
lastToken?: Token, // last token of the input for this node,
// will default to firstToken if unset
first?: {loc: ?SourceLocation}, // first token or node of the input for
// this node, will omit position information if unset
last?: {loc: ?SourceLocation}, // last token or node of the input for this
// node, will default to firstToken if unset
) {
this.type = type;
this.value = value;
this.mode = mode;
this.loc = SourceLocation.range(firstToken, lastToken);
this.loc = SourceLocation.range(first, last);
}
}

View File

@@ -1,3 +1,4 @@
// @flow
/* eslint no-constant-condition:0 */
/* eslint no-console:0 */
import functions from "./functions";
@@ -12,6 +13,12 @@ import unicodeSymbols from "./unicodeSymbols";
import ParseNode from "./ParseNode";
import ParseError from "./ParseError";
import { combiningDiacriticalMarksEndRegex } from "./Lexer.js";
import Settings from "./Settings";
import { Token } from "./Token";
import type { Mode, ArgType } from "./types";
import type { FunctionContext, FunctionSpec } from "./defineFunction" ;
import type { EnvSpec } from "./defineEnvironment";
/**
* This file contains the parser used to parse out a TeX expression from the
@@ -47,8 +54,6 @@ import { combiningDiacriticalMarksEndRegex } from "./Lexer.js";
* standalone object which can be used as an argument to another function.
*/
/* TODO: Uncomment when porting to flow.
type ParsedType = "fn" | "arg" | "$"
type ParsedFunc = {|
type: "fn",
result: string, // Function name defined via defineFunction (e.g. "\\frac").
@@ -66,38 +71,21 @@ type ParsedDollar = {|
token: Token,
|};
type ParsedFuncOrArgOrDollar = ParsedFunc | ParsedArg | ParsedDollar;
*/
type ParsedFuncOrArg = ParsedFunc | ParsedArg;
/**
* @param {ParseNode} result
* @param {Token} token
* @return {ParsedArg}
*/
function newArgument(result, token) {
function newArgument(result: ParseNode, token: Token): ParsedArg {
return {type: "arg", result, token};
}
/**
* @param {Token} token
* @return {ParsedFunc}
*/
function newFunction(token) {
function newFunction(token: Token): ParsedFunc {
return {type: "fn", result: token.text, token};
}
/**
* @param {Token} token
* @return {ParsedDollar}
*/
function newDollar(token) {
function newDollar(token: Token): ParsedDollar {
return {type: "$", result: "$", token};
}
/**
* @param {ParsedFuncOrArgOrDollar} parsed
* @return {ParsedFuncOrArg}
*/
function assertFuncOrArg(parsed) {
function assertFuncOrArg(parsed: ParsedFuncOrArgOrDollar): ParsedFuncOrArg {
if (parsed.type === "$") {
throw new ParseError("Unexpected $", parsed.token);
}
@@ -105,7 +93,13 @@ function assertFuncOrArg(parsed) {
}
export default class Parser {
constructor(input, settings) {
mode: Mode;
gullet: MacroExpander;
settings: Settings;
leftrightDepth: number;
nextToken: Token;
constructor(input: string, settings: Settings) {
// Start in math mode
this.mode = "math";
// Create a new macro expander (gullet) and (indirectly via that) also a
@@ -125,18 +119,15 @@ export default class Parser {
/**
* Checks a result to make sure it has the right type, and throws an
* appropriate error otherwise.
*
* @param {boolean=} consume whether to consume the expected token,
* defaults to true
*/
expect(text, consume) {
expect(text: string, consume?: boolean = true) {
if (this.nextToken.text !== text) {
throw new ParseError(
"Expected '" + text + "', got '" + this.nextToken.text + "'",
this.nextToken
);
}
if (consume !== false) {
if (consume) {
this.consume();
}
}
@@ -152,17 +143,15 @@ export default class Parser {
/**
* Switches between "text" and "math" modes.
*/
switchMode(newMode) {
switchMode(newMode: Mode) {
this.mode = newMode;
this.gullet.switchMode(newMode);
}
/**
* Main parsing function, which parses an entire input.
*
* @return {Array.<ParseNode>}
*/
parse() {
parse(): ParseNode[] {
// Try to parse the input
this.consume();
const parse = this.parseInput();
@@ -172,7 +161,7 @@ export default class Parser {
/**
* Parses an entire input tree.
*/
parseInput() {
parseInput(): ParseNode[] {
// Parse an expression
const expression = this.parseExpression(false);
// If we succeeded, make sure there's an EOF at the end
@@ -185,17 +174,18 @@ export default class Parser {
/**
* Parses an "expression", which is a list of atoms.
*
* @param {boolean} breakOnInfix Should the parsing stop when we hit infix
* nodes? This happens when functions have higher precendence
* than infix nodes in implicit parses.
* `breakOnInfix`: Should the parsing stop when we hit infix nodes? This
* happens when functions have higher precendence han infix
* nodes in implicit parses.
*
* @param {?string} breakOnTokenText The text of the token that the expression
* should end with, or `null` if something else should end the
* `breakOnTokenText`: The text of the token that the expression should end
* with, or `null` if something else should end the
* expression.
*
* @return {Array<ParseNode>}
*/
parseExpression(breakOnInfix, breakOnTokenText) {
parseExpression(
breakOnInfix: boolean,
breakOnTokenText?: "]" | "}" | "$",
): ParseNode[] {
const body = [];
// Keep adding atoms to the body until we can't parse any more atoms (either
// we reached the end, a }, or a \right)
@@ -235,11 +225,8 @@ export default class Parser {
*
* There can only be one infix operator per group. If there's more than one
* then the expression is ambiguous. This can be resolved by adding {}.
*
* @param {Array<ParseNode>} body
* @return {Array<ParseNode>}
*/
handleInfixNodes(body) {
handleInfixNodes(body: ParseNode[]): ParseNode[] {
let overIndex = -1;
let funcName;
@@ -256,7 +243,7 @@ export default class Parser {
}
}
if (overIndex !== -1) {
if (overIndex !== -1 && funcName) {
let numerNode;
let denomNode;
@@ -287,10 +274,10 @@ export default class Parser {
/**
* Handle a subscript or superscript with nice errors.
* @param {string} name For error reporting.
* @return {ParsedNode}
*/
handleSupSubscript(name) {
handleSupSubscript(
name: string, // For error reporting.
): ParseNode {
const symbolToken = this.nextToken;
const symbol = symbolToken.text;
this.consume();
@@ -312,16 +299,16 @@ export default class Parser {
if (arg.type === "fn") {
// ^ and _ have a greediness, so handle interactions with functions'
// greediness
const funcGreediness = functions[group.result].greediness;
const funcGreediness = functions[arg.result].greediness;
if (funcGreediness > Parser.SUPSUB_GREEDINESS) {
return this.parseGivenFunction(group);
} else {
throw new ParseError(
"Got function '" + group.result + "' with no arguments " +
"Got function '" + arg.result + "' with no arguments " +
"as " + name, symbolToken);
}
} else {
return group.result;
return arg.result;
}
}
@@ -329,7 +316,7 @@ export default class Parser {
* Converts the textual input of an unsupported command into a text node
* contained within a color node whose color is determined by errorColor
*/
handleUnsupportedCmd() {
handleUnsupportedCmd(): ParseNode {
const text = this.nextToken.text;
const textordArray = [];
@@ -360,11 +347,8 @@ export default class Parser {
/**
* Parses a group with optional super/subscripts.
*
* @param {"]" | "}"} breakOnTokenText - character to stop parsing the group on.
* @return {?ParseNode}
*/
parseAtom(breakOnTokenText) {
parseAtom(breakOnTokenText?: "]" | "}" | "$"): ?ParseNode {
// The body of an atom is an implicit group, so that things like
// \left(x\right)^2 work correctly.
const base = this.parseImplicitGroup(breakOnTokenText);
@@ -481,21 +465,39 @@ export default class Parser {
* implicit grouping after it until the end of the group. E.g.
* small text {\Large large text} small text again
* It is also used for \left and \right to get the correct grouping.
*
* @param {"]" | "}"} breakOnTokenText - character to stop parsing the group on.
* @return {?ParseNode}
*/
parseImplicitGroup(breakOnTokenText) {
parseImplicitGroup(breakOnTokenText?: "]" | "}" | "$"): ?ParseNode {
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 === "\\left") {
if (func === "$") {
if (this.mode === "math") {
throw new ParseError("$ within math mode");
}
const outerMode = this.mode;
this.switchMode("math");
// Expand next symbol now that we're in math mode.
this.consume();
const body = this.parseExpression(false, "$");
// We can't expand the next symbol after the $ until after
// switching modes back. So don't consume within expect.
this.expect("$", false);
this.switchMode(outerMode);
this.consume();
return new ParseNode("styling", {
style: "text",
value: body,
}, "math");
} else if (func === "\\left") {
// If we see a left:
// Parse the entire left function (including the delimiter)
const left = this.parseGivenFunction(start);
@@ -506,6 +508,9 @@ export default class Parser {
// Check the next token
this.expect("\\right", false);
const right = this.parseFunction();
if (!right) {
throw new ParseError('failed to parse function after \\right');
}
return new ParseNode("leftright", {
body: body,
left: left.value.value,
@@ -515,13 +520,13 @@ export default class Parser {
// begin...end is similar to left...right
const begin = this.parseGivenFunction(start);
const envName = begin.value.name;
if (!environments.has(envName)) {
if (!environments.hasOwnProperty(envName)) {
throw new ParseError(
"No such environment: " + envName, begin.value.nameGroup);
}
// Build the environment object. Arguments and other information will
// be made available to the begin and end methods using properties.
const env = environments.get(envName);
const env = environments[envName];
const {args, optArgs} =
this.parseArguments("\\begin{" + envName + "}", env);
const context = {
@@ -533,13 +538,14 @@ export default class Parser {
this.expect("\\end", false);
const endNameToken = this.nextToken;
const end = this.parseFunction();
if (end.value.name !== envName) {
if (!end) {
throw new ParseError("failed to parse function after \\end");
} else if (end.value.name !== envName) {
throw new ParseError(
"Mismatch: \\begin{" + envName + "} matched " +
"by \\end{" + end.value.name + "}",
endNameToken);
}
result.position = end.position;
return result;
} else if (utils.contains(Parser.sizeFuncs, func)) {
// If we see a sizing function, parse out the implicit body
@@ -588,24 +594,6 @@ export default class Parser {
color: color.result.value,
value: body,
}, this.mode);
} else if (func === "$") {
if (this.mode === "math") {
throw new ParseError("$ within math mode");
}
const outerMode = this.mode;
this.switchMode("math");
// Expand next symbol now that we're in math mode.
this.consume();
const body = this.parseExpression(false, "$");
// We can't expand the next symbol after the $ until after
// switching modes back. So don't consume within expect.
this.expect("$", false);
this.switchMode(outerMode);
this.consume();
return new ParseNode("styling", {
style: "text",
value: body,
}, "math");
} else {
// Defer to parseGivenFunction if it's not a function we handle
return this.parseGivenFunction(start);
@@ -615,10 +603,8 @@ export default class Parser {
/**
* 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.
*
* @return {?ParseNode}
*/
parseFunction() {
parseFunction(): ?ParseNode {
const baseGroup = this.parseGroup();
return baseGroup ? this.parseGivenFunction(baseGroup) : null;
}
@@ -626,11 +612,8 @@ export default class Parser {
/**
* Same as parseFunction(), except that the base is provided, guaranteeing a
* non-nullable result.
*
* @param {ParsedFuncOrArgOrDollar} baseGroup
* @return {ParseNode}
*/
parseGivenFunction(baseGroup) {
parseGivenFunction(baseGroup: ParsedFuncOrArgOrDollar): ParseNode {
baseGroup = assertFuncOrArg(baseGroup);
if (baseGroup.type === "fn") {
const func = baseGroup.result;
@@ -657,34 +640,36 @@ export default class Parser {
/**
* Call a function handler with a suitable context and arguments.
* @param {string} name
* @param {Array<ParseNode>} args
* @param {Array<?ParseNode>} optArgs
* @param {Token=} token
*/
callFunction(name, args, optArgs, token) {
const context = {
callFunction(
name: string,
args: ParseNode[],
optArgs: (?ParseNode)[],
token?: Token,
): * {
const context: FunctionContext = {
funcName: name,
parser: this,
token,
};
return functions[name].handler(context, args, optArgs);
const func = functions[name];
if (func && func.handler) {
return func.handler(context, args, optArgs);
} else {
throw new ParseError(`No function handler for ${name}`);
}
}
/**
* Parses the arguments of a function or environment
*
* @param {string} func "\name" or "\begin{name}"
* @param {{
* numArgs: number,
* numOptionalArgs: (number|undefined),
* }} funcData
* @return {{
* args: Array<ParseNode>,
* optArgs: Array<?ParseNode>,
* }}
*/
parseArguments(func, funcData) {
parseArguments(
func: string, // Should look like "\name" or "\begin{name}".
funcData: FunctionSpec | EnvSpec,
): {
args: ParseNode[],
optArgs: (?ParseNode)[],
} {
const totalArgs = funcData.numArgs + funcData.numOptionalArgs;
if (totalArgs === 0) {
return {args: [], optArgs: []};
@@ -730,7 +715,7 @@ export default class Parser {
"Expected group after '" + func + "'", nextToken);
}
}
let argNode;
let argNode: ParseNode;
arg = assertFuncOrArg(arg);
if (arg.type === "fn") {
const argGreediness =
@@ -753,10 +738,11 @@ export default class Parser {
/**
* Parses a group when the mode is changing.
*
* @return {?ParsedFuncOrArgOrDollar}
*/
parseGroupOfType(type, optional) {
parseGroupOfType(
type: ArgType, // Used to describe the mode in error messages.
optional: boolean,
): ?ParsedFuncOrArgOrDollar {
// Handle `original` argTypes
if (type === "original") {
type = this.mode;
@@ -786,12 +772,11 @@ export default class Parser {
/**
* Parses a group, essentially returning the string formed by the
* brace-enclosed tokens plus some position information.
*
* @param {string} modeName Used to describe the mode in error messages
* @param {boolean=} optional Whether the group is optional or required
* @return {?Token}
*/
parseStringGroup(modeName, optional) {
parseStringGroup(
modeName: ArgType, // Used to describe the mode in error messages.
optional: boolean,
): ?Token {
if (optional && this.nextToken.text !== "[") {
return null;
}
@@ -820,12 +805,11 @@ export default class Parser {
* Parses a group, essentially returning the string formed by the
* brace-enclosed tokens plus some position information, possibly
* with nested braces.
*
* @param {string} modeName Used to describe the mode in error messages
* @param {boolean=} optional Whether the group is optional or required
* @return {?Token}
*/
parseStringGroupWithBalancedBraces(modeName, optional) {
parseStringGroupWithBalancedBraces(
modeName: ArgType, // Used to describe the mode in error messages.
optional: boolean,
): ?Token {
if (optional && this.nextToken.text !== "[") {
return null;
}
@@ -866,12 +850,11 @@ export default class Parser {
* Parses a regex-delimited group: the largest sequence of tokens
* whose concatenated strings match `regex`. Returns the string
* formed by the tokens plus some position information.
*
* @param {RegExp} regex
* @param {string} modeName Used to describe the mode in error messages
* @return {Token}
*/
parseRegexGroup(regex, modeName) {
parseRegexGroup(
regex: RegExp,
modeName: string, // Used to describe the mode in error messages.
): Token {
const outerMode = this.mode;
this.mode = "text";
const firstToken = this.nextToken;
@@ -895,7 +878,7 @@ export default class Parser {
/**
* Parses a color description.
*/
parseColorGroup(optional) {
parseColorGroup(optional: boolean): ?ParsedArg {
const res = this.parseStringGroup("color", optional);
if (!res) {
return null;
@@ -910,7 +893,7 @@ export default class Parser {
/**
* Parses a url string.
*/
parseUrlGroup(optional) {
parseUrlGroup(optional: boolean): ?ParsedArg {
const res = this.parseStringGroupWithBalancedBraces("url", optional);
if (!res) {
return null;
@@ -927,7 +910,7 @@ export default class Parser {
/**
* Parses a size specification, consisting of magnitude and unit.
*/
parseSizeGroup(optional) {
parseSizeGroup(optional: boolean): ?ParsedArg {
let res;
if (!optional && this.nextToken.text !== "{") {
res = this.parseRegexGroup(
@@ -961,11 +944,8 @@ export default class Parser {
* bracket-enclosed group.
* If `mode` is present, switches to that mode while parsing the group,
* and switches back after.
*
* @param {boolean=} optional Whether the group is optional or required
* @return {?ParsedFuncOrArgOrDollar}
*/
parseGroup(optional, mode) {
parseGroup(optional?: boolean, mode?: Mode): ?ParsedFuncOrArgOrDollar {
const outerMode = this.mode;
const firstToken = this.nextToken;
// Try to parse an open brace
@@ -1010,11 +990,9 @@ export default class Parser {
* The result will simply replace multiple textord nodes with a single
* character in each value by a single textord node having multiple
* characters in its value. The representation is still ASCII source.
*
* @param {Array.<ParseNode>} group the nodes of this group,
* list will be moified in place
* The group will be modified in place.
*/
formLigatures(group) {
formLigatures(group: ParseNode[]) {
let n = group.length - 1;
for (let i = 0; i < n; ++i) {
const a = group[i];
@@ -1041,10 +1019,8 @@ export default class Parser {
/**
* Parse a single symbol out of the string. Here, we handle both the functions
* we have defined, as well as the single character symbols
*
* @return {?ParsedFuncOrArgOrDollar}
*/
parseSymbol() {
parseSymbol(): ?ParsedFuncOrArgOrDollar {
const nucleus = this.nextToken;
let text = nucleus.text;

View File

@@ -6,11 +6,13 @@
import utils from "./utils";
import type { MacroMap } from "./macros";
export type SettingsOptions = {
displayMode?: boolean;
throwOnError?: boolean;
errorColor?: string;
macros?: {[macroName: string]: string};
macros?: MacroMap;
colorIsTextColor?: boolean;
maxSize?: number;
};
@@ -29,7 +31,7 @@ class Settings {
displayMode: boolean;
throwOnError: boolean;
errorColor: string;
macros: {[macroName: string]: string};
macros: MacroMap;
colorIsTextColor: boolean;
maxSize: number;

View File

@@ -49,7 +49,7 @@ type EnvProps = {
* 2. requires all arguments except argType
* It is generated by `defineEnvironment()` below.
*/
type EnvSpec = {|
export type EnvSpec = {|
numArgs: number,
argTypes?: ArgType[],
greediness: number,

View File

@@ -1,14 +1,7 @@
// @flow
import {_environments} from "./defineEnvironment";
const environments = {
has(envName: string) {
return _environments.hasOwnProperty(envName);
},
get(envName: string) {
return _environments[envName];
},
};
const environments = _environments;
export default environments;

View File

@@ -48,7 +48,7 @@ function parseArray(
const body = [row];
const rowGaps = [];
while (true) { // eslint-disable-line no-constant-condition
let cell = parser.parseExpression(false, null);
let cell = parser.parseExpression(false, undefined);
cell = new ParseNode("ordgroup", cell, parser.mode);
if (style) {
cell = new ParseNode("styling", {
@@ -72,6 +72,9 @@ function parseArray(
break;
} else if (next === "\\\\" || next === "\\cr") {
const cr = parser.parseFunction();
if (!cr) {
throw new ParseError(`Failed to parse function after ${next}`);
}
rowGaps.push(cr.value.size);
row = [];
body.push(row);