Files
KaTeX/src/functions.js
2017-06-15 23:47:51 -04:00

788 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const utils = require("./utils");
const ParseError = require("./ParseError");
const parseData = require("./parseData");
const ParseNode = parseData.ParseNode;
/* 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`.
*/
function defineFunction(names, props, handler) {
if (typeof names === "string") {
names = [names];
}
if (typeof props === "number") {
props = { numArgs: props };
}
// Set default values of functions
const data = {
numArgs: props.numArgs,
argTypes: props.argTypes,
greediness: (props.greediness === undefined) ? 1 : props.greediness,
allowedInText: !!props.allowedInText,
numOptionalArgs: props.numOptionalArgs || 0,
infix: !!props.infix,
handler: handler,
};
for (let i = 0; i < names.length; ++i) {
module.exports[names[i]] = data;
}
}
// Since the corresponding buildHTML/buildMathML function expects a
// list of elements, we normalize for different kinds of arguments
const ordargument = function(arg) {
if (arg.type === "ordgroup") {
return arg.value;
} else {
return [arg];
}
};
// A normal square root
defineFunction("\\sqrt", {
numArgs: 1,
numOptionalArgs: 1,
}, function(context, args) {
const index = args[0];
const body = args[1];
return {
type: "sqrt",
body: body,
index: index,
};
});
// Non-mathy text, possibly in a font
const textFunctionStyles = {
"\\text": undefined, "\\textrm": "mathrm", "\\textsf": "mathsf",
"\\texttt": "mathtt", "\\textnormal": "mathrm", "\\textbf": "mathbf",
"\\textit": "textit",
};
defineFunction([
"\\text", "\\textrm", "\\textsf", "\\texttt", "\\textnormal",
"\\textbf", "\\textit",
], {
numArgs: 1,
argTypes: ["text"],
greediness: 2,
allowedInText: true,
}, function(context, args) {
const body = args[0];
return {
type: "text",
body: ordargument(body),
style: textFunctionStyles[context.funcName],
};
});
// A two-argument custom color
defineFunction("\\textcolor", {
numArgs: 2,
allowedInText: true,
greediness: 3,
argTypes: ["color", "original"],
}, function(context, args) {
const color = args[0];
const body = args[1];
return {
type: "color",
color: color.value,
value: ordargument(body),
};
});
// \color is handled in Parser.js's parseImplicitGroup
defineFunction("\\color", {
numArgs: 1,
allowedInText: true,
greediness: 3,
argTypes: ["color"],
}, null);
// An overline
defineFunction("\\overline", {
numArgs: 1,
}, function(context, args) {
const body = args[0];
return {
type: "overline",
body: body,
};
});
// An underline
defineFunction("\\underline", {
numArgs: 1,
}, function(context, args) {
const body = args[0];
return {
type: "underline",
body: body,
};
});
// A box of the width and height
defineFunction("\\rule", {
numArgs: 2,
numOptionalArgs: 1,
argTypes: ["size", "size", "size"],
}, function(context, args) {
const shift = args[0];
const width = args[1];
const height = args[2];
return {
type: "rule",
shift: shift && shift.value,
width: width.value,
height: height.value,
};
});
// TODO: In TeX, \mkern only accepts mu-units, and \kern does not accept
// mu-units. In current KaTeX we relax this; both commands accept any unit.
defineFunction(["\\kern", "\\mkern"], {
numArgs: 1,
argTypes: ["size"],
}, function(context, args) {
return {
type: "kern",
dimension: args[0].value,
};
});
// A KaTeX logo
defineFunction("\\KaTeX", {
numArgs: 0,
}, function(context) {
return {
type: "katex",
};
});
defineFunction("\\phantom", {
numArgs: 1,
}, function(context, args) {
const body = args[0];
return {
type: "phantom",
value: ordargument(body),
};
});
// Math class commands except \mathop
defineFunction([
"\\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 by placing one symbol on top of another
defineFunction("\\stackrel", {
numArgs: 2,
}, function(context, args) {
const top = args[0];
const bottom = args[1];
const bottomop = new ParseNode("op", {
type: "op",
limits: true,
alwaysHandleSupSub: true,
symbol: false,
value: ordargument(bottom),
}, bottom.mode);
const supsub = new ParseNode("supsub", {
base: bottomop,
sup: top,
sub: null,
}, top.mode);
return {
type: "mclass",
mclass: "mrel",
value: [supsub],
};
});
// \mod-type functions
defineFunction("\\bmod", {
numArgs: 0,
}, function(context, args) {
return {
type: "mod",
modType: "bmod",
value: null,
};
});
defineFunction(["\\pod", "\\pmod", "\\mod"], {
numArgs: 1,
}, function(context, args) {
const body = args[0];
return {
type: "mod",
modType: context.funcName.substr(1),
value: ordargument(body),
};
});
// Extra data needed for the delimiter handler down below
const delimiterSizes = {
"\\bigl" : {mclass: "mopen", size: 1},
"\\Bigl" : {mclass: "mopen", size: 2},
"\\biggl": {mclass: "mopen", size: 3},
"\\Biggl": {mclass: "mopen", size: 4},
"\\bigr" : {mclass: "mclose", size: 1},
"\\Bigr" : {mclass: "mclose", size: 2},
"\\biggr": {mclass: "mclose", size: 3},
"\\Biggr": {mclass: "mclose", size: 4},
"\\bigm" : {mclass: "mrel", size: 1},
"\\Bigm" : {mclass: "mrel", size: 2},
"\\biggm": {mclass: "mrel", size: 3},
"\\Biggm": {mclass: "mrel", size: 4},
"\\big" : {mclass: "mord", size: 1},
"\\Big" : {mclass: "mord", size: 2},
"\\bigg" : {mclass: "mord", size: 3},
"\\Bigg" : {mclass: "mord", size: 4},
};
const delimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
"<", ">", "\\langle", "\\rangle", "\\lt", "\\gt",
"\\lvert", "\\rvert", "\\lVert", "\\rVert",
"\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache",
"/", "\\backslash",
"|", "\\vert", "\\|", "\\Vert",
"\\uparrow", "\\Uparrow",
"\\downarrow", "\\Downarrow",
"\\updownarrow", "\\Updownarrow",
".",
];
const fontAliases = {
"\\Bbb": "\\mathbb",
"\\bold": "\\mathbf",
"\\frak": "\\mathfrak",
};
// Single-argument color functions
defineFunction([
"\\blue", "\\orange", "\\pink", "\\red",
"\\green", "\\gray", "\\purple",
"\\blueA", "\\blueB", "\\blueC", "\\blueD", "\\blueE",
"\\tealA", "\\tealB", "\\tealC", "\\tealD", "\\tealE",
"\\greenA", "\\greenB", "\\greenC", "\\greenD", "\\greenE",
"\\goldA", "\\goldB", "\\goldC", "\\goldD", "\\goldE",
"\\redA", "\\redB", "\\redC", "\\redD", "\\redE",
"\\maroonA", "\\maroonB", "\\maroonC", "\\maroonD", "\\maroonE",
"\\purpleA", "\\purpleB", "\\purpleC", "\\purpleD", "\\purpleE",
"\\mintA", "\\mintB", "\\mintC",
"\\grayA", "\\grayB", "\\grayC", "\\grayD", "\\grayE",
"\\grayF", "\\grayG", "\\grayH", "\\grayI",
"\\kaBlue", "\\kaGreen",
], {
numArgs: 1,
allowedInText: true,
greediness: 3,
}, function(context, args) {
const body = args[0];
return {
type: "color",
color: "katex-" + context.funcName.slice(1),
value: ordargument(body),
};
});
// 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([
"\\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([
"\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max",
"\\min", "\\Pr", "\\sup",
], {
numArgs: 0,
}, function(context) {
return {
type: "op",
limits: true,
symbol: false,
body: context.funcName,
};
});
// No limits, symbols
defineFunction([
"\\int", "\\iint", "\\iiint", "\\oint",
], {
numArgs: 0,
}, function(context) {
return {
type: "op",
limits: false,
symbol: true,
body: context.funcName,
};
});
// Limits, symbols
defineFunction([
"\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
"\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
"\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint",
], {
numArgs: 0,
}, function(context) {
return {
type: "op",
limits: true,
symbol: true,
body: context.funcName,
};
});
// \mathop class command
defineFunction("\\mathop", {
numArgs: 1,
}, function(context, args) {
const body = args[0];
return {
type: "op",
limits: false,
symbol: false,
value: ordargument(body),
};
});
// Fractions
defineFunction([
"\\dfrac", "\\frac", "\\tfrac",
"\\dbinom", "\\binom", "\\tbinom",
"\\\\atopfrac", // cant be entered directly
], {
numArgs: 2,
greediness: 2,
}, function(context, args) {
const numer = args[0];
const denom = args[1];
let hasBarLine;
let leftDelim = null;
let rightDelim = null;
let size = "auto";
switch (context.funcName) {
case "\\dfrac":
case "\\frac":
case "\\tfrac":
hasBarLine = true;
break;
case "\\\\atopfrac":
hasBarLine = false;
break;
case "\\dbinom":
case "\\binom":
case "\\tbinom":
hasBarLine = false;
leftDelim = "(";
rightDelim = ")";
break;
default:
throw new Error("Unrecognized genfrac command");
}
switch (context.funcName) {
case "\\dfrac":
case "\\dbinom":
size = "display";
break;
case "\\tfrac":
case "\\tbinom":
size = "text";
break;
}
return {
type: "genfrac",
numer: numer,
denom: denom,
hasBarLine: hasBarLine,
leftDelim: leftDelim,
rightDelim: rightDelim,
size: size,
};
});
// Left and right overlap functions
defineFunction(["\\llap", "\\rlap"], {
numArgs: 1,
allowedInText: true,
}, function(context, args) {
const body = args[0];
return {
type: context.funcName.slice(1),
body: body,
};
});
// Delimiter functions
const checkDelimiter = function(delim, context) {
if (utils.contains(delimiters, delim.value)) {
return delim;
} else {
throw new ParseError(
"Invalid delimiter: '" + delim.value + "' after '" +
context.funcName + "'", delim);
}
};
defineFunction([
"\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
"\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
"\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
"\\big", "\\Big", "\\bigg", "\\Bigg",
], {
numArgs: 1,
}, function(context, args) {
const delim = checkDelimiter(args[0], context);
return {
type: "delimsizing",
size: delimiterSizes[context.funcName].size,
mclass: delimiterSizes[context.funcName].mclass,
value: delim.value,
};
});
defineFunction([
"\\left", "\\right",
], {
numArgs: 1,
}, function(context, args) {
const delim = checkDelimiter(args[0], context);
// \left and \right are caught somewhere in Parser.js, which is
// why this data doesn't match what is in buildHTML.
return {
type: "leftright",
value: delim.value,
};
});
defineFunction("\\middle", {
numArgs: 1,
}, function(context, args) {
const delim = checkDelimiter(args[0], context);
if (!context.parser.leftrightDepth) {
throw new ParseError("\\middle without preceding \\left", delim);
}
return {
type: "middle",
value: delim.value,
};
});
// Sizing functions (handled in Parser.js explicitly, hence no handler)
defineFunction([
"\\tiny", "\\scriptsize", "\\footnotesize", "\\small",
"\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
], 0, null);
// Style changing functions (handled in Parser.js explicitly, hence no
// handler)
defineFunction([
"\\displaystyle", "\\textstyle", "\\scriptstyle",
"\\scriptscriptstyle",
], 0, null);
// Old font changing functions
defineFunction([
"\\rm", "\\sf", "\\tt", "\\bf", "\\it", //"\\sl", "\\sc",
], 0, null);
defineFunction([
// styles
"\\mathrm", "\\mathit", "\\mathbf",
// families
"\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf",
"\\mathtt",
// aliases
"\\Bbb", "\\bold", "\\frak",
], {
numArgs: 1,
greediness: 2,
}, function(context, args) {
const body = args[0];
let func = context.funcName;
if (func in fontAliases) {
func = fontAliases[func];
}
return {
type: "font",
font: func.slice(1),
body: body,
};
});
// Accents
defineFunction([
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
"\\check", "\\hat", "\\vec", "\\dot",
"\\widehat", "\\widetilde", "\\overrightarrow", "\\overleftarrow",
"\\Overrightarrow", "\\overleftrightarrow", "\\overgroup",
"\\overlinesegment", "\\overleftharpoon", "\\overrightharpoon",
], {
numArgs: 1,
}, function(context, args) {
const base = args[0];
const isStretchy = !utils.contains([
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
"\\check", "\\hat", "\\vec", "\\dot",
], context.funcName);
const isShifty = !isStretchy || utils.contains([
"\\widehat", "\\widetilde",
], context.funcName);
return {
type: "accent",
label: context.funcName,
isStretchy: isStretchy,
isShifty: isShifty,
value: ordargument(base),
base: base,
};
});
// Horizontal stretchy braces
defineFunction([
"\\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
defineFunction([
"\\underleftarrow", "\\underrightarrow", "\\underleftrightarrow",
"\\undergroup", "\\underlinesegment", "\\undertilde",
], {
numArgs: 1,
}, function(context, args) {
const body = args[0];
return {
type: "accentUnder",
label: context.funcName,
value: ordargument(body),
body: body,
};
});
// Stretchy arrows with an optional argument
defineFunction([
"\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow",
"\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow",
"\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown",
"\\xrightharpoonup", "\\xleftharpoondown", "\\xleftharpoonup",
"\\xrightleftharpoons", "\\xleftrightharpoons", "\\xLongequal",
"\\xtwoheadrightarrow", "\\xtwoheadleftarrow", "\\xLongequal",
"\\xtofrom",
], {
numArgs: 1,
numOptionalArgs: 1,
}, function(context, args) {
const below = args[0];
const body = args[1];
return {
type: "xArrow", // x for extensible
label: context.funcName,
body: body,
below: below,
};
});
// enclose
defineFunction(["\\cancel", "\\bcancel", "\\xcancel", "\\sout", "\\fbox"], {
numArgs: 1,
}, function(context, args) {
const body = args[0];
return {
type: "enclose",
label: context.funcName,
body: body,
};
});
// Infix generalized fractions
defineFunction(["\\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"], {
numArgs: 0,
numOptionalArgs: 1,
argTypes: ["size"],
}, function(context, args) {
const size = args[0];
return {
type: "cr",
size: size,
};
});
// Environment delimiters
defineFunction(["\\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,
};
});