latex2im / katex /src /functions.js
da03
.
b498cbf
var utils = require("./utils");
var ParseError = require("./ParseError");
/* 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 \color 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)
*
* 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
var data = {
numArgs: props.numArgs,
argTypes: props.argTypes,
greediness: (props.greediness === undefined) ? 1 : props.greediness,
allowedInText: !!props.allowedInText,
numOptionalArgs: props.numOptionalArgs || 0,
handler: handler,
};
for (var i = 0; i < names.length; ++i) {
module.exports[names[i]] = data;
}
}
// A normal square root
defineFunction("\\sqrt", {
numArgs: 1,
numOptionalArgs: 1,
}, function(context, args) {
var index = args[0];
var body = args[1];
return {
type: "sqrt",
body: body,
index: index,
};
});
// Some non-mathy text
defineFunction(["\\text", "\\mbox", "\\hbox", "\\vbox"], {
numArgs: 1,
argTypes: ["text"],
greediness: 2,
}, function(context, args) {
var body = args[0];
// Since the corresponding buildHTML/buildMathML function expects a
// list of elements, we normalize for different kinds of arguments
// TODO(emily): maybe this should be done somewhere else
var inner;
if (body.type === "ordgroup") {
inner = body.value;
} else {
inner = [body];
}
return {
type: "text",
body: inner,
};
});
// A two-argument custom color
defineFunction("\\color", {
numArgs: 2,
allowedInText: true,
greediness: 3,
argTypes: ["color", "original"],
}, function(context, args) {
var color = args[0];
var body = args[1];
// Normalize the different kinds of bodies (see \text above)
var inner;
if (body.type === "ordgroup") {
inner = body.value;
} else {
inner = [body];
}
return {
type: "color",
color: color.value,
value: inner,
};
});
// An overline
defineFunction("\\overline", {
numArgs: 1,
}, function(context, args) {
var body = args[0];
return {
type: "overline",
body: body,
};
});
// An underline
defineFunction("\\underline", {
numArgs: 1,
}, function(context, args) {
var 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) {
var shift = args[0];
var width = args[1];
var height = args[2];
return {
type: "rule",
shift: shift && shift.value,
width: width.value,
height: height.value,
};
});
// A KaTeX logo
defineFunction("\\KaTeX", {
numArgs: 0,
}, function(context) {
return {
type: "katex",
};
});
defineFunction("\\phantom", {
numArgs: 1,
}, function(context, args) {
var body = args[0];
var inner;
if (body.type === "ordgroup") {
inner = body.value;
} else {
inner = [body];
}
return {
type: "phantom",
value: inner,
};
});
// Extra data needed for the delimiter handler down below
var delimiterSizes = {
"\\bigl" : {type: "open", size: 1},
"\\Bigl" : {type: "open", size: 2},
"\\biggl": {type: "open", size: 3},
"\\Biggl": {type: "open", size: 4},
"\\bigr" : {type: "close", size: 1},
"\\Bigr" : {type: "close", size: 2},
"\\biggr": {type: "close", size: 3},
"\\Biggr": {type: "close", size: 4},
"\\bigm" : {type: "rel", size: 1},
"\\Bigm" : {type: "rel", size: 2},
"\\biggm": {type: "rel", size: 3},
"\\Biggm": {type: "rel", size: 4},
"\\big" : {type: "textord", size: 1},
"\\Big" : {type: "textord", size: 2},
"\\bigg" : {type: "textord", size: 3},
"\\Bigg" : {type: "textord", size: 4},
};
var 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",
".",
];
var 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) {
var body = args[0];
var atoms;
if (body.type === "ordgroup") {
atoms = body.value;
} else {
atoms = [body];
}
return {
type: "color",
color: "katex-" + context.funcName.slice(1),
value: atoms,
};
});
// 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", "\\arg", "\\cos", "\\cosh",
"\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom",
"\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh",
"\\tan", "\\tanh",
], {
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,
};
});
// Fractions
defineFunction([
"\\dfrac", "\\frac", "\\tfrac",
"\\dbinom", "\\binom", "\\tbinom",
], {
numArgs: 2,
greediness: 2,
}, function(context, args) {
var numer = args[0];
var denom = args[1];
var hasBarLine;
var leftDelim = null;
var rightDelim = null;
var size = "auto";
switch (context.funcName) {
case "\\dfrac":
case "\\frac":
case "\\tfrac":
hasBarLine = true;
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) {
var body = args[0];
return {
type: context.funcName.slice(1),
body: body,
};
});
// Delimiter functions
defineFunction([
"\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
"\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
"\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
"\\big", "\\Big", "\\bigg", "\\Bigg",
"\\left", "\\right"
], {
numArgs: 1,
}, function(context, args) {
var delim = args[0];
if (!utils.contains(delimiters, delim.value)) {
throw new ParseError(
"Invalid delimiter: '" + delim.value + "' after '" +
context.funcName + "'",
context.lexer, context.positions[1]);
}
// \left and \right are caught somewhere in Parser.js, which is
// why this data doesn't match what is in buildHTML.
if (context.funcName === "\\left" || context.funcName === "\\right") {
return {
type: "leftright",
value: delim.value,
funcName: context.funcName
};
} else {
return {
type: "delimsizing",
size: delimiterSizes[context.funcName].size,
delimType: delimiterSizes[context.funcName].type,
value: delim.value,
funcName: context.funcName
};
}
});
// Sizing functions (handled in Parser.js explicitly, hence no handler)
defineFunction([
"\\tiny", "\\scriptsize", "\\footnotesize", "\\small",
"\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", "\\textrm", "\\rm", "\\cal", "\\bf", "\\siptstyle", "\\boldmath", "\\it"
], 0, null);
// Style changing functions (handled in Parser.js explicitly, hence no
// handler)
defineFunction([
"\\displaystyle", "\\textstyle", "\\scriptstyle",
"\\scriptscriptstyle",
], 0, null);
defineFunction([
// styles
"\\mathrm", "\\mathit", "\\mathbf","\\mathop","\\stackrel",
// families
"\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf",
"\\mathtt",
"\\label", "\\comment", "\\hspace", "\\vspace", "\\atop", "\\fbox", "\\tag", "\\makebox",
"\\raisebox", "\\framebox", "\\circle", "\\line", "\\put", "\\vphantom", "\\textup", "\\noalign",
// aliases
"\\Bbb", "\\bold", "\\frak",
], {
numArgs: 1,
greediness: 2,
}, function(context, args) {
var body = args[0];
var 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",
// We don't support expanding accents yet
// "\\widetilde", "\\widehat"
], {
numArgs: 1,
}, function(context, args) {
var base = args[0];
return {
type: "accent",
accent: context.funcName,
base: base,
};
});
// Infix generalized fractions
defineFunction(["\\over", "\\choose"], {
numArgs: 0,
}, function(context) {
var replaceWith;
switch (context.funcName) {
case "\\over":
replaceWith = "\\frac";
break;
case "\\choose":
replaceWith = "\\binom";
break;
default:
throw new Error("Unrecognized infix genfrac command");
}
return {
type: "infix",
replaceWith: replaceWith,
};
});
// Row breaks for aligned data
defineFunction(["\\\\", "\\cr"], {
numArgs: 0,
numOptionalArgs: 1,
argTypes: ["size"],
}, function(context, args) {
var size = args[0];
return {
type: "cr",
size: size,
};
});
// Environment delimiters
defineFunction(["\\begin", "\\end"], {
numArgs: 1,
argTypes: ["text"],
}, function(context, args) {
var nameGroup = args[0];
if (nameGroup.type !== "ordgroup") {
throw new ParseError(
"Invalid environment name",
context.lexer, context.positions[1]);
}
var name = "";
for (var i = 0; i < nameGroup.value.length; ++i) {
name += nameGroup.value[i].value;
}
return {
type: "environment",
name: name,
namepos: context.positions[1],
};
});