diff --git a/src/defineFunction.js b/src/defineFunction.js index 7b12525e..4d2f1c95 100644 --- a/src/defineFunction.js +++ b/src/defineFunction.js @@ -1,77 +1,83 @@ // @flow -import functions from "./functions"; import {groupTypes as htmlGroupTypes} from "./buildHTML"; import {groupTypes as mathmlGroupTypes} from "./buildMathML"; +import type ParseNode from "./ParseNode" ; import type Options from "./Options"; import type {ArgType} from "./types" ; +import type {Parser} from "./Parser" ; +import type {Token} from "./Token" ; -type FunctionSpec = { +/** Context provided to function handlers for error messages. */ +export type FunctionContext = {| + funcName: string, + parser: Parser, + token?: Token, +|}; + +// TODO: Enumerate all allowed output types. +export type FunctionHandler = (context: FunctionContext, args: ParseNode[]) => *; + +export type FunctionPropSpec = { + // The number of arguments the function takes. + numArgs: number, + + // An array corresponding to each argument of the function, giving the + // type of argument that should be parsed. Its length should be equal + // to `numArgs + numOptionalArgs`. + argTypes?: ArgType[], + + // The greediness of the function to use ungrouped arguments. + // + // E.g. if you have an expression + // \sqrt \frac 1 2 + // since \frac has greediness=2 vs \sqrt's greediness=1, \frac + // will use the two arguments '1' and '2' as its two arguments, + // then that whole function will be used as the argument to + // \sqrt. On the other hand, the expressions + // \frac \frac 1 2 3 + // and + // \frac \sqrt 1 2 + // will fail because \frac and \frac have equal greediness + // and \sqrt has a lower greediness than \frac respectively. To + // make these parse, we would have to change them to: + // \frac {\frac 1 2} 3 + // and + // \frac {\sqrt 1} 2 + // + // The default value is `1` + greediness?: number, + + // Whether or not the function is allowed inside text mode + // (default false) + allowedInText?: boolean, + + // Whether or not the function is allowed inside text mode + // (default true) + allowedInMath?: boolean, + + // (optional) The number of optional arguments the function + // should parse. If the optional arguments aren't found, + // `null` will be passed to the handler in their place. + // (default 0) + numOptionalArgs?: number, + + // Must be true if the function is an infix operator. + infix?: boolean, +}; + +type FunctionDefSpec = {| // Unique string to differentiate parse nodes. - type: string, + type?: string, // 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. names: Array, // Properties that control how the functions are parsed. - props: { - // The number of arguments the function takes. - numArgs?: number, - - // An array corresponding to each argument of the function, giving the - // type of argument that should be parsed. Its length should be equal - // to `numArgs + numOptionalArgs`. - argTypes?: ArgType[], - - // The greediness of the function to use ungrouped arguments. - // - // E.g. if you have an expression - // \sqrt \frac 1 2 - // since \frac has greediness=2 vs \sqrt's greediness=1, \frac - // will use the two arguments '1' and '2' as its two arguments, - // then that whole function will be used as the argument to - // \sqrt. On the other hand, the expressions - // \frac \frac 1 2 3 - // and - // \frac \sqrt 1 2 - // will fail because \frac and \frac have equal greediness - // and \sqrt has a lower greediness than \frac respectively. To - // make these parse, we would have to change them to: - // \frac {\frac 1 2} 3 - // and - // \frac {\sqrt 1} 2 - // - // The default value is `1` - greediness?: number, - - // Whether or not the function is allowed inside text mode - // (default false) - allowedInText?: boolean, - - // Whether or not the function is allowed inside text mode - // (default true) - allowedInMath?: boolean, - - // (optional) The number of optional arguments the function - // should parse. If the optional arguments aren't found, - // `null` will be passed to the handler in their place. - // (default 0) - numOptionalArgs?: number, - - // Must be true if the function is an infix operator. - infix?: boolean, - }, + props: FunctionPropSpec, // The handler is called to handle these functions and their arguments. - // It receives two arguments: - // - context contains information and references provided by the parser - // - args is an array of arguments obtained from TeX input - // The context contains the following properties: - // - funcName: the text (i.e. name) of the function, including \ - // - parser: the parser object - // - lexer: the lexer object - // The latter three should only be used to produce error messages. // // The function should return an object with the following keys: // - type: The type of element that this is. This is then used in @@ -79,16 +85,44 @@ type FunctionSpec = { // 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: (context: any, args: any) => T, + handler: ?FunctionHandler, // This function returns an object representing the DOM structure to be // created when rendering the defined LaTeX function. - htmlBuilder: (group: T, options: Options) => any, + // TODO: Port buildHTML to flow and make the group and return types explicit. + htmlBuilder?: (group: *, options: Options) => *, // This function returns an object representing the MathML structure to be // created when rendering the defined LaTeX function. - mathmlBuilder: (group: T, options: Options) => any, -} + // TODO: Port buildMathML to flow and make the group and return types explicit. + mathmlBuilder?: (group: *, options: Options) => *, +|}; + +/** + * Final function spec for use at parse time. + * This is almost identical to `FunctionPropSpec`, except it + * 1. includes the function handler, and + * 2. requires all arguments except argTypes. + * It is generated by `defineFunction()` below. + */ +type FunctionSpec = {| + numArgs: number, + argTypes?: ArgType[], + greediness: number, + allowedInText: boolean, + allowedInMath: boolean, + numOptionalArgs: number, + infix: boolean, + // Must be specified unless it's handled directly in the parser. + handler: ?FunctionHandler, +|}; + +/** + * All registered functions. + * `functions.js` just exports this same dictionary again and makes it public. + * `Parser.js` requires this dictionary. + */ +export const _functions: {[string]: FunctionSpec} = {}; export default function defineFunction({ type, @@ -97,7 +131,7 @@ export default function defineFunction({ handler, htmlBuilder, mathmlBuilder, -}: FunctionSpec<*>) { +}: FunctionDefSpec) { // Set default values of functions const data = { numArgs: props.numArgs, @@ -112,7 +146,7 @@ export default function defineFunction({ handler: handler, }; for (let i = 0; i < names.length; ++i) { - functions[names[i]] = data; + _functions[names[i]] = data; } if (type) { if (htmlBuilder) { @@ -126,7 +160,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: any) { +export const ordargument = function(arg: ParseNode) { if (arg.type === "ordgroup") { return arg.value; } else { diff --git a/src/functions.js b/src/functions.js index c29bbe85..49de8f33 100644 --- a/src/functions.js +++ b/src/functions.js @@ -1,24 +1,33 @@ +// @flow +/** Include this to ensure that all functions are defined. */ import utils from "./utils"; import ParseError from "./ParseError"; import ParseNode from "./ParseNode"; -import {default as _defineFunction, ordargument} from "./defineFunction"; +import { + default as _defineFunction, + ordargument, + _functions, +} from "./defineFunction"; -// WARNING: New functions should be added to src/functions. +import type {FunctionPropSpec, FunctionHandler} from "./defineFunction" ; + +// WARNING: New functions should be added to src/functions and imported here. + +const functions = _functions; +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(names, props, handler) { - if (typeof names === "string") { - names = [names]; - } - if (typeof props === "number") { - props = { numArgs: props }; - } +const defineFunction = function( + names: string[], + props: FunctionPropSpec, + handler: ?FunctionHandler, // null only if handled in parser +) { _defineFunction({names, props, handler}); }; // A normal square root -defineFunction("\\sqrt", { +defineFunction(["\\sqrt"], { numArgs: 1, numOptionalArgs: 1, }, function(context, args) { @@ -56,7 +65,7 @@ defineFunction([ }); // A two-argument custom color -defineFunction("\\textcolor", { +defineFunction(["\\textcolor"], { numArgs: 2, allowedInText: true, greediness: 3, @@ -72,7 +81,7 @@ defineFunction("\\textcolor", { }); // \color is handled in Parser.js's parseImplicitGroup -defineFunction("\\color", { +defineFunction(["\\color"], { numArgs: 1, allowedInText: true, greediness: 3, @@ -80,7 +89,7 @@ defineFunction("\\color", { }, null); // An overline -defineFunction("\\overline", { +defineFunction(["\\overline"], { numArgs: 1, }, function(context, args) { const body = args[0]; @@ -91,7 +100,7 @@ defineFunction("\\overline", { }); // An underline -defineFunction("\\underline", { +defineFunction(["\\underline"], { numArgs: 1, }, function(context, args) { const body = args[0]; @@ -102,7 +111,7 @@ defineFunction("\\underline", { }); // A box of the width and height -defineFunction("\\rule", { +defineFunction(["\\rule"], { numArgs: 2, numOptionalArgs: 1, argTypes: ["size", "size", "size"], @@ -131,7 +140,7 @@ defineFunction(["\\kern", "\\mkern"], { }); // A KaTeX logo -defineFunction("\\KaTeX", { +defineFunction(["\\KaTeX"], { numArgs: 0, }, function(context) { return { @@ -157,7 +166,7 @@ defineFunction([ }); // Build a relation by placing one symbol on top of another -defineFunction("\\stackrel", { +defineFunction(["\\stackrel"], { numArgs: 2, }, function(context, args) { const top = args[0]; @@ -185,7 +194,7 @@ defineFunction("\\stackrel", { }); // \mod-type functions -defineFunction("\\bmod", { +defineFunction(["\\bmod"], { numArgs: 0, }, function(context, args) { return { @@ -308,7 +317,7 @@ defineFunction([ }); // \mathop class command -defineFunction("\\mathop", { +defineFunction(["\\mathop"], { numArgs: 1, }, function(context, args) { const body = args[0]; @@ -392,7 +401,7 @@ defineFunction(["\\mathllap", "\\mathrlap", "\\mathclap"], { }); // smash, with optional [tb], as in AMS -defineFunction("\\smash", { +defineFunction(["\\smash"], { numArgs: 1, numOptionalArgs: 1, allowedInText: true, @@ -437,19 +446,19 @@ import "./functions/delimsizing"; defineFunction([ "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", -], 0, null); +], {numArgs: 0}, null); // Style changing functions (handled in Parser.js explicitly, hence no // handler) defineFunction([ "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle", -], 0, null); +], {numArgs: 0}, null); // Old font changing functions defineFunction([ "\\rm", "\\sf", "\\tt", "\\bf", "\\it", //"\\sl", "\\sc", -], 0, null); +], {numArgs: 0}, null); defineFunction([ // styles diff --git a/src/functions/delimsizing.js b/src/functions/delimsizing.js index 51e981ee..408cc5fe 100644 --- a/src/functions/delimsizing.js +++ b/src/functions/delimsizing.js @@ -9,6 +9,9 @@ import utils from "../utils"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; +import type ParseNode from "../ParseNode"; +import type {FunctionContext} from "../defineFunction"; + // Extra data needed for the delimiter handler down below const delimiterSizes = { "\\bigl" : {mclass: "mopen", size: 1}, @@ -45,7 +48,7 @@ const delimiters = [ ]; // Delimiter functions -const checkDelimiter = function(delim, context) { +function checkDelimiter(delim: ParseNode, context: FunctionContext): ParseNode { if (utils.contains(delimiters, delim.value)) { return delim; } else { @@ -53,7 +56,7 @@ const checkDelimiter = function(delim, context) { "Invalid delimiter: '" + delim.value + "' after '" + context.funcName + "'", delim); } -}; +} defineFunction({ type: "delimsizing", diff --git a/src/types.js b/src/types.js index 30b55e88..ac28a639 100644 --- a/src/types.js +++ b/src/types.js @@ -15,7 +15,8 @@ export type Mode = "math" | "text"; // bodies of functions like \textcolor where the // first argument is special and the second // argument is parsed normally) -export type ArgType = "color" | "size" | "original"; +// - "text": Node group parsed as in text mode. +export type ArgType = "color" | "size" | "original" | "text"; // LaTeX display style. export type StyleStr = "text" | "display";