www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

functions.js (19473B)


      1 const utils = require("./utils");
      2 const ParseError = require("./ParseError");
      3 const parseData = require("./parseData");
      4 const ParseNode = parseData.ParseNode;
      5 
      6 /* This file contains a list of functions that we parse, identified by
      7  * the calls to defineFunction.
      8  *
      9  * The first argument to defineFunction is a single name or a list of names.
     10  * All functions named in such a list will share a single implementation.
     11  *
     12  * Each declared function can have associated properties, which
     13  * include the following:
     14  *
     15  *  - numArgs: The number of arguments the function takes.
     16  *             If this is the only property, it can be passed as a number
     17  *             instead of an element of a properties object.
     18  *  - argTypes: (optional) An array corresponding to each argument of the
     19  *              function, giving the type of argument that should be parsed. Its
     20  *              length should be equal to `numArgs + numOptionalArgs`. Valid
     21  *              types:
     22  *               - "size": A size-like thing, such as "1em" or "5ex"
     23  *               - "color": An html color, like "#abc" or "blue"
     24  *               - "original": The same type as the environment that the
     25  *                             function being parsed is in (e.g. used for the
     26  *                             bodies of functions like \color where the first
     27  *                             argument is special and the second argument is
     28  *                             parsed normally)
     29  *              Other possible types (probably shouldn't be used)
     30  *               - "text": Text-like (e.g. \text)
     31  *               - "math": Normal math
     32  *              If undefined, this will be treated as an appropriate length
     33  *              array of "original" strings
     34  *  - greediness: (optional) The greediness of the function to use ungrouped
     35  *                arguments.
     36  *
     37  *                E.g. if you have an expression
     38  *                  \sqrt \frac 1 2
     39  *                since \frac has greediness=2 vs \sqrt's greediness=1, \frac
     40  *                will use the two arguments '1' and '2' as its two arguments,
     41  *                then that whole function will be used as the argument to
     42  *                \sqrt. On the other hand, the expressions
     43  *                  \frac \frac 1 2 3
     44  *                and
     45  *                  \frac \sqrt 1 2
     46  *                will fail because \frac and \frac have equal greediness
     47  *                and \sqrt has a lower greediness than \frac respectively. To
     48  *                make these parse, we would have to change them to:
     49  *                  \frac {\frac 1 2} 3
     50  *                and
     51  *                  \frac {\sqrt 1} 2
     52  *
     53  *                The default value is `1`
     54  *  - allowedInText: (optional) Whether or not the function is allowed inside
     55  *                   text mode (default false)
     56  *  - numOptionalArgs: (optional) The number of optional arguments the function
     57  *                     should parse. If the optional arguments aren't found,
     58  *                     `null` will be passed to the handler in their place.
     59  *                     (default 0)
     60  *  - infix: (optional) Must be true if the function is an infix operator.
     61  *
     62  * The last argument is that implementation, the handler for the function(s).
     63  * It is called to handle these functions and their arguments.
     64  * It receives two arguments:
     65  *  - context contains information and references provided by the parser
     66  *  - args is an array of arguments obtained from TeX input
     67  * The context contains the following properties:
     68  *  - funcName: the text (i.e. name) of the function, including \
     69  *  - parser: the parser object
     70  *  - lexer: the lexer object
     71  *  - positions: the positions in the overall string of the function
     72  *               and the arguments.
     73  * The latter three should only be used to produce error messages.
     74  *
     75  * The function should return an object with the following keys:
     76  *  - type: The type of element that this is. This is then used in
     77  *          buildHTML/buildMathML to determine which function
     78  *          should be called to build this node into a DOM node
     79  * Any other data can be added to the object, which will be passed
     80  * in to the function in buildHTML/buildMathML as `group.value`.
     81  */
     82 
     83 function defineFunction(names, props, handler) {
     84     if (typeof names === "string") {
     85         names = [names];
     86     }
     87     if (typeof props === "number") {
     88         props = { numArgs: props };
     89     }
     90     // Set default values of functions
     91     const data = {
     92         numArgs: props.numArgs,
     93         argTypes: props.argTypes,
     94         greediness: (props.greediness === undefined) ? 1 : props.greediness,
     95         allowedInText: !!props.allowedInText,
     96         numOptionalArgs: props.numOptionalArgs || 0,
     97         infix: !!props.infix,
     98         handler: handler,
     99     };
    100     for (let i = 0; i < names.length; ++i) {
    101         module.exports[names[i]] = data;
    102     }
    103 }
    104 
    105 // Since the corresponding buildHTML/buildMathML function expects a
    106 // list of elements, we normalize for different kinds of arguments
    107 const ordargument = function(arg) {
    108     if (arg.type === "ordgroup") {
    109         return arg.value;
    110     } else {
    111         return [arg];
    112     }
    113 };
    114 
    115 // A normal square root
    116 defineFunction("\\sqrt", {
    117     numArgs: 1,
    118     numOptionalArgs: 1,
    119 }, function(context, args) {
    120     const index = args[0];
    121     const body = args[1];
    122     return {
    123         type: "sqrt",
    124         body: body,
    125         index: index,
    126     };
    127 });
    128 
    129 // Non-mathy text, possibly in a font
    130 const textFunctionStyles = {
    131     "\\text": undefined, "\\textrm": "mathrm", "\\textsf": "mathsf",
    132     "\\texttt": "mathtt", "\\textnormal": "mathrm", "\\textbf": "mathbf",
    133     "\\textit": "textit",
    134 };
    135 
    136 defineFunction([
    137     "\\text", "\\textrm", "\\textsf", "\\texttt", "\\textnormal",
    138     "\\textbf", "\\textit",
    139 ], {
    140     numArgs: 1,
    141     argTypes: ["text"],
    142     greediness: 2,
    143     allowedInText: true,
    144 }, function(context, args) {
    145     const body = args[0];
    146     return {
    147         type: "text",
    148         body: ordargument(body),
    149         style: textFunctionStyles[context.funcName],
    150     };
    151 });
    152 
    153 // A two-argument custom color
    154 defineFunction("\\color", {
    155     numArgs: 2,
    156     allowedInText: true,
    157     greediness: 3,
    158     argTypes: ["color", "original"],
    159 }, function(context, args) {
    160     const color = args[0];
    161     const body = args[1];
    162     return {
    163         type: "color",
    164         color: color.value,
    165         value: ordargument(body),
    166     };
    167 });
    168 
    169 // An overline
    170 defineFunction("\\overline", {
    171     numArgs: 1,
    172 }, function(context, args) {
    173     const body = args[0];
    174     return {
    175         type: "overline",
    176         body: body,
    177     };
    178 });
    179 
    180 // An underline
    181 defineFunction("\\underline", {
    182     numArgs: 1,
    183 }, function(context, args) {
    184     const body = args[0];
    185     return {
    186         type: "underline",
    187         body: body,
    188     };
    189 });
    190 
    191 // A box of the width and height
    192 defineFunction("\\rule", {
    193     numArgs: 2,
    194     numOptionalArgs: 1,
    195     argTypes: ["size", "size", "size"],
    196 }, function(context, args) {
    197     const shift = args[0];
    198     const width = args[1];
    199     const height = args[2];
    200     return {
    201         type: "rule",
    202         shift: shift && shift.value,
    203         width: width.value,
    204         height: height.value,
    205     };
    206 });
    207 
    208 // TODO: In TeX, \mkern only accepts mu-units, and \kern does not accept
    209 // mu-units. In current KaTeX we relax this; both commands accept any unit.
    210 defineFunction(["\\kern", "\\mkern"], {
    211     numArgs: 1,
    212     argTypes: ["size"],
    213 }, function(context, args) {
    214     return {
    215         type: "kern",
    216         dimension: args[0].value,
    217     };
    218 });
    219 
    220 // A KaTeX logo
    221 defineFunction("\\KaTeX", {
    222     numArgs: 0,
    223 }, function(context) {
    224     return {
    225         type: "katex",
    226     };
    227 });
    228 
    229 defineFunction("\\phantom", {
    230     numArgs: 1,
    231 }, function(context, args) {
    232     const body = args[0];
    233     return {
    234         type: "phantom",
    235         value: ordargument(body),
    236     };
    237 });
    238 
    239 // Math class commands except \mathop
    240 defineFunction([
    241     "\\mathord", "\\mathbin", "\\mathrel", "\\mathopen",
    242     "\\mathclose", "\\mathpunct", "\\mathinner",
    243 ], {
    244     numArgs: 1,
    245 }, function(context, args) {
    246     const body = args[0];
    247     return {
    248         type: "mclass",
    249         mclass: "m" + context.funcName.substr(5),
    250         value: ordargument(body),
    251     };
    252 });
    253 
    254 // Build a relation by placing one symbol on top of another
    255 defineFunction("\\stackrel", {
    256     numArgs: 2,
    257 }, function(context, args) {
    258     const top = args[0];
    259     const bottom = args[1];
    260 
    261     const bottomop = new ParseNode("op", {
    262         type: "op",
    263         limits: true,
    264         alwaysHandleSupSub: true,
    265         symbol: false,
    266         value: ordargument(bottom),
    267     }, bottom.mode);
    268 
    269     const supsub = new ParseNode("supsub", {
    270         base: bottomop,
    271         sup: top,
    272         sub: null,
    273     }, top.mode);
    274 
    275     return {
    276         type: "mclass",
    277         mclass: "mrel",
    278         value: [supsub],
    279     };
    280 });
    281 
    282 // \mod-type functions
    283 defineFunction("\\bmod", {
    284     numArgs: 0,
    285 }, function(context, args) {
    286     return {
    287         type: "mod",
    288         modType: "bmod",
    289         value: null,
    290     };
    291 });
    292 
    293 defineFunction(["\\pod", "\\pmod", "\\mod"], {
    294     numArgs: 1,
    295 }, function(context, args) {
    296     const body = args[0];
    297     return {
    298         type: "mod",
    299         modType: context.funcName.substr(1),
    300         value: ordargument(body),
    301     };
    302 });
    303 
    304 // Extra data needed for the delimiter handler down below
    305 const delimiterSizes = {
    306     "\\bigl" : {mclass: "mopen",    size: 1},
    307     "\\Bigl" : {mclass: "mopen",    size: 2},
    308     "\\biggl": {mclass: "mopen",    size: 3},
    309     "\\Biggl": {mclass: "mopen",    size: 4},
    310     "\\bigr" : {mclass: "mclose",   size: 1},
    311     "\\Bigr" : {mclass: "mclose",   size: 2},
    312     "\\biggr": {mclass: "mclose",   size: 3},
    313     "\\Biggr": {mclass: "mclose",   size: 4},
    314     "\\bigm" : {mclass: "mrel",     size: 1},
    315     "\\Bigm" : {mclass: "mrel",     size: 2},
    316     "\\biggm": {mclass: "mrel",     size: 3},
    317     "\\Biggm": {mclass: "mrel",     size: 4},
    318     "\\big"  : {mclass: "mord",     size: 1},
    319     "\\Big"  : {mclass: "mord",     size: 2},
    320     "\\bigg" : {mclass: "mord",     size: 3},
    321     "\\Bigg" : {mclass: "mord",     size: 4},
    322 };
    323 
    324 const delimiters = [
    325     "(", ")", "[", "\\lbrack", "]", "\\rbrack",
    326     "\\{", "\\lbrace", "\\}", "\\rbrace",
    327     "\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
    328     "<", ">", "\\langle", "\\rangle", "\\lt", "\\gt",
    329     "\\lvert", "\\rvert", "\\lVert", "\\rVert",
    330     "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache",
    331     "/", "\\backslash",
    332     "|", "\\vert", "\\|", "\\Vert",
    333     "\\uparrow", "\\Uparrow",
    334     "\\downarrow", "\\Downarrow",
    335     "\\updownarrow", "\\Updownarrow",
    336     ".",
    337 ];
    338 
    339 const fontAliases = {
    340     "\\Bbb": "\\mathbb",
    341     "\\bold": "\\mathbf",
    342     "\\frak": "\\mathfrak",
    343 };
    344 
    345 // Single-argument color functions
    346 defineFunction([
    347     "\\blue", "\\orange", "\\pink", "\\red",
    348     "\\green", "\\gray", "\\purple",
    349     "\\blueA", "\\blueB", "\\blueC", "\\blueD", "\\blueE",
    350     "\\tealA", "\\tealB", "\\tealC", "\\tealD", "\\tealE",
    351     "\\greenA", "\\greenB", "\\greenC", "\\greenD", "\\greenE",
    352     "\\goldA", "\\goldB", "\\goldC", "\\goldD", "\\goldE",
    353     "\\redA", "\\redB", "\\redC", "\\redD", "\\redE",
    354     "\\maroonA", "\\maroonB", "\\maroonC", "\\maroonD", "\\maroonE",
    355     "\\purpleA", "\\purpleB", "\\purpleC", "\\purpleD", "\\purpleE",
    356     "\\mintA", "\\mintB", "\\mintC",
    357     "\\grayA", "\\grayB", "\\grayC", "\\grayD", "\\grayE",
    358     "\\grayF", "\\grayG", "\\grayH", "\\grayI",
    359     "\\kaBlue", "\\kaGreen",
    360 ], {
    361     numArgs: 1,
    362     allowedInText: true,
    363     greediness: 3,
    364 }, function(context, args) {
    365     const body = args[0];
    366     return {
    367         type: "color",
    368         color: "katex-" + context.funcName.slice(1),
    369         value: ordargument(body),
    370     };
    371 });
    372 
    373 // There are 2 flags for operators; whether they produce limits in
    374 // displaystyle, and whether they are symbols and should grow in
    375 // displaystyle. These four groups cover the four possible choices.
    376 
    377 // No limits, not symbols
    378 defineFunction([
    379     "\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg",
    380     "\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg",
    381     "\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp",
    382     "\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin",
    383     "\\sinh", "\\sh", "\\tan", "\\tanh", "\\tg", "\\th",
    384 ], {
    385     numArgs: 0,
    386 }, function(context) {
    387     return {
    388         type: "op",
    389         limits: false,
    390         symbol: false,
    391         body: context.funcName,
    392     };
    393 });
    394 
    395 // Limits, not symbols
    396 defineFunction([
    397     "\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max",
    398     "\\min", "\\Pr", "\\sup",
    399 ], {
    400     numArgs: 0,
    401 }, function(context) {
    402     return {
    403         type: "op",
    404         limits: true,
    405         symbol: false,
    406         body: context.funcName,
    407     };
    408 });
    409 
    410 // No limits, symbols
    411 defineFunction([
    412     "\\int", "\\iint", "\\iiint", "\\oint",
    413 ], {
    414     numArgs: 0,
    415 }, function(context) {
    416     return {
    417         type: "op",
    418         limits: false,
    419         symbol: true,
    420         body: context.funcName,
    421     };
    422 });
    423 
    424 // Limits, symbols
    425 defineFunction([
    426     "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
    427     "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
    428     "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint",
    429 ], {
    430     numArgs: 0,
    431 }, function(context) {
    432     return {
    433         type: "op",
    434         limits: true,
    435         symbol: true,
    436         body: context.funcName,
    437     };
    438 });
    439 
    440 // \mathop class command
    441 defineFunction("\\mathop", {
    442     numArgs: 1,
    443 }, function(context, args) {
    444     const body = args[0];
    445     return {
    446         type: "op",
    447         limits: false,
    448         symbol: false,
    449         value: ordargument(body),
    450     };
    451 });
    452 
    453 // Fractions
    454 defineFunction([
    455     "\\dfrac", "\\frac", "\\tfrac",
    456     "\\dbinom", "\\binom", "\\tbinom",
    457     "\\\\atopfrac", // can’t be entered directly
    458 ], {
    459     numArgs: 2,
    460     greediness: 2,
    461 }, function(context, args) {
    462     const numer = args[0];
    463     const denom = args[1];
    464     let hasBarLine;
    465     let leftDelim = null;
    466     let rightDelim = null;
    467     let size = "auto";
    468 
    469     switch (context.funcName) {
    470         case "\\dfrac":
    471         case "\\frac":
    472         case "\\tfrac":
    473             hasBarLine = true;
    474             break;
    475         case "\\\\atopfrac":
    476             hasBarLine = false;
    477             break;
    478         case "\\dbinom":
    479         case "\\binom":
    480         case "\\tbinom":
    481             hasBarLine = false;
    482             leftDelim = "(";
    483             rightDelim = ")";
    484             break;
    485         default:
    486             throw new Error("Unrecognized genfrac command");
    487     }
    488 
    489     switch (context.funcName) {
    490         case "\\dfrac":
    491         case "\\dbinom":
    492             size = "display";
    493             break;
    494         case "\\tfrac":
    495         case "\\tbinom":
    496             size = "text";
    497             break;
    498     }
    499 
    500     return {
    501         type: "genfrac",
    502         numer: numer,
    503         denom: denom,
    504         hasBarLine: hasBarLine,
    505         leftDelim: leftDelim,
    506         rightDelim: rightDelim,
    507         size: size,
    508     };
    509 });
    510 
    511 // Left and right overlap functions
    512 defineFunction(["\\llap", "\\rlap"], {
    513     numArgs: 1,
    514     allowedInText: true,
    515 }, function(context, args) {
    516     const body = args[0];
    517     return {
    518         type: context.funcName.slice(1),
    519         body: body,
    520     };
    521 });
    522 
    523 // Delimiter functions
    524 const checkDelimiter = function(delim, context) {
    525     if (utils.contains(delimiters, delim.value)) {
    526         return delim;
    527     } else {
    528         throw new ParseError(
    529             "Invalid delimiter: '" + delim.value + "' after '" +
    530             context.funcName + "'", delim);
    531     }
    532 };
    533 
    534 defineFunction([
    535     "\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
    536     "\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
    537     "\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
    538     "\\big",  "\\Big",  "\\bigg",  "\\Bigg",
    539 ], {
    540     numArgs: 1,
    541 }, function(context, args) {
    542     const delim = checkDelimiter(args[0], context);
    543 
    544     return {
    545         type: "delimsizing",
    546         size: delimiterSizes[context.funcName].size,
    547         mclass: delimiterSizes[context.funcName].mclass,
    548         value: delim.value,
    549     };
    550 });
    551 
    552 defineFunction([
    553     "\\left", "\\right",
    554 ], {
    555     numArgs: 1,
    556 }, function(context, args) {
    557     const delim = checkDelimiter(args[0], context);
    558 
    559     // \left and \right are caught somewhere in Parser.js, which is
    560     // why this data doesn't match what is in buildHTML.
    561     return {
    562         type: "leftright",
    563         value: delim.value,
    564     };
    565 });
    566 
    567 defineFunction("\\middle", {
    568     numArgs: 1,
    569 }, function(context, args) {
    570     const delim = checkDelimiter(args[0], context);
    571     if (!context.parser.leftrightDepth) {
    572         throw new ParseError("\\middle without preceding \\left", delim);
    573     }
    574 
    575     return {
    576         type: "middle",
    577         value: delim.value,
    578     };
    579 });
    580 
    581 // Sizing functions (handled in Parser.js explicitly, hence no handler)
    582 defineFunction([
    583     "\\tiny", "\\scriptsize", "\\footnotesize", "\\small",
    584     "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
    585 ], 0, null);
    586 
    587 // Style changing functions (handled in Parser.js explicitly, hence no
    588 // handler)
    589 defineFunction([
    590     "\\displaystyle", "\\textstyle", "\\scriptstyle",
    591     "\\scriptscriptstyle",
    592 ], 0, null);
    593 
    594 // Old font changing functions
    595 defineFunction([
    596     "\\rm", "\\sf", "\\tt", "\\bf", "\\it", //"\\sl", "\\sc",
    597 ], 0, null);
    598 
    599 defineFunction([
    600     // styles
    601     "\\mathrm", "\\mathit", "\\mathbf",
    602 
    603     // families
    604     "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf",
    605     "\\mathtt",
    606 
    607     // aliases
    608     "\\Bbb", "\\bold", "\\frak",
    609 ], {
    610     numArgs: 1,
    611     greediness: 2,
    612 }, function(context, args) {
    613     const body = args[0];
    614     let func = context.funcName;
    615     if (func in fontAliases) {
    616         func = fontAliases[func];
    617     }
    618     return {
    619         type: "font",
    620         font: func.slice(1),
    621         body: body,
    622     };
    623 });
    624 
    625 // Accents
    626 defineFunction([
    627     "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
    628     "\\check", "\\hat", "\\vec", "\\dot",
    629     // We don't support expanding accents yet
    630     // "\\widetilde", "\\widehat"
    631 ], {
    632     numArgs: 1,
    633 }, function(context, args) {
    634     const base = args[0];
    635     return {
    636         type: "accent",
    637         accent: context.funcName,
    638         base: base,
    639     };
    640 });
    641 
    642 // Infix generalized fractions
    643 defineFunction(["\\over", "\\choose", "\\atop"], {
    644     numArgs: 0,
    645     infix: true,
    646 }, function(context) {
    647     let replaceWith;
    648     switch (context.funcName) {
    649         case "\\over":
    650             replaceWith = "\\frac";
    651             break;
    652         case "\\choose":
    653             replaceWith = "\\binom";
    654             break;
    655         case "\\atop":
    656             replaceWith = "\\\\atopfrac";
    657             break;
    658         default:
    659             throw new Error("Unrecognized infix genfrac command");
    660     }
    661     return {
    662         type: "infix",
    663         replaceWith: replaceWith,
    664         token: context.token,
    665     };
    666 });
    667 
    668 // Row breaks for aligned data
    669 defineFunction(["\\\\", "\\cr"], {
    670     numArgs: 0,
    671     numOptionalArgs: 1,
    672     argTypes: ["size"],
    673 }, function(context, args) {
    674     const size = args[0];
    675     return {
    676         type: "cr",
    677         size: size,
    678     };
    679 });
    680 
    681 // Environment delimiters
    682 defineFunction(["\\begin", "\\end"], {
    683     numArgs: 1,
    684     argTypes: ["text"],
    685 }, function(context, args) {
    686     const nameGroup = args[0];
    687     if (nameGroup.type !== "ordgroup") {
    688         throw new ParseError("Invalid environment name", nameGroup);
    689     }
    690     let name = "";
    691     for (let i = 0; i < nameGroup.value.length; ++i) {
    692         name += nameGroup.value[i].value;
    693     }
    694     return {
    695         type: "environment",
    696         name: name,
    697         nameGroup: nameGroup,
    698     };
    699 });