Port functions.js to @flow. (#867)

* Make types in defineFunction stricter and prep for porting functions.js to flow.

* Add more explicit types to functions/delimsizing.js.

* Port functions.js to @flow.
This commit is contained in:
Ashish Myles
2017-09-10 21:25:36 -04:00
committed by GitHub
parent ceefd4934f
commit d8060ca9b4
4 changed files with 138 additions and 91 deletions

View File

@@ -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<T> = {
/** 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<string>,
// 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<T> = {
// 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 {

View File

@@ -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

View File

@@ -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",

View File

@@ -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";