Files
KaTeX/src/functions.js
Ashish Myles 5a4aedd882 Make ParseNode value payload and defineFunction handler functions type-safe (#1276)
* Make ParseNode `value` payload type-safe.

* Make defineFunction handlers aware of ParseNode data types.

* Add `type` to all function definitions to help determine handler return type.

* Added unit test for case caught only in screenshot test and fixed issue.

* Rename some symbol `Group`s to avoid conflicts with `ParseNode` groups.

Symbol `Group`s are also used as `ParseNode` types. However, `ParseNode`s of
these types always contain a raw text token as opposed to any structured
content. These `ParseNode`s are passed as arguments into function handlers to
create more semantical `ParseNode`s with more structure.

Before this change, "accent" and "op" were both symbol `Group`s and `ParseNode`
types. With this change, these two types (the raw accent token `ParseNode`, and
the structured semantical `ParseNode` are separated for better type-safety on
the `ParseNode` payload).

* stretchy: Remove FlowFixMe for a forced typecast that's no longer needed.
2018-05-09 20:13:31 -04:00

307 lines
7.7 KiB
JavaScript

// @flow
/** Include this to ensure that all functions are defined. */
import ParseError from "./ParseError";
import ParseNode from "./ParseNode";
import {
default as _defineFunction,
ordargument,
_functions,
} from "./defineFunction";
import type {FunctionPropSpec, FunctionHandler} from "./defineFunction";
import type {NodeType} from "./ParseNode";
// 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<NODETYPE: NodeType>(
// Type of node data output by the function handler. This is required to aid
// type inference of the actual function output.
type: NODETYPE,
names: string[],
props: FunctionPropSpec,
handler: ?FunctionHandler<NODETYPE>, // null only if handled in parser
) {
_defineFunction({type, names, props, handler});
};
// TODO(kevinb): have functions return an object and call defineFunction with
// that object in this file instead of relying on side-effects.
import "./functions/sqrt";
import "./functions/color";
import "./functions/text";
import "./functions/math";
import "./functions/enclose";
import "./functions/overline";
import "./functions/underline";
import "./functions/rule";
import "./functions/kern";
import "./functions/phantom";
// Math class commands except \mathop
defineFunction("mclass", [
"\\mathord", "\\mathbin", "\\mathrel", "\\mathopen",
"\\mathclose", "\\mathpunct", "\\mathinner",
], {
numArgs: 1,
}, function(context, args) {
const body = args[0];
return {
type: "mclass",
mclass: "m" + context.funcName.substr(5),
value: ordargument(body),
};
});
// Build a relation or stacked op by placing one symbol on top of another
defineFunction("mclass", ["\\stackrel", "\\overset", "\\underset"], {
numArgs: 2,
}, function(context, args) {
const mathAxisArg = args[1];
const shiftedArg = args[0];
const xAxisOp = new ParseNode("op", {
type: "op",
limits: true,
alwaysHandleSupSub: true,
symbol: false,
value: ordargument(mathAxisArg),
}, mathAxisArg.mode);
const supsub = new ParseNode("supsub", {
base: xAxisOp,
sup: context.funcName === "\\underset" ? null : shiftedArg,
sub: context.funcName === "\\underset" ? shiftedArg : null,
}, shiftedArg.mode);
return {
type: "mclass",
mclass: "mrel",
value: [supsub],
};
});
import "./functions/mod";
const singleCharIntegrals: {[string]: string} = {
"\u222b": "\\int",
"\u222c": "\\iint",
"\u222d": "\\iiint",
"\u222e": "\\oint",
};
// There are 2 flags for operators; whether they produce limits in
// displaystyle, and whether they are symbols and should grow in
// displaystyle. These four groups cover the four possible choices.
// No limits, not symbols
defineFunction("op", [
"\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg",
"\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg",
"\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp",
"\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin",
"\\sinh", "\\sh", "\\tan", "\\tanh", "\\tg", "\\th",
], {
numArgs: 0,
}, function(context) {
return {
type: "op",
limits: false,
symbol: false,
body: context.funcName,
};
});
// Limits, not symbols
defineFunction("op", [
"\\det", "\\gcd", "\\inf", "\\lim", "\\max", "\\min", "\\Pr", "\\sup",
], {
numArgs: 0,
}, function(context) {
return {
type: "op",
limits: true,
symbol: false,
body: context.funcName,
};
});
// No limits, symbols
defineFunction("op", [
"\\int", "\\iint", "\\iiint", "\\oint", "\u222b", "\u222c",
"\u222d", "\u222e",
], {
numArgs: 0,
}, function(context) {
let fName = context.funcName;
if (fName.length === 1) {
fName = singleCharIntegrals[fName];
}
return {
type: "op",
limits: false,
symbol: true,
body: fName,
};
});
import "./functions/op";
import "./functions/operatorname";
import "./functions/genfrac";
import "./functions/lap";
import "./functions/smash";
import "./functions/delimsizing";
import "./functions/sizing";
import "./functions/styling";
import "./functions/font";
import "./functions/accent";
// Horizontal stretchy braces
defineFunction("horizBrace", [
"\\overbrace", "\\underbrace",
], {
numArgs: 1,
}, function(context, args) {
const base = args[0];
return {
type: "horizBrace",
label: context.funcName,
isOver: /^\\over/.test(context.funcName),
base: base,
};
});
// Stretchy accents under the body
import "./functions/accentunder";
// Stretchy arrows with an optional argument
defineFunction("xArrow", [
"\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow",
"\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow",
"\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown",
"\\xrightharpoonup", "\\xleftharpoondown", "\\xleftharpoonup",
"\\xrightleftharpoons", "\\xleftrightharpoons", "\\xlongequal",
"\\xtwoheadrightarrow", "\\xtwoheadleftarrow", "\\xtofrom",
// The next 3 functions are here to support the mhchem extension.
// Direct use of these functions is discouraged and may break someday.
"\\xrightleftarrows", "\\xrightequilibrium",
"\\xleftequilibrium",
], {
numArgs: 1,
numOptionalArgs: 1,
}, function(context, args, optArgs) {
const below = optArgs[0];
const body = args[0];
return {
type: "xArrow", // x for extensible
label: context.funcName,
body: body,
below: below,
};
});
// Infix generalized fractions
defineFunction("infix", ["\\over", "\\choose", "\\atop"], {
numArgs: 0,
infix: true,
}, function(context) {
let replaceWith;
switch (context.funcName) {
case "\\over":
replaceWith = "\\frac";
break;
case "\\choose":
replaceWith = "\\binom";
break;
case "\\atop":
replaceWith = "\\\\atopfrac";
break;
default:
throw new Error("Unrecognized infix genfrac command");
}
return {
type: "infix",
replaceWith: replaceWith,
token: context.token,
};
});
// Row breaks for aligned data
defineFunction("cr", ["\\\\", "\\cr"], {
numArgs: 0,
numOptionalArgs: 1,
argTypes: ["size"],
}, function(context, args, optArgs) {
const size = optArgs[0];
return {
type: "cr",
size: size,
};
});
// Environment delimiters
defineFunction("environment", ["\\begin", "\\end"], {
numArgs: 1,
argTypes: ["text"],
}, function(context, args) {
const nameGroup = args[0];
if (nameGroup.type !== "ordgroup") {
throw new ParseError("Invalid environment name", nameGroup);
}
let name = "";
for (let i = 0; i < nameGroup.value.length; ++i) {
name += nameGroup.value[i].value;
}
return {
type: "environment",
name: name,
nameGroup: nameGroup,
};
});
// Box manipulation
defineFunction("raisebox", ["\\raisebox"], {
numArgs: 2,
argTypes: ["size", "text"],
allowedInText: true,
}, function(context, args) {
const amount = args[0];
const body = args[1];
return {
type: "raisebox",
dy: amount,
body: body,
value: ordargument(body),
};
});
import "./functions/verb";
// Hyperlinks
import "./functions/href";
// MathChoice
import "./functions/mathchoice";