Spaces:
Running
on
T4
Running
on
T4
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], | |
}; | |
}); | |