Files
KaTeX/src/functions.js
Erik Demaine 4f29c5a942 Support \( and fix instant mode switching for $, \(, \text (#1213)
* Fix #1212 by supporting `\(...\)` in addition to `$...$` inline math nested inside `\text`. This turned out to be harder than one would think because of the way `$` was being handled specially, while `\(` needed to be handled as a function, which immediately consumed the `\(` token, which meant that the first following token got consumed in text mode instead of math mode (unlike `$` where we could switch the mode before consumption).
* Added new `consumeMode` setting for functions which tells the parser to switch modes just for the consumption of the command token.
* Now that we're working with functions, move all the `$` handling code into `functions/math.js`.  Somewhat bizarrely, we can define `$` as a function (nearly identical to `\(`) and it all works (just like we can have single-character macros).  This move removed a lot of messy stuff from `Parser.js`: `ParsedDollar`, `ParsedFuncOrArgOrDollar`, `newDollar`, `assertFuncOrArg`, and at least three explicit checks for `if (... === "$")`.
* Moved the `consume()` for the command token from `parseSymbol` to `parseGivenFunction`.  This ended up not being strictly necessary, but seems conceptually cleaner.
* Partially address #1027 by setting `consumeMode: "text"` for `\text` commands.  As a result, `\text` now instantly switches the mode to `"text"`, so even an unbraced macro argument will properly detect text mode.  I added a test for this, which is a slightly weaker form of #1027.
2018-03-14 10:54:36 -04:00

303 lines
7.2 KiB
JavaScript

// @flow
/** Include this to ensure that all functions are defined. */
import ParseError from "./ParseError";
import ParseNode from "./ParseNode";
import {
default as _defineFunction,
ordargument,
_functions,
} from "./defineFunction";
import type {FunctionPropSpec, FunctionHandler} from "./defineFunction";
// WARNING: New functions should be added to src/functions and imported here.
const functions = _functions;
export default functions;
// Define a convenience function that mimcs the old semantics of defineFunction
// to support existing code so that we can migrate it a little bit at a time.
const defineFunction = function(
names: string[],
props: FunctionPropSpec,
handler: ?FunctionHandler, // null only if handled in parser
) {
_defineFunction({names, props, handler});
};
// TODO(kevinb): have functions return an object and call defineFunction with
// that object in this file instead of relying on side-effects.
import "./functions/sqrt";
import "./functions/color";
import "./functions/text";
import "./functions/math";
import "./functions/enclose";
import "./functions/overline";
import "./functions/underline";
import "./functions/rule";
import "./functions/kern";
import "./functions/phantom";
// Math class commands except \mathop
defineFunction([
"\\mathord", "\\mathbin", "\\mathrel", "\\mathopen",
"\\mathclose", "\\mathpunct", "\\mathinner",
], {
numArgs: 1,
}, function(context, args) {
const body = args[0];
return {
type: "mclass",
mclass: "m" + context.funcName.substr(5),
value: ordargument(body),
};
});
// Build a relation by placing one symbol on top of another
defineFunction(["\\stackrel"], {
numArgs: 2,
}, function(context, args) {
const top = args[0];
const bottom = args[1];
const bottomop = new ParseNode("op", {
type: "op",
limits: true,
alwaysHandleSupSub: true,
symbol: false,
value: ordargument(bottom),
}, bottom.mode);
const supsub = new ParseNode("supsub", {
base: bottomop,
sup: top,
sub: null,
}, top.mode);
return {
type: "mclass",
mclass: "mrel",
value: [supsub],
};
});
import "./functions/mod";
const singleCharIntegrals: {[string]: string} = {
"\u222b": "\\int",
"\u222c": "\\iint",
"\u222d": "\\iiint",
"\u222e": "\\oint",
};
// 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", "\\arctg", "\\arcctg",
"\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg",
"\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp",
"\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin",
"\\sinh", "\\sh", "\\tan", "\\tanh", "\\tg", "\\th",
], {
numArgs: 0,
}, function(context) {
return {
type: "op",
limits: false,
symbol: false,
body: context.funcName,
};
});
// Limits, not symbols
defineFunction([
"\\det", "\\gcd", "\\inf", "\\lim", "\\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", "\u222b", "\u222c",
"\u222d", "\u222e",
], {
numArgs: 0,
}, function(context) {
let fName = context.funcName;
if (fName.length === 1) {
fName = singleCharIntegrals[fName];
}
return {
type: "op",
limits: false,
symbol: true,
body: fName,
};
});
import "./functions/op";
import "./functions/operatorname";
import "./functions/genfrac";
import "./functions/lap";
import "./functions/smash";
import "./functions/delimsizing";
import "./functions/sizing";
import "./functions/styling";
import "./functions/font";
import "./functions/accent";
// Horizontal stretchy braces
defineFunction([
"\\overbrace", "\\underbrace",
], {
numArgs: 1,
}, function(context, args) {
const base = args[0];
return {
type: "horizBrace",
label: context.funcName,
isOver: /^\\over/.test(context.funcName),
base: base,
};
});
// Stretchy accents under the body
import "./functions/accentunder";
// Stretchy arrows with an optional argument
defineFunction([
"\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow",
"\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow",
"\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown",
"\\xrightharpoonup", "\\xleftharpoondown", "\\xleftharpoonup",
"\\xrightleftharpoons", "\\xleftrightharpoons", "\\xlongequal",
"\\xtwoheadrightarrow", "\\xtwoheadleftarrow", "\\xtofrom",
// The next 3 functions are here to support the mhchem extension.
// Direct use of these functions is discouraged and may break someday.
"\\xrightleftarrows", "\\xrightequilibrium",
"\\xleftequilibrium",
], {
numArgs: 1,
numOptionalArgs: 1,
}, function(context, args, optArgs) {
const below = optArgs[0];
const body = args[0];
return {
type: "xArrow", // x for extensible
label: context.funcName,
body: body,
below: below,
};
});
// Infix generalized fractions
defineFunction(["\\over", "\\choose", "\\atop"], {
numArgs: 0,
infix: true,
}, function(context) {
let replaceWith;
switch (context.funcName) {
case "\\over":
replaceWith = "\\frac";
break;
case "\\choose":
replaceWith = "\\binom";
break;
case "\\atop":
replaceWith = "\\\\atopfrac";
break;
default:
throw new Error("Unrecognized infix genfrac command");
}
return {
type: "infix",
replaceWith: replaceWith,
token: context.token,
};
});
// Row breaks for aligned data
defineFunction(["\\\\", "\\cr"], {
numArgs: 0,
numOptionalArgs: 1,
argTypes: ["size"],
}, function(context, args, optArgs) {
const size = optArgs[0];
return {
type: "cr",
size: size,
};
});
// Environment delimiters
defineFunction(["\\begin", "\\end"], {
numArgs: 1,
argTypes: ["text"],
}, function(context, args) {
const nameGroup = args[0];
if (nameGroup.type !== "ordgroup") {
throw new ParseError("Invalid environment name", nameGroup);
}
let name = "";
for (let i = 0; i < nameGroup.value.length; ++i) {
name += nameGroup.value[i].value;
}
return {
type: "environment",
name: name,
nameGroup: nameGroup,
};
});
// Box manipulation
defineFunction(["\\raisebox"], {
numArgs: 2,
argTypes: ["size", "text"],
allowedInText: true,
}, function(context, args) {
const amount = args[0];
const body = args[1];
return {
type: "raisebox",
dy: amount,
body: body,
value: ordargument(body),
};
});
import "./functions/verb";
// Hyperlinks
import "./functions/href";
// MathChoice
import "./functions/mathchoice";