update defineFunction to accept a single arg, introduce flow to do some typechecking

This commit is contained in:
Kevin Barabash
2017-08-19 21:18:12 -04:00
committed by Kevin Barabash
parent a99c7c9e0f
commit d8116bdc64
7 changed files with 174 additions and 131 deletions

View File

@@ -1,5 +1,5 @@
{
"presets": ["es2015"],
"presets": ["es2015", "flow"],
"plugins": [
"transform-runtime",
"transform-class-properties"

11
.flowconfig Normal file
View File

@@ -0,0 +1,11 @@
[ignore]
<PROJECT_ROOT>/build
<PROJECT_ROOT>/node_modules/match-at
[include]
[libs]
[lints]
[options]

View File

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

View File

@@ -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<T> = {
// 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<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`. 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 {

View File

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

View File

@@ -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;
},
);
});

View File

@@ -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;
},
);
});