
Summary: On https://app.asana.com/0/34646644303310/33935538887378, @eater requested we add some new colors to KaTeX, which lives in the spin-off Khan/KaTeX open source project. (See screenshot for colors.) I added these colors to KaTeX so math typesetting tools in exercises have access to them. I used "blueA", "blueB", etc. because dashes and numbers aren't supported in KaTeX/LaTeX functions. The actual mapping of color name => hex value is in "Options", and the listing of colors available for typesetting is in "functions". See also https://phabricator.khanacademy.org/D18158 for the related additions to utils/math.js and KAthJax. Test Plan: - Set up the KaTeX dev environment (instructions taken from https://github.com/Khan/KaTeX/blob/master/CONTRIBUTING.md): ``` cd KaTeX make setup make serve ``` - Now that the server is up and running, visit http://localhost:7936/ to try live typesetting. Enter the following LaTeX code to try the new colors: ``` \blueE{e=mc^2} ``` - Try other new colors including \redD, \mintC, \grayH, \kaBlue, etc. - Old colors like \orange should still work. - Run the Jasmine test suite at http://localhost:7936/test/test.html. Reviewers: emily Reviewed By: emily Subscribers: nataliefitzgerald, eater, cameron, david Differential Revision: https://phabricator.khanacademy.org/D18152
554 lines
17 KiB
JavaScript
554 lines
17 KiB
JavaScript
var utils = require("./utils");
|
|
var ParseError = require("./ParseError");
|
|
|
|
// This file contains a list of functions that we parse. The functions map
|
|
// contains the following data:
|
|
|
|
/*
|
|
* Keys are the name of the functions to parse
|
|
* The data contains the following keys:
|
|
* - numArgs: The number of arguments the function takes.
|
|
* - 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)
|
|
* - handler: The function that is called to handle this function and its
|
|
* arguments. The arguments are:
|
|
* - func: the text of the function
|
|
* - [args]: the next arguments are the arguments to the function,
|
|
* of which there are numArgs of them
|
|
* - positions: the positions in the overall string of the function
|
|
* and the arguments. 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`.
|
|
*/
|
|
|
|
var functions = {
|
|
// A normal square root
|
|
"\\sqrt": {
|
|
numArgs: 1,
|
|
numOptionalArgs: 1,
|
|
handler: function(func, index, body, positions) {
|
|
return {
|
|
type: "sqrt",
|
|
body: body,
|
|
index: index
|
|
};
|
|
}
|
|
},
|
|
|
|
// Some non-mathy text
|
|
"\\text": {
|
|
numArgs: 1,
|
|
argTypes: ["text"],
|
|
greediness: 2,
|
|
handler: function(func, body) {
|
|
// 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
|
|
"\\color": {
|
|
numArgs: 2,
|
|
allowedInText: true,
|
|
greediness: 3,
|
|
argTypes: ["color", "original"],
|
|
handler: function(func, color, body) {
|
|
// 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
|
|
"\\overline": {
|
|
numArgs: 1,
|
|
handler: function(func, body) {
|
|
return {
|
|
type: "overline",
|
|
body: body
|
|
};
|
|
}
|
|
},
|
|
|
|
// A box of the width and height
|
|
"\\rule": {
|
|
numArgs: 2,
|
|
numOptionalArgs: 1,
|
|
argTypes: ["size", "size", "size"],
|
|
handler: function(func, shift, width, height) {
|
|
return {
|
|
type: "rule",
|
|
shift: shift && shift.value,
|
|
width: width.value,
|
|
height: height.value
|
|
};
|
|
}
|
|
},
|
|
|
|
// A KaTeX logo
|
|
"\\KaTeX": {
|
|
numArgs: 0,
|
|
handler: function(func) {
|
|
return {
|
|
type: "katex"
|
|
};
|
|
}
|
|
},
|
|
|
|
"\\phantom": {
|
|
numArgs: 1,
|
|
handler: function(func, body) {
|
|
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",
|
|
"/", "\\backslash",
|
|
"|", "\\vert", "\\|", "\\Vert",
|
|
"\\uparrow", "\\Uparrow",
|
|
"\\downarrow", "\\Downarrow",
|
|
"\\updownarrow", "\\Updownarrow",
|
|
"."
|
|
];
|
|
|
|
/*
|
|
* This is a list of functions which each have the same function but have
|
|
* different names so that we don't have to duplicate the data a bunch of times.
|
|
* Each element in the list is an object with the following keys:
|
|
* - funcs: A list of function names to be associated with the data
|
|
* - data: An objecty with the same data as in each value of the `function`
|
|
* table above
|
|
*/
|
|
var duplicatedFunctions = [
|
|
// Single-argument color functions
|
|
{
|
|
funcs: [
|
|
"\\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"
|
|
],
|
|
data: {
|
|
numArgs: 1,
|
|
allowedInText: true,
|
|
greediness: 3,
|
|
handler: function(func, body) {
|
|
var atoms;
|
|
if (body.type === "ordgroup") {
|
|
atoms = body.value;
|
|
} else {
|
|
atoms = [body];
|
|
}
|
|
|
|
return {
|
|
type: "color",
|
|
color: "katex-" + func.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
|
|
{
|
|
funcs: [
|
|
"\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh",
|
|
"\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom",
|
|
"\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh",
|
|
"\\tan","\\tanh"
|
|
],
|
|
data: {
|
|
numArgs: 0,
|
|
handler: function(func) {
|
|
return {
|
|
type: "op",
|
|
limits: false,
|
|
symbol: false,
|
|
body: func
|
|
};
|
|
}
|
|
}
|
|
},
|
|
|
|
// Limits, not symbols
|
|
{
|
|
funcs: [
|
|
"\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max",
|
|
"\\min", "\\Pr", "\\sup"
|
|
],
|
|
data: {
|
|
numArgs: 0,
|
|
handler: function(func) {
|
|
return {
|
|
type: "op",
|
|
limits: true,
|
|
symbol: false,
|
|
body: func
|
|
};
|
|
}
|
|
}
|
|
},
|
|
|
|
// No limits, symbols
|
|
{
|
|
funcs: [
|
|
"\\int", "\\iint", "\\iiint", "\\oint"
|
|
],
|
|
data: {
|
|
numArgs: 0,
|
|
handler: function(func) {
|
|
return {
|
|
type: "op",
|
|
limits: false,
|
|
symbol: true,
|
|
body: func
|
|
};
|
|
}
|
|
}
|
|
},
|
|
|
|
// Limits, symbols
|
|
{
|
|
funcs: [
|
|
"\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
|
|
"\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
|
|
"\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint"
|
|
],
|
|
data: {
|
|
numArgs: 0,
|
|
handler: function(func) {
|
|
return {
|
|
type: "op",
|
|
limits: true,
|
|
symbol: true,
|
|
body: func
|
|
};
|
|
}
|
|
}
|
|
},
|
|
|
|
// Fractions
|
|
{
|
|
funcs: [
|
|
"\\dfrac", "\\frac", "\\tfrac",
|
|
"\\dbinom", "\\binom", "\\tbinom"
|
|
],
|
|
data: {
|
|
numArgs: 2,
|
|
greediness: 2,
|
|
handler: function(func, numer, denom) {
|
|
var hasBarLine;
|
|
var leftDelim = null;
|
|
var rightDelim = null;
|
|
var size = "auto";
|
|
|
|
switch (func) {
|
|
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 (func) {
|
|
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
|
|
{
|
|
funcs: ["\\llap", "\\rlap"],
|
|
data: {
|
|
numArgs: 1,
|
|
allowedInText: true,
|
|
handler: function(func, body) {
|
|
return {
|
|
type: func.slice(1),
|
|
body: body
|
|
};
|
|
}
|
|
}
|
|
},
|
|
|
|
// Delimiter functions
|
|
{
|
|
funcs: [
|
|
"\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
|
|
"\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
|
|
"\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
|
|
"\\big", "\\Big", "\\bigg", "\\Bigg",
|
|
"\\left", "\\right"
|
|
],
|
|
data: {
|
|
numArgs: 1,
|
|
handler: function(func, delim, positions) {
|
|
if (!utils.contains(delimiters, delim.value)) {
|
|
throw new ParseError(
|
|
"Invalid delimiter: '" + delim.value + "' after '" +
|
|
func + "'",
|
|
this.lexer, positions[1]);
|
|
}
|
|
|
|
// \left and \right are caught somewhere in Parser.js, which is
|
|
// why this data doesn't match what is in buildHTML.
|
|
if (func === "\\left" || func === "\\right") {
|
|
return {
|
|
type: "leftright",
|
|
value: delim.value
|
|
};
|
|
} else {
|
|
return {
|
|
type: "delimsizing",
|
|
size: delimiterSizes[func].size,
|
|
delimType: delimiterSizes[func].type,
|
|
value: delim.value
|
|
};
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Sizing functions (handled in Parser.js explicitly, hence no handler)
|
|
{
|
|
funcs: [
|
|
"\\tiny", "\\scriptsize", "\\footnotesize", "\\small",
|
|
"\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge"
|
|
],
|
|
data: {
|
|
numArgs: 0
|
|
}
|
|
},
|
|
|
|
// Style changing functions (handled in Parser.js explicitly, hence no
|
|
// handler)
|
|
{
|
|
funcs: [
|
|
"\\displaystyle", "\\textstyle", "\\scriptstyle",
|
|
"\\scriptscriptstyle"
|
|
],
|
|
data: {
|
|
numArgs: 0
|
|
}
|
|
},
|
|
|
|
// Accents
|
|
{
|
|
funcs: [
|
|
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
|
|
"\\check", "\\hat", "\\vec", "\\dot"
|
|
// We don't support expanding accents yet
|
|
// "\\widetilde", "\\widehat"
|
|
],
|
|
data: {
|
|
numArgs: 1,
|
|
handler: function(func, base) {
|
|
return {
|
|
type: "accent",
|
|
accent: func,
|
|
base: base
|
|
};
|
|
}
|
|
}
|
|
},
|
|
|
|
// Infix generalized fractions
|
|
{
|
|
funcs: ["\\over", "\\choose"],
|
|
data: {
|
|
numArgs: 0,
|
|
handler: function (func) {
|
|
var replaceWith;
|
|
switch (func) {
|
|
case "\\over":
|
|
replaceWith = "\\frac";
|
|
break;
|
|
case "\\choose":
|
|
replaceWith = "\\binom";
|
|
break;
|
|
default:
|
|
throw new Error("Unrecognized infix genfrac command");
|
|
}
|
|
return {
|
|
type: "infix",
|
|
replaceWith: replaceWith
|
|
};
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
var addFuncsWithData = function(funcs, data) {
|
|
for (var i = 0; i < funcs.length; i++) {
|
|
functions[funcs[i]] = data;
|
|
}
|
|
};
|
|
|
|
// Add all of the functions in duplicatedFunctions to the functions map
|
|
for (var i = 0; i < duplicatedFunctions.length; i++) {
|
|
addFuncsWithData(duplicatedFunctions[i].funcs, duplicatedFunctions[i].data);
|
|
}
|
|
|
|
// Set default values of functions
|
|
for (var f in functions) {
|
|
if (functions.hasOwnProperty(f)) {
|
|
var func = functions[f];
|
|
|
|
functions[f] = {
|
|
numArgs: func.numArgs,
|
|
argTypes: func.argTypes,
|
|
greediness: (func.greediness === undefined) ? 1 : func.greediness,
|
|
allowedInText: func.allowedInText ? func.allowedInText : false,
|
|
numOptionalArgs: (func.numOptionalArgs === undefined) ? 0 :
|
|
func.numOptionalArgs,
|
|
handler: func.handler
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
funcs: functions
|
|
};
|