diff --git a/.babelrc b/.babelrc index 08e9719e..09bb8eaa 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["es2015"], + "presets": ["es2015", "flow"], "plugins": [ "transform-runtime", "transform-class-properties" diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 00000000..1bd2021b --- /dev/null +++ b/.flowconfig @@ -0,0 +1,11 @@ +[ignore] +/build +/node_modules/match-at + +[include] + +[libs] + +[lints] + +[options] diff --git a/package.json b/package.json index 1910d25c..3073d697 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,14 @@ "babel-plugin-transform-class-properties": "^6.23.0", "babel-plugin-transform-runtime": "^6.15.0", "babel-preset-es2015": "^6.18.0", + "babel-preset-flow": "^6.23.0", "babel-register": "^6.24.0", "babelify": "^7.3.0", "browserify": "^13.3.0", "clean-css": "^3.4.23", "eslint": "^3.13.0", "express": "^4.14.0", + "flow-bin": "^0.53.1", "glob": "^7.1.1", "jest": "^20.0.4", "jest-serializer-html": "^4.0.0", diff --git a/src/defineFunction.js b/src/defineFunction.js index 1b78bcbe..d4cd1873 100644 --- a/src/defineFunction.js +++ b/src/defineFunction.js @@ -1,87 +1,108 @@ +// @flow import functions from "./functions"; import {groupTypes as htmlGroupTypes} from "./buildHTML"; import {groupTypes as mathmlGroupTypes} from "./buildMathML"; -/* This file contains a list of functions that we parse, identified by - * the calls to defineFunction. - * - * 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. - * - * Each declared function can have associated properties, which - * include the following: - * - * - numArgs: The number of arguments the function takes. - * If this is the only property, it can be passed as a number - * instead of an element of a properties object. - * - argTypes: (optional) 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`. Valid - * types: - * - "size": A size-like thing, such as "1em" or "5ex" - * - "color": An html color, like "#abc" or "blue" - * - "original": The same type as the environment that the - * function being parsed is in (e.g. used for the - * bodies of functions like \textcolor where the - * first argument is special and the second - * argument is parsed normally) - * Other possible types (probably shouldn't be used) - * - "text": Text-like (e.g. \text) - * - "math": Normal math - * If undefined, this will be treated as an appropriate length - * array of "original" strings - * - greediness: (optional) 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` - * - allowedInText: (optional) Whether or not the function is allowed inside - * text mode (default false) - * - numOptionalArgs: (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) - * - infix: (optional) Must be true if the function is an infix operator. - * - * The last argument is that implementation, the handler for the function(s). - * It 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 - * - positions: the positions in the overall string of the function - * and the arguments. - * 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 - * 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`. - */ +// TODO(kevinb) use flow to define a proper type for Options +type Options = any; -export default function defineFunction( - names, props, handler, type, htmlBuilder, mathmlBuilder -) { +type FunctionSpec = { + // Unique string to differentiate parse nodes. + 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: string | 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`. Valid types: + // - "size": A size-like thing, such as "1em" or "5ex" + // - "color": An html color, like "#abc" or "blue" + // - "original": The same type as the environment that the + // function being parsed is in (e.g. used for the + // bodies of functions like \textcolor where the + // first argument is special and the second + // argument is parsed normally) + argTypes?: "color" | "size" | "original", + + // 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, + + // (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, + }, + + // 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 + // - positions: the positions in the overall string of the function + // and the arguments. + // 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 + // 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: (context: any, args: any) => T, + + // This function returns an object representing the DOM structure to be + // created when rendering the defined LaTeX function. + htmlBuilder: (group: T, options: Options) => any, + + // This function returns an object representing the MathML structure to be + // created when rendering the defined LaTeX function. + mathmlBuilder: (group: T, options: Options) => any, +} + +export default function defineFunction({ + type, + names, + props, + handler, + htmlBuilder, + mathmlBuilder, +}: FunctionSpec<*>) { if (typeof names === "string") { names = [names]; } @@ -114,7 +135,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) { +export const ordargument = function(arg: any) { if (arg.type === "ordgroup") { return arg.value; } else { diff --git a/src/functions.js b/src/functions.js index 2b42a27f..8c63278b 100644 --- a/src/functions.js +++ b/src/functions.js @@ -1,8 +1,13 @@ import utils from "./utils"; import ParseError from "./ParseError"; import ParseNode from "./ParseNode"; +import {default as _defineFunction, ordargument} from "./defineFunction"; -import defineFunction, {ordargument} from "./defineFunction"; +// 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) { + _defineFunction({names, props, handler}); +}; // A normal square root defineFunction("\\sqrt", { diff --git a/src/functions/delimsizing.js b/src/functions/delimsizing.js index 4674d8ee..3884c212 100644 --- a/src/functions/delimsizing.js +++ b/src/functions/delimsizing.js @@ -1,3 +1,4 @@ +// @flow import buildCommon, {makeSpan} from "../buildCommon"; import defineFunction from "../defineFunction"; import delimiter from "../delimiter"; @@ -54,17 +55,18 @@ const checkDelimiter = function(delim, context) { } }; -defineFunction( - [ +defineFunction({ + type: "delimsizing", + names: [ "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", "\\big", "\\Big", "\\bigg", "\\Bigg", ], - { + props: { numArgs: 1, }, - function(context, args) { + handler: (context, args) => { const delim = checkDelimiter(args[0], context); return { @@ -74,8 +76,7 @@ defineFunction( value: delim.value, }; }, - "delimsizing", - function(group, options) { + htmlBuilder: (group, options) => { const delim = group.value.value; if (delim === ".") { @@ -89,7 +90,7 @@ defineFunction( delim, group.value.size, options, group.mode, [group.value.mclass]); }, - function(group) { + mathmlBuilder: (group) => { const children = []; if (group.value.value !== ".") { @@ -111,14 +112,17 @@ defineFunction( return node; }, -); +}); -defineFunction( - [ +defineFunction({ + type: "leftright", + names: [ "\\left", "\\right", - ], { + ], + props: { numArgs: 1, - }, function(context, args) { + }, + handler: (context, args) => { const delim = checkDelimiter(args[0], context); // \left and \right are caught somewhere in Parser.js, which is @@ -128,8 +132,7 @@ defineFunction( value: delim.value, }; }, - "leftright", - function(group, options) { + htmlBuilder: (group, options) => { // Build the inner expression const inner = html.buildExpression(group.value.body, options, true); @@ -199,7 +202,7 @@ defineFunction( return makeSpan(["minner"], inner, options); }, - function(group, options) { + mathmlBuilder: (group, options) => { const inner = mml.buildExpression(group.value.body, options); if (group.value.left !== ".") { @@ -224,13 +227,15 @@ defineFunction( return outerNode; }, -); +}); -defineFunction( - "\\middle", - { +defineFunction({ + type: "middle", + names: "\\middle", + props: { numArgs: 1, - }, function(context, args) { + }, + handler: (context, args) => { const delim = checkDelimiter(args[0], context); if (!context.parser.leftrightDepth) { throw new ParseError("\\middle without preceding \\left", delim); @@ -241,8 +246,7 @@ defineFunction( value: delim.value, }; }, - "middle", - function(group, options) { + htmlBuilder: (group, options) => { let middleDelim; if (group.value.value === ".") { middleDelim = html.makeNullDelimiter(options, []); @@ -254,10 +258,10 @@ defineFunction( } return middleDelim; }, - function(group, options) { + mathmlBuilder: (group, options) => { const middleNode = new mathMLTree.MathNode( "mo", [mml.makeText(group.value.middle, group.mode)]); middleNode.setAttribute("fence", "true"); return middleNode; }, -); +}); diff --git a/src/functions/phantom.js b/src/functions/phantom.js index 7b6623f0..00cd7a4c 100644 --- a/src/functions/phantom.js +++ b/src/functions/phantom.js @@ -1,3 +1,4 @@ +// @flow import defineFunction, {ordargument} from "../defineFunction"; import buildCommon from "../buildCommon"; import mathMLTree from "../mathMLTree"; @@ -5,20 +6,20 @@ import mathMLTree from "../mathMLTree"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; -defineFunction( - "\\phantom", - { +defineFunction({ + type: "phantom", + names: "\\phantom", + props: { numArgs: 1, }, - (context, args) => { + handler: (context, args) => { const body = args[0]; return { type: "phantom", value: ordargument(body), }; }, - "phantom", - (group, options) => { + htmlBuilder: (group, options) => { const elements = html.buildExpression( group.value.value, options.withPhantom(), @@ -29,18 +30,19 @@ defineFunction( // See "color" for more details. return new buildCommon.makeFragment(elements); }, - (group, options) => { + mathmlBuilder: (group, options) => { const inner = mml.buildExpression(group.value.value, options); return new mathMLTree.MathNode("mphantom", inner); }, -); +}); -defineFunction( - "\\hphantom", - { +defineFunction({ + type: "hphantom", + names: "\\hphantom", + props: { numArgs: 1, }, - (context, args) => { + handler: (context, args) => { const body = args[0]; return { type: "hphantom", @@ -48,8 +50,7 @@ defineFunction( body: body, }; }, - "hphantom", - (group, options) => { + htmlBuilder: (group, options) => { let node = buildCommon.makeSpan( [], [html.buildGroup(group.value.body, options.withPhantom())]); node.height = 0; @@ -68,20 +69,21 @@ defineFunction( return node; }, - (group, options) => { + mathmlBuilder: (group, options) => { const inner = mml.buildExpression(group.value.value, options); const node = new mathMLTree.MathNode("mphantom", inner); node.setAttribute("height", "0px"); return node; }, -); +}); -defineFunction( - "\\vphantom", - { +defineFunction({ + type: "vphantom", + names: "\\vphantom", + props: { numArgs: 1, }, - (context, args) => { + handler: (context, args) => { const body = args[0]; return { type: "vphantom", @@ -89,8 +91,7 @@ defineFunction( body: body, }; }, - "vphantom", - (group, options) => { + htmlBuilder: (group, options) => { const inner = buildCommon.makeSpan( ["inner"], [html.buildGroup(group.value.body, options.withPhantom())]); @@ -98,11 +99,10 @@ defineFunction( return buildCommon.makeSpan( ["mord", "rlap"], [inner, fix], options); }, - (group, options) => { + mathmlBuilder: (group, options) => { const inner = mml.buildExpression(group.value.value, options); const node = new mathMLTree.MathNode("mphantom", inner); node.setAttribute("width", "0px"); return node; }, -); - +});