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