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