From 19d2aa63c36d77ef2066e569032257d27cd2f554 Mon Sep 17 00:00:00 2001 From: Ashish Myles Date: Mon, 4 Jun 2018 10:56:51 -0400 Subject: [PATCH] Replace ParseNode<*> with a more accurate AnyParseNode and fix flow errors. (#1387) * Replace ParseNode<*> with a more accurate AnyParseNode and fix flow errors. * Allow "array" environment type spec to use any all symbol type. Before this commit, it was constrained to use "mathord" and "textord", but a recent change in HEAD resulted in a "rel" node being used in the spec for, e.g. \begin{array}{|l||c:r::}\end{array} * Address reviewer comments: rename `lastRow` to `row` in array.js. --- katex.js | 4 +- src/ParseError.js | 7 +- src/ParseNode.js | 198 +++++++++++++++++++++++++---------- src/Parser.js | 61 ++++++----- src/Settings.js | 8 +- src/buildTree.js | 6 +- src/defineEnvironment.js | 13 ++- src/defineFunction.js | 8 +- src/environments/array.js | 72 +++++++------ src/functions/accent.js | 3 +- src/functions/color.js | 6 +- src/functions/delimsizing.js | 14 +-- src/functions/environment.js | 4 +- src/functions/rule.js | 2 +- src/functions/smash.js | 4 +- src/functions/supsub.js | 15 +-- src/parseTree.js | 3 +- src/stretchy.js | 4 +- src/symbols.js | 17 ++- src/utils.js | 6 +- 20 files changed, 284 insertions(+), 171 deletions(-) diff --git a/katex.js b/katex.js index cb8cd494..d0957271 100644 --- a/katex.js +++ b/katex.js @@ -18,7 +18,7 @@ import domTree from "./src/domTree"; import utils from "./src/utils"; import type {SettingsOptions} from "./src/Settings"; -import type ParseNode from "./src/ParseNode"; +import type {AnyParseNode} from "./src/ParseNode"; import {defineSymbol} from './src/symbols'; import {defineMacro} from './src/macros'; @@ -70,7 +70,7 @@ const renderToString = function( const generateParseTree = function( expression: string, options: SettingsOptions, -): ParseNode<*>[] { +): AnyParseNode[] { const settings = new Settings(options); return parseTree(expression, settings); }; diff --git a/src/ParseError.js b/src/ParseError.js index 486e1555..8591665e 100644 --- a/src/ParseError.js +++ b/src/ParseError.js @@ -1,7 +1,8 @@ // @flow -import ParseNode from "./ParseNode"; import {Token} from "./Token"; +import type {AnyParseNode} from "./ParseNode"; + /** * This is the ParseError class, which is the main error thrown by KaTeX * functions when something has gone wrong. This is used to distinguish internal @@ -15,8 +16,8 @@ class ParseError { // Error position based on passed-in Token or ParseNode. constructor( - message: string, // The error message - token?: Token | ParseNode<*>, // An object providing position information + message: string, // The error message + token?: ?Token | AnyParseNode, // An object providing position information ) { let error = "KaTeX parse error: " + message; let start; diff --git a/src/ParseNode.js b/src/ParseNode.js index bcb10dd9..192bbe5d 100644 --- a/src/ParseNode.js +++ b/src/ParseNode.js @@ -1,9 +1,10 @@ // @flow import SourceLocation from "./SourceLocation"; -import type {ArrayEnvNodeData} from "./environments/array.js"; +import {GROUPS} from "./symbols"; +import type {ArrayEnvNodeData} from "./environments/array"; import type {Mode, StyleStr} from "./types"; -import type {Token} from "./Token.js"; -import type {Measurement} from "./units.js"; +import type {Token} from "./Token"; +import type {Measurement} from "./units"; /** * The resulting parse tree nodes of the parse tree. @@ -40,18 +41,83 @@ export type NodeValue = $ElementType; export type LeftRightDelimType = {| type: "leftright", - body: ParseNode<*>[], + body: AnyParseNode[], left: string, right: string, |}; +// ParseNode's corresponding to Symbol `Group`s in symbols.js. +export type SymbolParseNode = + ParseNode<"accent-token"> | + ParseNode<"bin"> | + ParseNode<"close"> | + ParseNode<"inner"> | + ParseNode<"mathord"> | + ParseNode<"op-token"> | + ParseNode<"open"> | + ParseNode<"punct"> | + ParseNode<"rel"> | + ParseNode<"spacing"> | + ParseNode<"textord">; + +// Union of all possible `ParseNode<>` types. +// Unable to derive this directly from `ParseNodeTypes` due to +// https://github.com/facebook/flow/issues/6369. +// Cannot use `ParseNode` since `ParseNode` is not strictly co-variant +// w.r.t. its type parameter due to the way the value type is computed. +export type AnyParseNode = + SymbolParseNode | + ParseNode<"array"> | + ParseNode<"color"> | + ParseNode<"color-token"> | + ParseNode<"op"> | + ParseNode<"ordgroup"> | + ParseNode<"size"> | + ParseNode<"styling"> | + ParseNode<"supsub"> | + ParseNode<"tag"> | + ParseNode<"text"> | + ParseNode<"url"> | + ParseNode<"verb"> | + ParseNode<"accent"> | + ParseNode<"accentUnder"> | + ParseNode<"cr"> | + ParseNode<"delimsizing"> | + ParseNode<"enclose"> | + ParseNode<"environment"> | + ParseNode<"font"> | + ParseNode<"genfrac"> | + ParseNode<"horizBrace"> | + ParseNode<"href"> | + ParseNode<"infix"> | + ParseNode<"kern"> | + ParseNode<"lap"> | + ParseNode<"leftright"> | + ParseNode<"leftright-right"> | + ParseNode<"mathchoice"> | + ParseNode<"middle"> | + ParseNode<"mclass"> | + ParseNode<"mod"> | + ParseNode<"operatorname"> | + ParseNode<"overline"> | + ParseNode<"phantom"> | + ParseNode<"hphantom"> | + ParseNode<"vphantom"> | + ParseNode<"raisebox"> | + ParseNode<"rule"> | + ParseNode<"sizing"> | + ParseNode<"smash"> | + ParseNode<"sqrt"> | + ParseNode<"underline"> | + ParseNode<"xArrow">; + // Map from `type` field value to corresponding `value` type. export type ParseNodeTypes = { "array": ArrayEnvNodeData, "color": {| type: "color", color: string, - value: ParseNode<*>[], + value: AnyParseNode[], |}, "color-token": string, // To avoid requiring run-time type assertions, this more carefully captures @@ -73,9 +139,9 @@ export type ParseNodeTypes = { suppressBaseShift?: boolean, symbol: false, // If 'symbol' is true, `body` *must* be set. body?: void, - value: ParseNode<*>[], + value: AnyParseNode[], |}, - "ordgroup": ParseNode<*>[], + "ordgroup": AnyParseNode[], "size": {| type: "size", value: Measurement, @@ -83,22 +149,22 @@ export type ParseNodeTypes = { "styling": {| type: "styling", style: StyleStr, - value: ParseNode<*>[], + value: AnyParseNode[], |}, "supsub": {| type: "supsub", - base: ?ParseNode<*>, - sup?: ?ParseNode<*>, - sub?: ?ParseNode<*>, + base: ?AnyParseNode, + sup?: ?AnyParseNode, + sub?: ?AnyParseNode, |}, "tag": {| type: "tag", - body: ParseNode<*>[], - tag: ParseNode<*>[], + body: AnyParseNode[], + tag: AnyParseNode[], |}, "text": {| type: "text", - body: ParseNode<*>[], + body: AnyParseNode[], font?: string, |}, "url": {| @@ -131,14 +197,14 @@ export type ParseNodeTypes = { label: string, isStretchy?: boolean, isShifty?: boolean, - base: ParseNode<*>, + base: AnyParseNode, |}, "accentUnder": {| type: "accentUnder", label: string, isStretchy?: boolean, isShifty?: boolean, - base: ParseNode<*>, + base: AnyParseNode, |}, "cr": {| type: "cr", @@ -157,23 +223,23 @@ export type ParseNodeTypes = { label: string, backgroundColor?: ParseNode<"color-token">, borderColor?: ParseNode<"color-token">, - body: ParseNode<*>, + body: AnyParseNode, |}, "environment": {| type: "environment", name: string, - nameGroup: ParseNode<*>, + nameGroup: AnyParseNode, |}, "font": {| type: "font", font: string, - body: ParseNode<*>, + body: AnyParseNode, |}, "genfrac": {| type: "genfrac", continued: boolean, - numer: ParseNode<*>, - denom: ParseNode<*>, + numer: AnyParseNode, + denom: AnyParseNode, hasBarLine: boolean, leftDelim: ?string, rightDelim: ?string, @@ -183,12 +249,12 @@ export type ParseNodeTypes = { type: "horizBrace", label: string, isOver: boolean, - base: ParseNode<*>, + base: AnyParseNode, |}, "href": {| type: "href", href: string, - body: ParseNode<*>[], + body: AnyParseNode[], |}, "infix": {| type: "infix", @@ -202,7 +268,7 @@ export type ParseNodeTypes = { "lap": {| type: "lap", alignment: string, - body: ParseNode<*>, + body: AnyParseNode, |}, "leftright": LeftRightDelimType, "leftright-right": {| @@ -211,10 +277,10 @@ export type ParseNodeTypes = { |}, "mathchoice": {| type: "mathchoice", - display: ParseNode<*>[], - text: ParseNode<*>[], - script: ParseNode<*>[], - scriptscript: ParseNode<*>[], + display: AnyParseNode[], + text: AnyParseNode[], + script: AnyParseNode[], + scriptscript: AnyParseNode[], |}, "middle": {| type: "middle", @@ -223,40 +289,40 @@ export type ParseNodeTypes = { "mclass": {| type: "mclass", mclass: string, - value: ParseNode<*>[], + value: AnyParseNode[], |}, "mod": {| type: "mod", modType: string, - value: ?ParseNode<*>[], + value: ?AnyParseNode[], |}, "operatorname": {| type: "operatorname", - value: ParseNode<*>[], + value: AnyParseNode[], |}, "overline": {| type: "overline", - body: ParseNode<*>, + body: AnyParseNode, |}, "phantom": {| type: "phantom", - value: ParseNode<*>[], + value: AnyParseNode[], |}, "hphantom": {| type: "hphantom", - body: ParseNode<*>, - value: ParseNode<*>[], + body: AnyParseNode, + value: AnyParseNode[], |}, "vphantom": {| type: "vphantom", - body: ParseNode<*>, - value: ParseNode<*>[], + body: AnyParseNode, + value: AnyParseNode[], |}, "raisebox": {| type: "raisebox", dy: ParseNode<"size">, - body: ParseNode<*>, - value: ParseNode<*>[], + body: AnyParseNode, + value: AnyParseNode[], |}, "rule": {| type: "rule", @@ -267,28 +333,28 @@ export type ParseNodeTypes = { "sizing": {| type: "sizing", size: number, - value: ParseNode<*>[], + value: AnyParseNode[], |}, "smash": {| type: "smash", - body: ParseNode<*>, + body: AnyParseNode, smashHeight: boolean, smashDepth: boolean, |}, "sqrt": {| type: "sqrt", - body: ParseNode<*>, - index: ?ParseNode<*>, + body: AnyParseNode, + index: ?AnyParseNode, |}, "underline": {| type: "underline", - body: ParseNode<*>, + body: AnyParseNode, |}, "xArrow": {| type: "xArrow", label: string, - body: ParseNode<*>, - below: ?ParseNode<*>, + body: AnyParseNode, + below: ?AnyParseNode, |}, }; @@ -297,8 +363,7 @@ export type ParseNodeTypes = { * typing. Throws if the node's type does not match. */ export function assertNodeType( - // The union allows either ParseNode<*> or the union of two specific nodes. - node: ?ParseNode<*> | ParseNode<*>, + node: ?AnyParseNode, type: NODETYPE, ): ParseNode { const typedNode = checkNodeType(node, type); @@ -315,11 +380,38 @@ export function assertNodeType( * returns null. */ export function checkNodeType( - // The union allows either ParseNode<*> or the union of two specific nodes. - node: ?ParseNode<*> | ParseNode<*>, + node: ?AnyParseNode, type: NODETYPE, ): ?ParseNode { - return node && node.type === type ? - (node: ParseNode) : - null; + if (node && node.type === type) { + // $FlowFixMe: Inference not sophisticated enough to figure this out. + return node; + } + return null; +} + +/** + * Returns the node more strictly typed iff it is of the given type. Otherwise, + * returns null. + */ +export function assertSymbolNodeType(node: ?AnyParseNode): SymbolParseNode { + const typedNode = checkSymbolNodeType(node); + if (!typedNode) { + throw new Error( + `Expected node of symbol group type, but got ` + + (node ? `node of type ${node.type}` : String(node))); + } + return typedNode; +} + +/** + * Returns the node more strictly typed iff it is of the given type. Otherwise, + * returns null. + */ +export function checkSymbolNodeType(node: ?AnyParseNode): ?SymbolParseNode { + if (node && GROUPS.hasOwnProperty(node.type)) { + // $FlowFixMe + return node; + } + return null; } diff --git a/src/Parser.js b/src/Parser.js index 3c18d343..0ac2a93f 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -9,12 +9,12 @@ import {validUnit} from "./units"; import {supportedCodepoint} from "./unicodeScripts"; import unicodeAccents from "./unicodeAccents"; import unicodeSymbols from "./unicodeSymbols"; -import ParseNode, {assertNodeType} from "./ParseNode"; +import ParseNode, {assertNodeType, checkNodeType} from "./ParseNode"; import ParseError from "./ParseError"; import {combiningDiacriticalMarksEndRegex} from "./Lexer.js"; import Settings from "./Settings"; import {Token} from "./Token"; - +import type {AnyParseNode} from "./ParseNode"; import type {Mode, ArgType, BreakToken} from "./types"; import type {FunctionContext, FunctionSpec} from "./defineFunction"; import type {EnvSpec} from "./defineEnvironment"; @@ -60,12 +60,12 @@ type ParsedFunc = {| |}; type ParsedArg = {| type: "arg", - result: ParseNode<*>, + result: AnyParseNode, token: Token, |}; type ParsedFuncOrArg = ParsedFunc | ParsedArg; -function newArgument(result: ParseNode<*>, token: Token): ParsedArg { +function newArgument(result: AnyParseNode, token: Token): ParsedArg { return {type: "arg", result, token}; } @@ -127,7 +127,7 @@ export default class Parser { /** * Main parsing function, which parses an entire input. */ - parse(): ParseNode<*>[] { + parse(): AnyParseNode[] { // Create a group namespace for the math expression. // (LaTeX creates a new group for every $...$, $$...$$, \[...\].) this.gullet.beginGroup(); @@ -167,7 +167,7 @@ export default class Parser { parseExpression( breakOnInfix: boolean, breakOnTokenText?: BreakToken, - ): ParseNode<*>[] { + ): AnyParseNode[] { 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) @@ -208,13 +208,13 @@ 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 {}. */ - handleInfixNodes(body: ParseNode<*>[]): ParseNode<*>[] { + handleInfixNodes(body: AnyParseNode[]): AnyParseNode[] { let overIndex = -1; let funcName; for (let i = 0; i < body.length; i++) { - const node = body[i]; - if (node.type === "infix") { + const node = checkNodeType(body[i], "infix"); + if (node) { if (overIndex !== -1) { throw new ParseError( "only one infix operator per group", @@ -259,7 +259,7 @@ export default class Parser { */ handleSupSubscript( name: string, // For error reporting. - ): ParseNode<*> { + ): AnyParseNode { const symbolToken = this.nextToken; const symbol = symbolToken.text; this.consume(); @@ -297,7 +297,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(): ParseNode<*> { + handleUnsupportedCmd(): AnyParseNode { const text = this.nextToken.text; const textordArray = []; @@ -329,7 +329,7 @@ export default class Parser { /** * Parses a group with optional super/subscripts. */ - parseAtom(breakOnTokenText?: BreakToken): ?ParseNode<*> { + 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.parseImplicitGroup(breakOnTokenText); @@ -352,14 +352,15 @@ export default class Parser { if (lex.text === "\\limits" || lex.text === "\\nolimits") { // We got a limit control - if (!base || base.type !== "op") { + const opNode = checkNodeType(base, "op"); + if (opNode) { + const limits = lex.text === "\\limits"; + opNode.value.limits = limits; + opNode.value.alwaysHandleSupSub = true; + } else { throw new ParseError( "Limit controls must follow a math operator", lex); - } else { - const limits = lex.text === "\\limits"; - base.value.limits = limits; - base.value.alwaysHandleSupSub = true; } this.consume(); } else if (lex.text === "^") { @@ -427,7 +428,7 @@ export default class Parser { * implicit grouping after it until the end of the group. E.g. * small text {\Large large text} small text again */ - parseImplicitGroup(breakOnTokenText?: BreakToken): ?ParseNode<*> { + parseImplicitGroup(breakOnTokenText?: BreakToken): ?AnyParseNode { const start = this.parseSymbol(); if (start == null) { @@ -463,10 +464,12 @@ export default class Parser { const result = env.handler(context, args, optArgs); this.expect("\\end", false); const endNameToken = this.nextToken; - const end = assertNodeType(this.parseFunction(), "environment"); + let end = this.parseFunction(); if (!end) { throw new ParseError("failed to parse function after \\end"); - } else if (end.value.name !== envName) { + } + end = assertNodeType(end, "environment"); + if (end.value.name !== envName) { throw new ParseError( "Mismatch: \\begin{" + envName + "} matched " + "by \\end{" + end.value.name + "}", @@ -483,7 +486,7 @@ 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. */ - parseFunction(): ?ParseNode<*> { + parseFunction(): ?AnyParseNode { const baseGroup = this.parseGroup(); return baseGroup ? this.parseGivenFunction(baseGroup) : null; } @@ -495,7 +498,7 @@ export default class Parser { parseGivenFunction( baseGroup: ParsedFuncOrArg, breakOnTokenText?: BreakToken, - ): ParseNode<*> { + ): AnyParseNode { if (baseGroup.type === "fn") { const func = baseGroup.result; const funcData = functions[func]; @@ -535,11 +538,11 @@ export default class Parser { */ callFunction( name: string, - args: ParseNode<*>[], - optArgs: (?ParseNode<*>)[], + args: AnyParseNode[], + optArgs: (?AnyParseNode)[], token?: Token, breakOnTokenText?: BreakToken, - ): ParseNode<*> { + ): AnyParseNode { const context: FunctionContext = { funcName: name, parser: this, @@ -561,8 +564,8 @@ export default class Parser { func: string, // Should look like "\name" or "\begin{name}". funcData: FunctionSpec<*> | EnvSpec<*>, ): { - args: ParseNode<*>[], - optArgs: (?ParseNode<*>)[], + args: AnyParseNode[], + optArgs: (?AnyParseNode)[], } { const totalArgs = funcData.numArgs + funcData.numOptionalArgs; if (totalArgs === 0) { @@ -609,7 +612,7 @@ export default class Parser { "Expected group after '" + func + "'", nextToken); } } - let argNode: ParseNode<*>; + let argNode: AnyParseNode; if (arg.type === "fn") { const argGreediness = functions[arg.result].greediness; @@ -895,7 +898,7 @@ export default class Parser { * characters in its value. The representation is still ASCII source. * The group will be modified in place. */ - formLigatures(group: ParseNode<*>[]) { + formLigatures(group: AnyParseNode[]) { let n = group.length - 1; for (let i = 0; i < n; ++i) { const a = group[i]; diff --git a/src/Settings.js b/src/Settings.js index a024248d..493e9eee 100644 --- a/src/Settings.js +++ b/src/Settings.js @@ -7,13 +7,13 @@ import utils from "./utils"; import ParseError from "./ParseError.js"; -import ParseNode from "./ParseNode"; import {Token} from "./Token"; +import type {AnyParseNode} from "./ParseNode"; import type {MacroMap} from "./macros"; export type StrictFunction = - (errorCode: string, errorMsg: string, token?: Token | ParseNode<*>) => + (errorCode: string, errorMsg: string, token?: Token | AnyParseNode) => ?(boolean | string); export type SettingsOptions = { @@ -65,7 +65,7 @@ class Settings { * Can safely not be called if `this.strict` is false in JavaScript. */ reportNonstrict(errorCode: string, errorMsg: string, - token?: Token | ParseNode<*>) { + token?: Token | AnyParseNode) { let strict = this.strict; if (typeof strict === "function") { // Allow return value of strict function to be boolean or string @@ -98,7 +98,7 @@ class Settings { * This is for the second category of `errorCode`s listed in the README. */ useStrictBehavior(errorCode: string, errorMsg: string, - token?: Token | ParseNode<*>) { + token?: Token | AnyParseNode) { let strict = this.strict; if (typeof strict === "function") { // Allow return value of strict function to be boolean or string diff --git a/src/buildTree.js b/src/buildTree.js index a08b3b39..92a8540d 100644 --- a/src/buildTree.js +++ b/src/buildTree.js @@ -6,7 +6,7 @@ import Options from "./Options"; import Settings from "./Settings"; import Style from "./Style"; -import type ParseNode from "./ParseNode"; +import type {AnyParseNode} from "./ParseNode"; import type {DomSpan} from "./domTree"; const optionsFromSettings = function(settings: Settings) { @@ -17,7 +17,7 @@ const optionsFromSettings = function(settings: Settings) { }; export const buildTree = function( - tree: ParseNode<*>[], + tree: AnyParseNode[], expression: string, settings: Settings, ): DomSpan { @@ -39,7 +39,7 @@ export const buildTree = function( }; export const buildHTMLTree = function( - tree: ParseNode<*>[], + tree: AnyParseNode[], expression: string, settings: Settings, ): DomSpan { diff --git a/src/defineEnvironment.js b/src/defineEnvironment.js index 891334d3..54a7f6f9 100644 --- a/src/defineEnvironment.js +++ b/src/defineEnvironment.js @@ -1,14 +1,13 @@ // @flow import {_htmlGroupBuilders, _mathmlGroupBuilders} from "./defineFunction"; -import Options from "./Options"; import ParseNode from "./ParseNode"; import type Parser from "./Parser"; +import type {AnyParseNode} from "./ParseNode"; import type {ArgType, Mode} from "./types"; -import type {HtmlDomNode} from "./domTree"; import type {NodeType} from "./ParseNode"; -import type {MathNode} from "./mathMLTree"; +import type {HtmlBuilder, MathMLBuilder} from "./defineFunction"; /** * The context contains the following properties: @@ -29,8 +28,8 @@ type EnvContext = {| */ type EnvHandler = ( context: EnvContext, - args: ParseNode<*>[], - optArgs: (?ParseNode<*>)[], + args: AnyParseNode[], + optArgs: (?AnyParseNode)[], ) => ParseNode; /** @@ -85,11 +84,11 @@ type EnvDefSpec = {| // This function returns an object representing the DOM structure to be // created when rendering the defined LaTeX function. - htmlBuilder: (group: ParseNode, options: Options) => HtmlDomNode, + htmlBuilder: HtmlBuilder, // This function returns an object representing the MathML structure to be // created when rendering the defined LaTeX function. - mathmlBuilder: (group: ParseNode, options: Options) => MathNode, + mathmlBuilder: MathMLBuilder, |}; export default function defineEnvironment({ diff --git a/src/defineFunction.js b/src/defineFunction.js index ae547e51..8a8da209 100644 --- a/src/defineFunction.js +++ b/src/defineFunction.js @@ -3,7 +3,7 @@ import {checkNodeType} from "./ParseNode"; import domTree from "./domTree"; import type Parser from "./Parser"; -import type ParseNode, {NodeType} from "./ParseNode"; +import type ParseNode, {AnyParseNode, NodeType} from "./ParseNode"; import type Options from "./Options"; import type {ArgType, BreakToken, Mode} from "./types"; import type {HtmlDomNode} from "./domTree"; @@ -20,8 +20,8 @@ export type FunctionContext = {| export type FunctionHandler = ( context: FunctionContext, - args: ParseNode<*>[], - optArgs: (?ParseNode<*>)[], + args: AnyParseNode[], + optArgs: (?AnyParseNode)[], ) => ParseNode; export type HtmlBuilder = (ParseNode, Options) => HtmlDomNode; @@ -245,7 +245,7 @@ export function defineFunctionBuilders({ // Since the corresponding buildHTML/buildMathML function expects a // list of elements, we normalize for different kinds of arguments -export const ordargument = function(arg: ParseNode<*>): ParseNode<*>[] { +export const ordargument = function(arg: AnyParseNode): AnyParseNode[] { const node = checkNodeType(arg, "ordgroup"); return node ? node.value : [arg]; }; diff --git a/src/environments/array.js b/src/environments/array.js index c7891258..27b91361 100644 --- a/src/environments/array.js +++ b/src/environments/array.js @@ -4,7 +4,8 @@ import defineEnvironment from "../defineEnvironment"; import mathMLTree from "../mathMLTree"; import ParseError from "../ParseError"; import ParseNode from "../ParseNode"; -import {assertNodeType} from "../ParseNode"; +import {assertNodeType, assertSymbolNodeType} from "../ParseNode"; +import {checkNodeType, checkSymbolNodeType} from "../ParseNode"; import {calculateSize} from "../units"; import utils from "../utils"; @@ -12,7 +13,9 @@ import * as html from "../buildHTML"; import * as mml from "../buildMathML"; import type Parser from "../Parser"; +import type {AnyParseNode} from "../ParseNode"; import type {StyleStr} from "../types"; +import type {HtmlBuilder, MathMLBuilder} from "../defineFunction"; // Data stored in the ParseNode associated with the environment. type AlignSpec = { type: "separator", separator: string } | { @@ -28,8 +31,8 @@ export type ArrayEnvNodeData = {| arraystretch: number, addJot?: boolean, cols?: AlignSpec[], - body: ParseNode<*>[][], // List of rows in the (2D) array. - rowGaps: (?ParseNode<*>)[], + body: AnyParseNode[][], // List of rows in the (2D) array. + rowGaps: (?ParseNode<"size">)[], numHLinesBeforeRow: number[], |}; // Same as above but with some fields not yet filled. @@ -40,8 +43,8 @@ type ArrayEnvNodeDataIncomplete = {| addJot?: boolean, cols?: AlignSpec[], // Before these fields are filled. - body?: ParseNode<*>[][], - rowGaps?: (?ParseNode<*>)[], + body?: AnyParseNode[][], + rowGaps?: (?ParseNode<"size">)[], numHLinesBeforeRow?: number[], |}; @@ -110,10 +113,8 @@ function parseArray( } else if (next === "\\end") { // Arrays terminate newlines with `\crcr` which consumes a `\cr` if // the last line is empty. - const lastRow = body[body.length - 1]; - if (body.length > 1 - && lastRow.length === 1 - && lastRow[0].value.value[0].value.length === 0) { + // NOTE: Currently, `cell` is the last item added into `row`. + if (row.length === 1 && cell.value.value[0].value.length === 0) { body.pop(); } break; @@ -161,8 +162,7 @@ type Outrow = { pos: number, }; -const htmlBuilder = function(group, options) { - const groupValue = assertNodeType(group, "array").value; +const htmlBuilder: HtmlBuilder<"array"> = function(group, options) { let r; let c; const nr = group.value.body.length; @@ -180,7 +180,7 @@ const htmlBuilder = function(group, options) { // Default \jot from ltmath.dtx // TODO(edemaine): allow overriding \jot via \setlength (#687) const jot = 3 * pt; - const arrayskip = groupValue.arraystretch * baselineskip; + const arrayskip = group.value.arraystretch * baselineskip; const arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and const arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx @@ -194,8 +194,8 @@ const htmlBuilder = function(group, options) { hlinePos.push(totalHeight); } - for (r = 0; r < groupValue.body.length; ++r) { - const inrow = groupValue.body[r]; + for (r = 0; r < group.value.body.length; ++r) { + const inrow = group.value.body[r]; let height = arstrutHeight; // \@array adds an \@arstrut let depth = arstrutDepth; // to each tow (via the template) @@ -215,7 +215,7 @@ const htmlBuilder = function(group, options) { outrow[c] = elt; } - const rowGap = groupValue.rowGaps[r]; + const rowGap = group.value.rowGaps[r]; let gap = 0; if (rowGap) { gap = calculateSize(rowGap.value.value, options); @@ -230,7 +230,7 @@ const htmlBuilder = function(group, options) { // In AMS multiline environments such as aligned and gathered, rows // correspond to lines that have additional \jot added to the // \baselineskip via \openup. - if (groupValue.addJot) { + if (group.value.addJot) { depth += jot; } @@ -251,7 +251,7 @@ const htmlBuilder = function(group, options) { } const offset = totalHeight / 2 + options.fontMetrics().axisHeight; - const colDescriptions = groupValue.cols || []; + const colDescriptions = group.value.cols || []; const cols = []; let colSep; let colDescrNum; @@ -307,7 +307,7 @@ const htmlBuilder = function(group, options) { } let sepwidth; - if (c > 0 || groupValue.hskipBeforeAndAfter) { + if (c > 0 || group.value.hskipBeforeAndAfter) { sepwidth = utils.deflt(colDescr.pregap, arraycolsep); if (sepwidth !== 0) { colSep = buildCommon.makeSpan(["arraycolsep"], []); @@ -338,7 +338,7 @@ const htmlBuilder = function(group, options) { [col]); cols.push(col); - if (c < nc - 1 || groupValue.hskipBeforeAndAfter) { + if (c < nc - 1 || group.value.hskipBeforeAndAfter) { sepwidth = utils.deflt(colDescr.postgap, arraycolsep); if (sepwidth !== 0) { colSep = buildCommon.makeSpan(["arraycolsep"], []); @@ -366,10 +366,9 @@ const htmlBuilder = function(group, options) { return buildCommon.makeSpan(["mord"], [body], options); }; -const mathmlBuilder = function(group, options) { - const groupValue = assertNodeType(group, "array").value; +const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) { return new mathMLTree.MathNode( - "mtable", groupValue.body.map(function(row) { + "mtable", group.value.body.map(function(row) { return new mathMLTree.MathNode( "mtr", row.map(function(cell) { return new mathMLTree.MathNode( @@ -400,10 +399,12 @@ const alignedHandler = function(context, args) { let numMaths; let numCols = 0; const emptyGroup = new ParseNode("ordgroup", [], context.mode); - if (args[0] && args[0].value) { + const ordgroup = checkNodeType(args[0], "ordgroup"); + if (ordgroup) { let arg0 = ""; - for (let i = 0; i < args[0].value.length; i++) { - arg0 += args[0].value[i].value; + for (let i = 0; i < ordgroup.value.length; i++) { + const textord = assertNodeType(ordgroup.value[i], "textord"); + arg0 += textord.value; } numMaths = Number(arg0); numCols = numMaths * 2; @@ -412,7 +413,8 @@ const alignedHandler = function(context, args) { res.value.body.forEach(function(row) { for (let i = 1; i < row.length; i += 2) { // Modify ordgroup node within styling node - const ordgroup = row[i].value.value[0]; + const styling = assertNodeType(row[i], "styling"); + const ordgroup = assertNodeType(styling.value.value[0], "ordgroup"); ordgroup.value.unshift(emptyGroup); } if (!isAligned) { // Case 1 @@ -459,10 +461,16 @@ defineEnvironment({ props: { numArgs: 1, }, - handler: function(context, args) { - let colalign = args[0]; - colalign = colalign.value.map ? colalign.value : [colalign]; - const cols = colalign.map(function(node) { + handler(context, args) { + // Since no types are specified above, the two possibilities are + // - The argument is wrapped in {} or [], in which case Parser's + // parseGroup() returns an "ordgroup" wrapping some symbol node. + // - The argument is a bare symbol node. + const symNode = checkSymbolNodeType(args[0]); + const colalign: AnyParseNode[] = + symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").value; + const cols = colalign.map(function(nde) { + const node = assertSymbolNodeType(nde); const ca = node.value; if ("lcr".indexOf(ca) !== -1) { return { @@ -480,9 +488,7 @@ defineEnvironment({ separator: ":", }; } - throw new ParseError( - "Unknown column alignment: " + node.value, - node); + throw new ParseError("Unknown column alignment: " + ca, nde); }); let res = { type: "array", diff --git a/src/functions/accent.js b/src/functions/accent.js index 4b702257..71a3f657 100644 --- a/src/functions/accent.js +++ b/src/functions/accent.js @@ -9,13 +9,14 @@ import ParseNode, {assertNodeType, checkNodeType} from "../ParseNode"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; +import type {AnyParseNode} from "../ParseNode"; import type {HtmlBuilderSupSub, MathMLBuilder} from "../defineFunction"; // NOTE: Unlike most `htmlBuilder`s, this one handles not only "accent", but // also "supsub" since an accent can affect super/subscripting. export const htmlBuilder: HtmlBuilderSupSub<"accent"> = (grp, options) => { // Accents are handled in the TeXbook pg. 443, rule 12. - let base: ParseNode<*>; + let base: AnyParseNode; let group: ParseNode<"accent">; const supSub = checkNodeType(grp, "supsub"); diff --git a/src/functions/color.js b/src/functions/color.js index 6b328192..431f086a 100644 --- a/src/functions/color.js +++ b/src/functions/color.js @@ -2,7 +2,6 @@ import defineFunction, {ordargument} from "../defineFunction"; import buildCommon from "../buildCommon"; import mathMLTree from "../mathMLTree"; -import ParseError from "../ParseError"; import ParseNode, {assertNodeType} from "../ParseNode"; import * as html from "../buildHTML"; @@ -99,10 +98,7 @@ defineFunction({ argTypes: ["color"], }, handler({parser, breakOnTokenText}, args) { - const color = args[0]; - if (!color) { - throw new ParseError("\\color not followed by color"); - } + const color = assertNodeType(args[0], "color-token"); // If we see a styling function, parse out the implicit body const body = parser.parseExpression(true, breakOnTokenText); diff --git a/src/functions/delimsizing.js b/src/functions/delimsizing.js index 415a7731..5390ee25 100644 --- a/src/functions/delimsizing.js +++ b/src/functions/delimsizing.js @@ -5,11 +5,12 @@ import delimiter from "../delimiter"; import mathMLTree from "../mathMLTree"; import ParseError from "../ParseError"; import utils from "../utils"; -import ParseNode, {assertNodeType} from "../ParseNode"; +import ParseNode, {assertNodeType, checkSymbolNodeType} from "../ParseNode"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; +import type {AnyParseNode, SymbolParseNode} from "../ParseNode"; import type {LeftRightDelimType} from "../ParseNode"; import type {FunctionContext} from "../defineFunction"; @@ -52,14 +53,15 @@ const delimiters = [ // Delimiter functions function checkDelimiter( - delim: ParseNode<*>, + delim: AnyParseNode, context: FunctionContext, -): ParseNode<*> { - if (utils.contains(delimiters, delim.value)) { - return delim; +): SymbolParseNode { + const symDelim = checkSymbolNodeType(delim); + if (symDelim && utils.contains(delimiters, symDelim.value)) { + return symDelim; } else { throw new ParseError( - "Invalid delimiter: '" + delim.value + "' after '" + + "Invalid delimiter: '" + String(delim.value) + "' after '" + context.funcName + "'", delim); } } diff --git a/src/functions/environment.js b/src/functions/environment.js index dcc5c146..e7000276 100644 --- a/src/functions/environment.js +++ b/src/functions/environment.js @@ -1,7 +1,7 @@ // @flow import defineFunction from "../defineFunction"; import ParseError from "../ParseError"; -import ParseNode from "../ParseNode"; +import ParseNode, {assertNodeType} from "../ParseNode"; // Environment delimiters. HTML/MathML rendering is defined in the corresponding // defineEnvironment definitions. @@ -19,7 +19,7 @@ defineFunction({ } let name = ""; for (let i = 0; i < nameGroup.value.length; ++i) { - name += nameGroup.value[i].value; + name += assertNodeType(nameGroup.value[i], "textord").value; } return new ParseNode("environment", { type: "environment", diff --git a/src/functions/rule.js b/src/functions/rule.js index b42f3afe..1ff7324e 100644 --- a/src/functions/rule.js +++ b/src/functions/rule.js @@ -19,7 +19,7 @@ defineFunction({ const height = assertNodeType(args[1], "size"); return new ParseNode("rule", { type: "rule", - shift: shift && shift.value, + shift: shift && assertNodeType(shift, "size").value.value, width: width.value.value, height: height.value.value, }, parser.mode); diff --git a/src/functions/smash.js b/src/functions/smash.js index 0053995f..4b7b1cdd 100644 --- a/src/functions/smash.js +++ b/src/functions/smash.js @@ -3,7 +3,7 @@ import defineFunction from "../defineFunction"; import buildCommon from "../buildCommon"; import mathMLTree from "../mathMLTree"; -import ParseNode from "../ParseNode"; +import ParseNode, {assertNodeType} from "../ParseNode"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; @@ -19,7 +19,7 @@ defineFunction({ handler: ({parser}, args, optArgs) => { let smashHeight = false; let smashDepth = false; - const tbArg = optArgs[0]; + const tbArg = optArgs[0] && assertNodeType(optArgs[0], "ordgroup"); if (tbArg) { // Optional [tb] argument is engaged. // ref: amsmath: \renewcommand{\smash}[1][tb]{% diff --git a/src/functions/supsub.js b/src/functions/supsub.js index c0d6ef15..acd69665 100644 --- a/src/functions/supsub.js +++ b/src/functions/supsub.js @@ -5,6 +5,7 @@ import domTree from "../domTree"; import mathMLTree from "../mathMLTree"; import utils from "../utils"; import Style from "../Style"; +import {checkNodeType} from "../ParseNode"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; @@ -184,13 +185,13 @@ defineFunctionBuilders({ let isBrace = false; let isOver; let isSup; - if (group.value.base) { - if (group.value.base.value.type === "horizBrace") { - isSup = (group.value.sup ? true : false); - if (isSup === group.value.base.value.isOver) { - isBrace = true; - isOver = group.value.base.value.isOver; - } + + const horizBrace = checkNodeType(group.value.base, "horizBrace"); + if (horizBrace) { + isSup = !!group.value.sup; + if (isSup === horizBrace.value.isOver) { + isBrace = true; + isOver = horizBrace.value.isOver; } } diff --git a/src/parseTree.js b/src/parseTree.js index 00db328b..7e5d641d 100644 --- a/src/parseTree.js +++ b/src/parseTree.js @@ -9,11 +9,12 @@ import ParseError from "./ParseError"; import ParseNode from "./ParseNode"; import type Settings from "./Settings"; +import type {AnyParseNode} from "./ParseNode"; /** * Parses an expression using a Parser, then returns the parsed result. */ -const parseTree = function(toParse: string, settings: Settings): ParseNode<*>[] { +const parseTree = function(toParse: string, settings: Settings): AnyParseNode[] { if (!(typeof toParse === 'string' || toParse instanceof String)) { throw new TypeError('KaTeX can only parse string typed expression'); } diff --git a/src/stretchy.js b/src/stretchy.js index 83fcc1a1..e86abc54 100644 --- a/src/stretchy.js +++ b/src/stretchy.js @@ -11,7 +11,7 @@ import mathMLTree from "./mathMLTree"; import utils from "./utils"; import type Options from "./Options"; -import type ParseNode from "./ParseNode"; +import type ParseNode, {AnyParseNode} from "./ParseNode"; import type {DomSpan, SvgSpan} from "./domTree"; const stretchyCodePoint: {[string]: string} = { @@ -159,7 +159,7 @@ const katexImagesData: { "shortrightharpoonabovebar"], 1.75, 716], }; -const groupLength = function(arg: ParseNode<*>): number { +const groupLength = function(arg: AnyParseNode): number { if (arg.type === "ordgroup") { return arg.value.length; } else { diff --git a/src/symbols.js b/src/symbols.js index c8bb9581..c4b5c6db 100644 --- a/src/symbols.js +++ b/src/symbols.js @@ -24,9 +24,20 @@ type Font = "main" | "ams" // types for raw text tokens, and we want to avoid conflicts with higher-level // `ParseNode` types. These `ParseNode`s are constructed within `Parser` by // looking up the `symbols` map. -export type Group = - "accent-token" | "bin" | "close" | "inner" | "mathord" | - "op-token" | "open" | "punct" | "rel" | "spacing" | "textord"; +export const GROUPS = { // Set of all the groups. + "accent-token": 1, + "bin": 1, + "close": 1, + "inner": 1, + "mathord": 1, + "op-token": 1, + "open": 1, + "punct": 1, + "rel": 1, + "spacing": 1, + "textord": 1, +}; +export type Group = $Keys; type CharInfoMap = {[string]: {font: Font, group: Group, replace: ?string}}; const symbols: {[Mode]: CharInfoMap} = { diff --git a/src/utils.js b/src/utils.js index 368500c4..5fa423ed 100644 --- a/src/utils.js +++ b/src/utils.js @@ -4,7 +4,7 @@ * files. */ -import type ParseNode from "./ParseNode"; +import type {AnyParseNode} from "./ParseNode"; /** * Provide an `indexOf` function which works in IE8, but defers to native if @@ -96,7 +96,7 @@ function clearNode(node: Node) { * cases, this will just be the group itself, but when ordgroups and colors have * a single element, we want to pull that out. */ -const getBaseElem = function(group: ParseNode<*>): ParseNode<*> { +const getBaseElem = function(group: AnyParseNode): AnyParseNode { if (group.type === "ordgroup") { if (group.value.length === 1) { return getBaseElem(group.value[0]); @@ -121,7 +121,7 @@ const getBaseElem = function(group: ParseNode<*>): ParseNode<*> { * with a single character in them. To decide if something is a character box, * we find its innermost group, and see if it is a single character. */ -const isCharacterBox = function(group: ParseNode<*>): boolean { +const isCharacterBox = function(group: AnyParseNode): boolean { const baseElem = getBaseElem(group); // These are all they types of groups which hold single characters