diff --git a/src/ParseNode.js b/src/ParseNode.js index a78c0d2c..490eccb1 100644 --- a/src/ParseNode.js +++ b/src/ParseNode.js @@ -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); } } diff --git a/src/Parser.js b/src/Parser.js index 71b81418..d9da5c5b 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -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.} */ - 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 - * expression. - * - * @return {Array} + * `breakOnTokenText`: The text of the token that the expression should end + * with, or `null` if something else should end the + * expression. */ - 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} body - * @return {Array} */ - 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} args - * @param {Array} 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, - * optArgs: Array, - * }} */ - 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.} 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; diff --git a/src/Settings.js b/src/Settings.js index 23f10e29..79be336f 100644 --- a/src/Settings.js +++ b/src/Settings.js @@ -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; diff --git a/src/defineEnvironment.js b/src/defineEnvironment.js index 8890fe2b..aedcf9c7 100644 --- a/src/defineEnvironment.js +++ b/src/defineEnvironment.js @@ -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, diff --git a/src/environments.js b/src/environments.js index a6324b52..96fb4211 100644 --- a/src/environments.js +++ b/src/environments.js @@ -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; diff --git a/src/environments/array.js b/src/environments/array.js index 9de2b402..b1cdcabc 100644 --- a/src/environments/array.js +++ b/src/environments/array.js @@ -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);