// @flow import type Parser from "./Parser"; import type {ParseNode, AnyParseNode, NodeType, UnsupportedCmdParseNode} from "./parseNode"; import type Options from "./Options"; import type {ArgType, BreakToken} from "./types"; import type {HtmlDomNode} from "./domTree"; import type {Token} from "./Token"; import type {MathDomNode} from "./mathMLTree"; /** Context provided to function handlers for error messages. */ export type FunctionContext = {| funcName: string, parser: Parser, token?: Token, breakOnTokenText?: BreakToken, |}; export type FunctionHandler = ( context: FunctionContext, args: AnyParseNode[], optArgs: (?AnyParseNode)[], ) => UnsupportedCmdParseNode | ParseNode; // Note: reverse the order of the return type union will cause a flow error. // See https://github.com/facebook/flow/issues/3663. export type HtmlBuilder = (ParseNode, Options) => HtmlDomNode; export type MathMLBuilder = ( group: ParseNode, options: Options, ) => MathDomNode; // More general version of `HtmlBuilder` for nodes (e.g. \sum, accent types) // whose presence impacts super/subscripting. In this case, ParseNode<"supsub"> // delegates its HTML building to the HtmlBuilder corresponding to these nodes. export type HtmlBuilderSupSub = (ParseNode<"supsub"> | ParseNode, Options) => HtmlDomNode; 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 `numOptionalArgs + numArgs`, and types for optional arguments // should appear before types for mandatory arguments. argTypes?: ArgType[], // Whether it expands to a single token or a braced group of tokens. // If it's grouped, it can be used as an argument to primitive commands, // such as \sqrt (without the optional argument) and super/subscript. allowedInArgument?: boolean, // 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, // Whether or not the function is a TeX primitive. primitive?: boolean, }; type FunctionDefSpec = {| // Unique string to differentiate parse nodes. // 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. names: Array, // Properties that control how the functions are parsed. props: FunctionPropSpec, // The handler is called to handle these functions and their arguments and // returns a `ParseNode`. handler: ?FunctionHandler, // This function returns an object representing the DOM structure to be // created when rendering the defined LaTeX function. // This should not modify the `ParseNode`. htmlBuilder?: HtmlBuilder, // This function returns an object representing the MathML structure to be // created when rendering the defined LaTeX function. // This should not modify the `ParseNode`. mathmlBuilder?: MathMLBuilder, |}; /** * 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. */ export type FunctionSpec = {| type: NODETYPE, // Need to use the type to avoid error. See NOTES below. numArgs: number, argTypes?: ArgType[], allowedInArgument: boolean, allowedInText: boolean, allowedInMath: boolean, numOptionalArgs: number, infix: boolean, primitive: boolean, // FLOW TYPE NOTES: Doing either one of the following two // // - removing the NODETYPE 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<*>, |}; /** * 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<*>} = {}; /** * All HTML builders. Should be only used in the `define*` and the `build*ML` * functions. */ export const _htmlGroupBuilders: {[string]: HtmlBuilder<*>} = {}; /** * All MathML builders. Should be only used in the `define*` and the `build*ML` * functions. */ export const _mathmlGroupBuilders: {[string]: MathMLBuilder<*>} = {}; export default function defineFunction({ type, names, props, handler, htmlBuilder, mathmlBuilder, }: FunctionDefSpec) { // Set default values of functions const data = { type, numArgs: props.numArgs, argTypes: props.argTypes, allowedInArgument: !!props.allowedInArgument, allowedInText: !!props.allowedInText, allowedInMath: (props.allowedInMath === undefined) ? true : props.allowedInMath, numOptionalArgs: props.numOptionalArgs || 0, infix: !!props.infix, primitive: !!props.primitive, handler: handler, }; for (let i = 0; i < names.length; ++i) { _functions[names[i]] = data; } if (type) { if (htmlBuilder) { _htmlGroupBuilders[type] = htmlBuilder; } if (mathmlBuilder) { _mathmlGroupBuilders[type] = mathmlBuilder; } } } /** * Use this to register only the HTML and MathML builders for a function (e.g. * if the function's ParseNode is generated in Parser.js rather than via a * stand-alone handler provided to `defineFunction`). */ export function defineFunctionBuilders({ type, htmlBuilder, mathmlBuilder, }: {| type: NODETYPE, htmlBuilder?: HtmlBuilder, mathmlBuilder: MathMLBuilder, |}) { defineFunction({ type, names: [], props: {numArgs: 0}, handler() { throw new Error('Should never be called.'); }, htmlBuilder, mathmlBuilder, }); } export const normalizeArgument = function(arg: AnyParseNode): AnyParseNode { return arg.type === "ordgroup" && arg.body.length === 1 ? arg.body[0] : arg; }; // Since the corresponding buildHTML/buildMathML function expects a // list of elements, we normalize for different kinds of arguments export const ordargument = function(arg: AnyParseNode): AnyParseNode[] { return arg.type === "ordgroup" ? arg.body : [arg]; };