diff --git a/katex.js b/katex.js index 89073210..a68b934e 100644 --- a/katex.js +++ b/katex.js @@ -69,7 +69,7 @@ const renderToString = function( const generateParseTree = function( expression: string, options: SettingsOptions, -): ParseNode[] { +): ParseNode<*>[] { const settings = new Settings(options); return parseTree(expression, settings); }; diff --git a/src/ParseError.js b/src/ParseError.js index 0b33b817..486e1555 100644 --- a/src/ParseError.js +++ b/src/ParseError.js @@ -15,8 +15,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 | ParseNode<*>, // An object providing position information ) { let error = "KaTeX parse error: " + message; let start; diff --git a/src/ParseNode.js b/src/ParseNode.js index 490eccb1..114b9c58 100644 --- a/src/ParseNode.js +++ b/src/ParseNode.js @@ -1,6 +1,9 @@ // @flow import SourceLocation from "./SourceLocation"; -import type {Mode} from "./types"; +import type {ArrayEnvNodeData} from "./environments/array.js"; +import type {Mode, StyleStr} from "./types"; +import type {Token} from "./Token.js"; +import type {Measurement} from "./units.js"; /** * The resulting parse tree nodes of the parse tree. @@ -10,15 +13,15 @@ import type {Mode} from "./types"; * For details on the corresponding properties see `Token` constructor. * Providing such information can lead to better error reporting. */ -export default class ParseNode { - type: *; - value: *; +export default class ParseNode { + type: TYPE; + value: NodeValue; mode: Mode; loc: ?SourceLocation; constructor( - type: string, // type of node, like e.g. "ordgroup" - value: mixed, // type-specific representation of the node + type: TYPE, // type of node, like e.g. "ordgroup" + value: NodeValue, // type-specific representation of the node mode: Mode, // parse mode in action for this node, "math" or "text" first?: {loc: ?SourceLocation}, // first token or node of the input for // this node, will omit position information if unset @@ -31,3 +34,239 @@ export default class ParseNode { this.loc = SourceLocation.range(first, last); } } + +export type NodeType = $Keys; +export type NodeValue = $ElementType; + +export type AccentStructType = {| + type: "accent", + label: string, + isStretchy: boolean, + isShifty: boolean, + base: ParseNode<*>, +|}; + +// Map from `type` field value to corresponding `value` type. +type ParseNodeTypes = { + "array": ArrayEnvNodeData, + "accent": AccentStructType, + "color": {| + type: "color", + color: string, + value: ParseNode<*>[], + |}, + "leftright": {| + body: [{| + type: "array", + hskipBeforeAndAfter: boolean, + |} | ParseNode<*>], + left: string, + right: string, + |}, + "op": {| + type: "op", + limits: boolean, + symbol: boolean, + alwaysHandleSupSub?: boolean, + body?: string, + value?: ParseNode<*>[], + |}, + "ordgroup": ParseNode<*>[], + "size": {| + number: number, + unit: string, + |}, + "styling": {| + type: "styling", + style: StyleStr, + value: ParseNode<*>[], + |}, + "supsub": {| + base: ?ParseNode<*>, + sup?: ?ParseNode<*>, + sub?: ?ParseNode<*>, + |}, + "text": {| + type: "text", + body: ParseNode<*>[], + font?: string, + |}, + "textord": string, + "url": string, + "verb": {| + body: string, + star: boolean, + |}, + // From symbol groups, constructed in Parser.js via `symbols` lookup. + // (Some of these have "-token" suffix to distinguish them from existing + // `ParseNode` types.) + "accent-token": string, + "bin": string, + "close": string, + "inner": string, + "mathord": string, + "op-token": string, + "open": string, + "punct": string, + "rel": string, + "spacing": string, + "textord": string, + // From functions.js and functions/*.js. See also "accent", "color", "op", + // "styling", and "text" above. + "accentUnder": {| + type: "accentUnder", + label: string, + base: ParseNode<*>, + |}, + "cr": {| + type: "cr", + size: ?ParseNode<*>, + |}, + "delimsizing": {| + type: "delimsizing", + size: 1 | 2 | 3 | 4, + mclass: "mopen" | "mclose" | "mrel" | "mord", + value: string, + |}, + "enclose": {| + type: "enclose", + label: string, + backgroundColor?: ParseNode<*>, + borderColor?: ParseNode<*>, + body: ParseNode<*>, + |}, + "environment": {| + type: "environment", + name: string, + nameGroup: ParseNode<*>, + |}, + "font": {| + type: "font", + font: string, + body: ParseNode<*>, + |}, + "genfrac": {| + type: "genfrac", + numer: ParseNode<*>, + denom: ParseNode<*>, + hasBarLine: boolean, + leftDelim: ?string, + rightDelim: ?string, + size: StyleStr | "auto", + |}, + "horizBrace": {| + type: "horizBrace", + label: string, + isOver: boolean, + base: ParseNode<*>, + |}, + "href": {| + type: "href", + href: string, + body: ParseNode<*>[], + |}, + "infix": {| + type: "infix", + replaceWith: string, + token: ?Token, + |}, + "kern": {| + type: "kern", + dimension: Measurement, + |}, + "lap": {| + type: "lap", + alignment: string, + body: ParseNode<*>, + |}, + "leftright": {| + type?: "leftright", + body?: ParseNode<*>[], + left: string, + right: string, + |} | {| + type: "leftright", + value: string, + |}, + "mathchoice": {| + type: "mathchoice", + display: ParseNode<*>[], + text: ParseNode<*>[], + script: ParseNode<*>[], + scriptscript: ParseNode<*>[], + |}, + "middle": {| + type: "middle", + value: string, + |}, + "mclass": {| + type: "mclass", + mclass: string, + value: ParseNode<*>[], + |}, + "mod": {| + type: "mod", + modType: string, + value: ?ParseNode<*>[], + |}, + "operatorname": {| + type: "operatorname", + value: ParseNode<*>[], + |}, + "overline": {| + type: "overline", + body: ParseNode<*>, + |}, + "phantom": {| + type: "phantom", + value: ParseNode<*>[], + |}, + "hphantom": {| + type: "hphantom", + body: ParseNode<*>, + value: ParseNode<*>[], + |}, + "vphantom": {| + type: "vphantom", + body: ParseNode<*>, + value: ParseNode<*>[], + |}, + "raisebox": {| + type: "raisebox", + dy: ParseNode<*>, + body: ParseNode<*>, + value: ParseNode<*>[], + |}, + "rule": {| + type: "rule", + shift: ?Measurement, + width: ParseNode<*>, + height: ParseNode<*>, + |}, + "sizing": {| + type: "sizing", + size: number, + value: ParseNode<*>[], + |}, + "smash": {| + type: "smash", + body: ParseNode<*>, + smashHeight: boolean, + smashDepth: boolean, + |}, + "sqrt": {| + type: "sqrt", + body: ParseNode<*>, + index: ?ParseNode<*>, + |}, + "underline": {| + type: "underline", + body: ParseNode<*>, + |}, + "xArrow": {| + type: "xArrow", + label: string, + body: ParseNode<*>, + below: ?ParseNode<*>, + |}, +}; diff --git a/src/Parser.js b/src/Parser.js index 5b5bdf32..c2951788 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -60,12 +60,12 @@ type ParsedFunc = {| |}; type ParsedArg = {| type: "arg", - result: ParseNode, + result: ParseNode<*>, token: Token, |}; type ParsedFuncOrArg = ParsedFunc | ParsedArg; -function newArgument(result: ParseNode, token: Token): ParsedArg { +function newArgument(result: ParseNode<*>, token: Token): ParsedArg { return {type: "arg", result, token}; } @@ -132,7 +132,7 @@ export default class Parser { /** * Main parsing function, which parses an entire input. */ - parse(): ParseNode[] { + parse(): ParseNode<*>[] { // Try to parse the input this.consume(); const parse = this.parseInput(); @@ -142,7 +142,7 @@ export default class Parser { /** * Parses an entire input tree. */ - parseInput(): ParseNode[] { + parseInput(): ParseNode<*>[] { // Parse an expression const expression = this.parseExpression(false); // If we succeeded, make sure there's an EOF at the end @@ -166,7 +166,7 @@ export default class Parser { parseExpression( breakOnInfix: boolean, breakOnTokenText?: BreakToken, - ): ParseNode[] { + ): 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) @@ -207,7 +207,7 @@ 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: ParseNode<*>[]): ParseNode<*>[] { let overIndex = -1; let funcName; @@ -258,7 +258,7 @@ export default class Parser { */ handleSupSubscript( name: string, // For error reporting. - ): ParseNode { + ): ParseNode<*> { const symbolToken = this.nextToken; const symbol = symbolToken.text; this.consume(); @@ -296,7 +296,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(): ParseNode<*> { const text = this.nextToken.text; const textordArray = []; @@ -328,7 +328,7 @@ export default class Parser { /** * Parses a group with optional super/subscripts. */ - parseAtom(breakOnTokenText?: BreakToken): ?ParseNode { + parseAtom(breakOnTokenText?: BreakToken): ?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); @@ -402,6 +402,8 @@ export default class Parser { } } + // Base must be set if superscript or subscript are set per logic above, + // but need to check here for type check to pass. if (superscript || subscript) { // If we got either a superscript or subscript, create a supsub return new ParseNode("supsub", { @@ -423,7 +425,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): ?ParseNode<*> { const start = this.parseSymbol(); if (start == null) { @@ -477,7 +479,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(): ?ParseNode<*> { const baseGroup = this.parseGroup(); return baseGroup ? this.parseGivenFunction(baseGroup) : null; } @@ -489,7 +491,7 @@ export default class Parser { parseGivenFunction( baseGroup: ParsedFuncOrArg, breakOnTokenText?: BreakToken, - ): ParseNode { + ): ParseNode<*> { if (baseGroup.type === "fn") { const func = baseGroup.result; const funcData = functions[func]; @@ -530,8 +532,8 @@ export default class Parser { */ callFunction( name: string, - args: ParseNode[], - optArgs: (?ParseNode)[], + args: ParseNode<*>[], + optArgs: (?ParseNode<*>)[], token?: Token, breakOnTokenText?: BreakToken, ): * { @@ -554,10 +556,10 @@ export default class Parser { */ parseArguments( func: string, // Should look like "\name" or "\begin{name}". - funcData: FunctionSpec | EnvSpec, + funcData: FunctionSpec<*> | EnvSpec, ): { - args: ParseNode[], - optArgs: (?ParseNode)[], + args: ParseNode<*>[], + optArgs: (?ParseNode<*>)[], } { const totalArgs = funcData.numArgs + funcData.numOptionalArgs; if (totalArgs === 0) { @@ -604,7 +606,7 @@ export default class Parser { "Expected group after '" + func + "'", nextToken); } } - let argNode: ParseNode; + let argNode: ParseNode<*>; if (arg.type === "fn") { const argGreediness = functions[arg.result].greediness; @@ -880,7 +882,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: ParseNode<*>[]) { let n = group.length - 1; for (let i = 0; i < n; ++i) { const a = group[i]; @@ -977,7 +979,7 @@ export default class Parser { // Transform combining characters into accents if (match) { for (let i = 0; i < match[0].length; i++) { - const accent = match[0][i]; + const accent: string = match[0][i]; if (!unicodeAccents[accent]) { throw new ParseError(`Unknown accent ' ${accent}'`, nucleus); } diff --git a/src/buildCommon.js b/src/buildCommon.js index 8d4f26fc..95227c9e 100644 --- a/src/buildCommon.js +++ b/src/buildCommon.js @@ -15,6 +15,7 @@ import {calculateSize} from "./units"; import type Options from "./Options"; import type ParseNode from "./ParseNode"; +import type {NodeType} from "./ParseNode"; import type {CharacterMetrics} from "./fontMetrics"; import type {Mode} from "./types"; import type {HtmlDomNode, DomSpan, SvgSpan, CssStyle} from "./domTree"; @@ -138,7 +139,7 @@ const mathDefault = function( mode: Mode, options: Options, classes: string[], - type: string, // TODO(#892): Use ParseNode type here. + type: NodeType, ): domTree.symbolNode { if (type === "mathord") { const fontLookup = mathit(value, mode, options, classes); @@ -223,9 +224,9 @@ const boldsymbol = function( * Makes either a mathord or textord in the correct font and color. */ const makeOrd = function( - group: ParseNode, + group: ParseNode<*>, options: Options, - type: string, // TODO(#892): Use ParseNode type here. + type: NodeType, ): domTree.symbolNode { const mode = group.mode; const value = group.value; @@ -591,9 +592,7 @@ const makeVList = function(params: VListParam, options: Options): DomSpan { }; // Converts verb group into body string, dealing with \verb* form -const makeVerb = function(group: ParseNode, options: Options): string { - // TODO(#892): Make ParseNode type-safe and confirm `group.type` to guarantee - // that `group.value.body` is of type string. +const makeVerb = function(group: ParseNode<"verb">, options: Options): string { let text = group.value.body; if (group.value.star) { text = text.replace(/ /g, '\u2423'); // Open Box diff --git a/src/buildTree.js b/src/buildTree.js index c0c34b75..a08b3b39 100644 --- a/src/buildTree.js +++ b/src/buildTree.js @@ -17,7 +17,7 @@ const optionsFromSettings = function(settings: Settings) { }; export const buildTree = function( - tree: ParseNode[], + tree: ParseNode<*>[], expression: string, settings: Settings, ): DomSpan { @@ -39,7 +39,7 @@ export const buildTree = function( }; export const buildHTMLTree = function( - tree: ParseNode[], + tree: ParseNode<*>[], expression: string, settings: Settings, ): DomSpan { diff --git a/src/defineEnvironment.js b/src/defineEnvironment.js index aedcf9c7..30e4ff92 100644 --- a/src/defineEnvironment.js +++ b/src/defineEnvironment.js @@ -27,9 +27,9 @@ type EnvContext = {| */ type EnvHandler = ( context: EnvContext, - args: ParseNode[], - optArgs: (?ParseNode)[], -) => ParseNode; + args: ParseNode<*>[], + optArgs: (?ParseNode<*>)[], +) => ParseNode<*>; /** * - numArgs: (default 0) The number of arguments after the \begin{name} function. diff --git a/src/defineFunction.js b/src/defineFunction.js index 186bf674..d3762974 100644 --- a/src/defineFunction.js +++ b/src/defineFunction.js @@ -4,6 +4,7 @@ import {groupTypes as mathmlGroupTypes} from "./buildMathML"; import type Parser from "./Parser"; import type ParseNode from "./ParseNode"; +import type {NodeType, NodeValue} from "./ParseNode"; import type Options from "./Options"; import type {ArgType, BreakToken, Mode} from "./types"; import type {Token} from "./Token"; @@ -16,12 +17,11 @@ export type FunctionContext = {| breakOnTokenText?: BreakToken, |}; -// TODO: Enumerate all allowed output types. -export type FunctionHandler = ( +export type FunctionHandler = ( context: FunctionContext, - args: ParseNode[], - optArgs: (?ParseNode)[], -) => *; + args: ParseNode<*>[], + optArgs: (?ParseNode<*>)[], +) => NodeValue; export type FunctionPropSpec = { // The number of arguments the function takes. @@ -80,9 +80,10 @@ export type FunctionPropSpec = { consumeMode?: ?Mode, }; -type FunctionDefSpec = {| +type FunctionDefSpec = {| // Unique string to differentiate parse nodes. - type?: string, + // Also determines the type of the value returned by `handler`. + type: NODETYPE, // The first argument to defineFunction is a single name or a list of names. // All functions named in such a list will share a single implementation. @@ -92,23 +93,22 @@ type FunctionDefSpec = {| props: FunctionPropSpec, // The handler is called to handle these functions and their arguments. - // // The function should return an object with the following keys: // - type: The type of element that this is. This is then used in // buildHTML/buildMathML to determine which function // should be called to build this node into a DOM node // Any other data can be added to the object, which will be passed // in to the function in buildHTML/buildMathML as `group.value`. - handler: ?FunctionHandler, + handler: ?FunctionHandler, // This function returns an object representing the DOM structure to be // created when rendering the defined LaTeX function. - // TODO: Port buildHTML to flow and make the group and return types explicit. + // TODO: Change `group` to ParseNode and make return type explicit. htmlBuilder?: (group: *, options: Options) => *, // This function returns an object representing the MathML structure to be // created when rendering the defined LaTeX function. - // TODO: Port buildMathML to flow and make the group and return types explicit. + // TODO: Change `group` to ParseNode and make return type explicit. mathmlBuilder?: (group: *, options: Options) => *, |}; @@ -119,7 +119,8 @@ type FunctionDefSpec = {| * 2. requires all arguments except argTypes. * It is generated by `defineFunction()` below. */ -export type FunctionSpec = {| +export type FunctionSpec = {| + type: NODETYPE, // Need to use the type to avoid error. See NOTES below. numArgs: number, argTypes?: ArgType[], greediness: number, @@ -128,8 +129,23 @@ export type FunctionSpec = {| numOptionalArgs: number, infix: boolean, consumeMode: ?Mode, + + // FLOW TYPE NOTES: Doing either one of the following two + // + // - removing the NOTETYPE type parameter in FunctionSpec above; + // - using ?FunctionHandler below; + // + // results in a confusing flow typing error: + // "string literal `styling`. This type is incompatible with..." + // pointing to the definition of `defineFunction` and finishing with + // "some incompatible instantiation of `NODETYPE`" + // + // Having FunctionSpec above and FunctionHandler<*> below seems to + // circumvent this error. This is not harmful for catching errors since + // _functions is typed FunctionSpec<*> (it stores all TeX function specs). + // Must be specified unless it's handled directly in the parser. - handler: ?FunctionHandler, + handler: ?FunctionHandler<*>, |}; /** @@ -137,18 +153,20 @@ export type FunctionSpec = {| * `functions.js` just exports this same dictionary again and makes it public. * `Parser.js` requires this dictionary. */ -export const _functions: {[string]: FunctionSpec} = {}; +export const _functions: {[string]: FunctionSpec<*>} = {}; -export default function defineFunction({ +export default function defineFunction({ type, + nodeType, names, props, handler, htmlBuilder, mathmlBuilder, -}: FunctionDefSpec) { +}: FunctionDefSpec) { // Set default values of functions const data = { + type, numArgs: props.numArgs, argTypes: props.argTypes, greediness: (props.greediness === undefined) ? 1 : props.greediness, @@ -176,7 +194,7 @@ export default function defineFunction({ // 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: ParseNode<*>): ParseNode<*>[] { if (arg.type === "ordgroup") { return arg.value; } else { diff --git a/src/environments/array.js b/src/environments/array.js index b1cdcabc..8f9476f4 100644 --- a/src/environments/array.js +++ b/src/environments/array.js @@ -21,7 +21,7 @@ type AlignSpec = { type: "separator", separator: string } | { pregap?: number, postgap?: number, }; -type ArrayEnvNodeData = { +export type ArrayEnvNodeData = { type: "array", hskipBeforeAndAfter?: boolean, arraystretch?: number, @@ -29,7 +29,7 @@ type ArrayEnvNodeData = { cols?: AlignSpec[], // These fields are always set, but not on struct construction // initialization. - body?: ParseNode[][], // List of rows in the (2D) array. + body?: ParseNode<*>[][], // List of rows in the (2D) array. rowGaps?: number[], }; @@ -43,7 +43,7 @@ function parseArray( parser: Parser, result: ArrayEnvNodeData, style: StyleStr, -): ParseNode { +): ParseNode<*> { let row = []; const body = [row]; const rowGaps = []; @@ -52,6 +52,7 @@ function parseArray( cell = new ParseNode("ordgroup", cell, parser.mode); if (style) { cell = new ParseNode("styling", { + type: "styling", style: style, value: [cell], }, parser.mode); @@ -85,7 +86,7 @@ function parseArray( } result.body = body; result.rowGaps = rowGaps; - return new ParseNode(result.type, result, parser.mode); + return new ParseNode("array", result, parser.mode); } diff --git a/src/functions.js b/src/functions.js index 2b6f0f62..681b29bd 100644 --- a/src/functions.js +++ b/src/functions.js @@ -9,6 +9,7 @@ import { } from "./defineFunction"; import type {FunctionPropSpec, FunctionHandler} from "./defineFunction"; +import type {NodeType} from "./ParseNode"; // WARNING: New functions should be added to src/functions and imported here. @@ -17,12 +18,15 @@ export default functions; // Define a convenience function that mimcs the old semantics of defineFunction // to support existing code so that we can migrate it a little bit at a time. -const defineFunction = function( +const defineFunction = function( + // Type of node data output by the function handler. This is required to aid + // type inference of the actual function output. + type: NODETYPE, names: string[], props: FunctionPropSpec, - handler: ?FunctionHandler, // null only if handled in parser + handler: ?FunctionHandler, // null only if handled in parser ) { - _defineFunction({names, props, handler}); + _defineFunction({type, names, props, handler}); }; // TODO(kevinb): have functions return an object and call defineFunction with @@ -48,7 +52,7 @@ import "./functions/kern"; import "./functions/phantom"; // Math class commands except \mathop -defineFunction([ +defineFunction("mclass", [ "\\mathord", "\\mathbin", "\\mathrel", "\\mathopen", "\\mathclose", "\\mathpunct", "\\mathinner", ], { @@ -63,7 +67,7 @@ defineFunction([ }); // Build a relation or stacked op by placing one symbol on top of another -defineFunction(["\\stackrel", "\\overset", "\\underset"], { +defineFunction("mclass", ["\\stackrel", "\\overset", "\\underset"], { numArgs: 2, }, function(context, args) { const mathAxisArg = args[1]; @@ -104,7 +108,7 @@ const singleCharIntegrals: {[string]: string} = { // displaystyle. These four groups cover the four possible choices. // No limits, not symbols -defineFunction([ +defineFunction("op", [ "\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg", "\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg", "\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp", @@ -122,7 +126,7 @@ defineFunction([ }); // Limits, not symbols -defineFunction([ +defineFunction("op", [ "\\det", "\\gcd", "\\inf", "\\lim", "\\max", "\\min", "\\Pr", "\\sup", ], { numArgs: 0, @@ -136,7 +140,7 @@ defineFunction([ }); // No limits, symbols -defineFunction([ +defineFunction("op", [ "\\int", "\\iint", "\\iiint", "\\oint", "\u222b", "\u222c", "\u222d", "\u222e", ], { @@ -175,7 +179,7 @@ import "./functions/font"; import "./functions/accent"; // Horizontal stretchy braces -defineFunction([ +defineFunction("horizBrace", [ "\\overbrace", "\\underbrace", ], { numArgs: 1, @@ -193,7 +197,7 @@ defineFunction([ import "./functions/accentunder"; // Stretchy arrows with an optional argument -defineFunction([ +defineFunction("xArrow", [ "\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow", "\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow", "\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown", @@ -219,7 +223,7 @@ defineFunction([ }); // Infix generalized fractions -defineFunction(["\\over", "\\choose", "\\atop"], { +defineFunction("infix", ["\\over", "\\choose", "\\atop"], { numArgs: 0, infix: true, }, function(context) { @@ -245,7 +249,7 @@ defineFunction(["\\over", "\\choose", "\\atop"], { }); // Row breaks for aligned data -defineFunction(["\\\\", "\\cr"], { +defineFunction("cr", ["\\\\", "\\cr"], { numArgs: 0, numOptionalArgs: 1, argTypes: ["size"], @@ -258,7 +262,7 @@ defineFunction(["\\\\", "\\cr"], { }); // Environment delimiters -defineFunction(["\\begin", "\\end"], { +defineFunction("environment", ["\\begin", "\\end"], { numArgs: 1, argTypes: ["text"], }, function(context, args) { @@ -278,7 +282,7 @@ defineFunction(["\\begin", "\\end"], { }); // Box manipulation -defineFunction(["\\raisebox"], { +defineFunction("raisebox", ["\\raisebox"], { numArgs: 2, argTypes: ["size", "text"], allowedInText: true, diff --git a/src/functions/delimsizing.js b/src/functions/delimsizing.js index da66bc30..c212cfb6 100644 --- a/src/functions/delimsizing.js +++ b/src/functions/delimsizing.js @@ -50,7 +50,10 @@ const delimiters = [ ]; // Delimiter functions -function checkDelimiter(delim: ParseNode, context: FunctionContext): ParseNode { +function checkDelimiter( + delim: ParseNode<*>, + context: FunctionContext, +): ParseNode<*> { if (utils.contains(delimiters, delim.value)) { return delim; } else { diff --git a/src/functions/math.js b/src/functions/math.js index 502761ef..cee0d50e 100644 --- a/src/functions/math.js +++ b/src/functions/math.js @@ -4,6 +4,7 @@ import ParseError from "../ParseError"; // Switching from text mode back to math mode defineFunction({ + type: "styling", names: ["\\(", "$"], props: { numArgs: 0, @@ -32,6 +33,7 @@ defineFunction({ // Check for extra closing math delimiters defineFunction({ + type: "text", // Doesn't matter what this is. names: ["\\)", "\\]"], props: { numArgs: 0, diff --git a/src/functions/styling.js b/src/functions/styling.js index 52409dfe..d3558dc3 100644 --- a/src/functions/styling.js +++ b/src/functions/styling.js @@ -30,11 +30,15 @@ defineFunction({ parser.consumeSpaces(); const body = parser.parseExpression(true, breakOnTokenText); + // TODO: Refactor to avoid duplicating styleMap in multiple places (e.g. + // here and in buildHTML and de-dupe the enumeration of all the styles). + // $FlowFixMe: The names above exactly match the styles. + const style: StyleStr = funcName.slice(1, funcName.length - 5); return { type: "styling", // Figure out what style to use by pulling out the style from // the function name - style: funcName.slice(1, funcName.length - 5), + style, value: body, }; }, diff --git a/src/parseTree.js b/src/parseTree.js index 0b6f26a9..d6fc082c 100644 --- a/src/parseTree.js +++ b/src/parseTree.js @@ -12,7 +12,7 @@ import type Settings from "./Settings"; /** * 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): ParseNode<*>[] { 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 ed469a7f..3b570853 100644 --- a/src/stretchy.js +++ b/src/stretchy.js @@ -107,7 +107,7 @@ const mathMLnode = function(label: string): mathMLTree.MathNode { // corresponds to 0.522 em inside the document. const katexImagesData: { - [string]: ([string[], number, number] | [string[], number, number, string]) + [string]: ([string[], number, number] | [[string], number, number, string]) } = { // path(s), minWidth, height, align overrightarrow: [["rightarrow"], 0.888, 522, "xMaxYMin"], @@ -159,7 +159,7 @@ const katexImagesData: { "shortrightharpoonabovebar"], 1.75, 716], }; -const groupLength = function(arg: ParseNode): number { +const groupLength = function(arg: ParseNode<*>): number { if (arg.type === "ordgroup") { return arg.value.length; } else { @@ -167,7 +167,10 @@ const groupLength = function(arg: ParseNode): number { } }; -const svgSpan = function(group: ParseNode, options: Options): DomSpan | SvgSpan { +const svgSpan = function( + group: ParseNode<"accent">, + options: Options, +): DomSpan | SvgSpan { // Create a span with inline SVG for the element. function buildSvgSpan_(): { span: DomSpan | SvgSpan, @@ -219,13 +222,16 @@ const svgSpan = function(group: ParseNode, options: Options): DomSpan | SvgSpan } else { const spans = []; - const [paths, minWidth, viewBoxHeight, align1] = katexImagesData[label]; + const data = katexImagesData[label]; + const [paths, minWidth, viewBoxHeight] = data; const height = viewBoxHeight / 1000; const numSvgChildren = paths.length; let widthClasses; let aligns; if (numSvgChildren === 1) { + // $FlowFixMe: All these cases must be of the 4-tuple type. + const align1: string = data[3]; widthClasses = ["hide-tail"]; aligns = [align1]; } else if (numSvgChildren === 2) { diff --git a/src/symbols.js b/src/symbols.js index 9e69a77f..993c8ccb 100644 --- a/src/symbols.js +++ b/src/symbols.js @@ -20,9 +20,13 @@ import type {Mode} from "./types"; type Font = "main" | "ams" +// Some of these have a "-token" suffix since these are also used as `ParseNode` +// 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. type Group = - "accent" | "bin" | "close" | "inner" | "mathord" | "op" | "open" | "punct" | - "rel" | "spacing" | "textord"; + "accent-token" | "bin" | "close" | "inner" | "mathord" | + "op-token" | "open" | "punct" | "rel" | "spacing" | "textord"; type CharInfoMap = {[string]: {font: Font, group: Group, replace: ?string}}; const symbols: {[Mode]: CharInfoMap} = { @@ -59,12 +63,12 @@ const main = "main"; const ams = "ams"; // groups: -const accent = "accent"; +const accent = "accent-token"; const bin = "bin"; const close = "close"; const inner = "inner"; const mathord = "mathord"; -const op = "op"; +const op = "op-token"; const open = "open"; const punct = "punct"; const rel = "rel"; diff --git a/src/types.js b/src/types.js index a1e7865b..48887b12 100644 --- a/src/types.js +++ b/src/types.js @@ -21,7 +21,7 @@ export type Mode = "math" | "text"; export type ArgType = "color" | "size" | "url" | "original" | Mode; // LaTeX display style. -export type StyleStr = "text" | "display"; +export type StyleStr = "text" | "display" | "script" | "scriptscript"; // Allowable token text for "break" arguments in parser export type BreakToken = "]" | "}" | "$" | "\\)"; diff --git a/test/katex-spec.js b/test/katex-spec.js index 9c4c6fdb..e82540c0 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -407,6 +407,7 @@ describe("A subscript and superscript parser", function() { it("should not fail when there is no nucleus", function() { expect("^3").toParse(); + expect("^3+").toParse(); expect("_2").toParse(); expect("^3_2").toParse(); expect("_2^3").toParse();