Files
KaTeX/src/defineFunction.js
Ashish Myles 0ac4b6e89d Convert ParseNode to struct (#1534)
* Define the nested version of ParseNodes structs explicitly.

Passes test:jest, but fails test:flow.

* Fix additional type errors reported by flow.

* Migrate rebased code from master.

* Rename ParseNode.js to parseNode.js.

* Update defineEnvironment output type to fix the flow errors in environment/array.js.
2018-08-01 15:41:27 +09:00

251 lines
8.8 KiB
JavaScript

// @flow
import {checkNodeType} from "./parseNode";
import type Parser from "./Parser";
import type {ParseNode, AnyParseNode, NodeType} from "./parseNode";
import type Options from "./Options";
import type {ArgType, BreakToken, Mode} 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<NODETYPE: NodeType> = (
context: FunctionContext,
args: AnyParseNode[],
optArgs: (?AnyParseNode)[],
) => ParseNode<NODETYPE>;
export type HtmlBuilder<NODETYPE> = (ParseNode<NODETYPE>, Options) => HtmlDomNode;
export type MathMLBuilder<NODETYPE> = (
group: ParseNode<NODETYPE>,
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<NODETYPE> =
(ParseNode<"supsub"> | ParseNode<NODETYPE>, 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[],
// 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,
// Switch to the specified mode while consuming the command token.
// This is useful for commands that switch between math and text mode,
// for making sure that a switch happens early enough. Note that the
// mode is switched immediately back to its original value after consuming
// the command token, so that the argument parsing and/or function handler
// can easily access the old mode while doing their own mode switching.
consumeMode?: ?Mode,
};
type FunctionDefSpec<NODETYPE: NodeType> = {|
// 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<string>,
// Properties that control how the functions are parsed.
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<NODETYPE>,
// 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<NODETYPE>,
// 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<NODETYPE>,
|};
/**
* 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<NODETYPE: NodeType> = {|
type: NODETYPE, // Need to use the type to avoid error. See NOTES below.
numArgs: number,
argTypes?: ArgType[],
greediness: number,
allowedInText: boolean,
allowedInMath: boolean,
numOptionalArgs: number,
infix: boolean,
consumeMode: ?Mode,
// FLOW TYPE NOTES: Doing either one of the following two
//
// - removing the NODETYPE type parameter in FunctionSpec above;
// - using ?FunctionHandler<NODETYPE> 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<NODETYPE> 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<NODETYPE: NodeType>({
type,
nodeType,
names,
props,
handler,
htmlBuilder,
mathmlBuilder,
}: FunctionDefSpec<NODETYPE>) {
// Set default values of functions
const data = {
type,
numArgs: props.numArgs,
argTypes: props.argTypes,
greediness: (props.greediness === undefined) ? 1 : props.greediness,
allowedInText: !!props.allowedInText,
allowedInMath: (props.allowedInMath === undefined)
? true
: props.allowedInMath,
numOptionalArgs: props.numOptionalArgs || 0,
infix: !!props.infix,
consumeMode: props.consumeMode,
handler: handler,
};
for (let i = 0; i < names.length; ++i) {
// TODO: The value type of _functions should be a type union of all
// possible `FunctionSpec<>` possibilities instead of `FunctionSpec<*>`,
// which is an existential type.
// $FlowFixMe
_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<NODETYPE: NodeType>({
type, htmlBuilder, mathmlBuilder,
}: {|
type: NODETYPE,
htmlBuilder?: HtmlBuilder<NODETYPE>,
mathmlBuilder: MathMLBuilder<NODETYPE>,
|}) {
defineFunction({
type,
names: [],
props: {numArgs: 0},
handler() { throw new Error('Should never be called.'); },
htmlBuilder,
mathmlBuilder,
});
}
// 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[] {
const node = checkNodeType(arg, "ordgroup");
return node ? node.value : [arg];
};