feat(function): add allowedInArgument instead of greediness property (#2134)

* Remove `greediness` property

Use boolean `grouped` property instead, which is more consistent with 
LaTeX.

BREAKING CHANGE: \cfrac, \color, \textcolor, \colorbox, \fcolorbox are 
no longer grouped.

* Rename grouped to allowedInArgument

* Reenable tests

* Update documentation

* Fix typo

Co-authored-by: Kevin Barabash <kevinb@khanacademy.org>
This commit is contained in:
ylemkimon
2020-09-07 13:38:17 +09:00
committed by GitHub
parent 37990cc5b3
commit 5a90558116
10 changed files with 53 additions and 61 deletions

View File

@@ -9,8 +9,8 @@ some gaps between KaTeX and LaTeX and therefore there may be breaking changes.
## Macro arguments
Tokens will not be expanded while parsing a macro argument. For example, `\frac\foo\foo`,
where the `\foo` is defined as `12`, will be parsed as `\frac{12}{12}`, not
`\frac{1}{2}12`. <!--To expand the argument before parsing, `\expandafter` can
be used` like `\expandafter\frac\foo\foo`.-->
`\frac{1}{2}12`. To expand the argument before parsing, `\expandafter` can
be used like `\expandafter\frac\foo\foo`.
## `\def`
`\def` no longer accepts a control sequence enclosed in braces. For example,
@@ -22,3 +22,9 @@ It also no longer accepts replacement text not enclosed in braces. For example,
## `\newline` and `\cr`
`\newline` and `\cr` no longer takes an optional size argument. To specify vertical
spacing, `\\` should be used.
## `\cfrac`, `\color`, `\textcolor`, `\colorbox`, `\fcolorbox`
They are no longer allowed as an argument to primitive commands, such as `\sqrt`
(without the optional argument) and super/subscript. For example,
`\sqrt\textcolor{red}{x}` no longer works and should be changed to
`\sqrt{\textcolor{red}{x}}`.

View File

@@ -247,9 +247,6 @@ export default class Parser {
}
}
// The greediness of a superscript or subscript
static SUPSUB_GREEDINESS = 1;
/**
* Handle a subscript or superscript with nice errors.
*/
@@ -260,7 +257,7 @@ export default class Parser {
const symbol = symbolToken.text;
this.consume();
this.consumeSpaces(); // ignore spaces before sup/subscript argument
const group = this.parseGroup(name, Parser.SUPSUB_GREEDINESS);
const group = this.parseGroup(name);
if (!group) {
throw new ParseError(
@@ -305,7 +302,7 @@ export default class Parser {
parseAtom(breakOnTokenText?: BreakToken): ?AnyParseNode {
// The body of an atom is an implicit group, so that things like
// \left(x\right)^2 work correctly.
const base = this.parseGroup("atom", null, breakOnTokenText);
const base = this.parseGroup("atom", breakOnTokenText);
// In text mode, we don't have superscripts or subscripts
if (this.mode === "text") {
@@ -402,8 +399,7 @@ export default class Parser {
*/
parseFunction(
breakOnTokenText?: BreakToken,
name?: string, // For error reporting.
greediness?: ?number,
name?: string, // For determining its context
): ?AnyParseNode {
const token = this.fetch();
const func = token.text;
@@ -413,7 +409,7 @@ export default class Parser {
}
this.consume(); // consume command token
if (greediness != null && funcData.greediness <= greediness) {
if (name && name !== "atom" && !funcData.allowedInArgument) {
throw new ParseError(
"Got function '" + func + "' with no arguments" +
(name ? " as " + name : ""), token);
@@ -468,7 +464,6 @@ export default class Parser {
return {args: [], optArgs: []};
}
const baseGreediness = funcData.greediness;
const args = [];
const optArgs = [];
@@ -483,7 +478,7 @@ export default class Parser {
}
const arg = this.parseGroupOfType(`argument to '${func}'`,
argType, isOptional, baseGreediness);
argType, isOptional);
if (isOptional) {
optArgs.push(arg);
} else if (arg != null) {
@@ -503,7 +498,6 @@ export default class Parser {
name: string,
type: ?ArgType,
optional: boolean,
greediness: ?number,
): ?AnyParseNode {
switch (type) {
case "color":
@@ -538,7 +532,7 @@ export default class Parser {
if (optional) {
throw new ParseError("A primitive argument cannot be optional");
}
const group = this.parseGroup(name, greediness);
const group = this.parseGroup(name);
if (group == null) {
throw new ParseError("Expected group as " + name, this.fetch());
}
@@ -744,7 +738,6 @@ export default class Parser {
*/
parseGroup(
name: string, // For error reporting.
greediness?: ?number,
breakOnTokenText?: BreakToken,
): ?AnyParseNode {
const firstToken = this.fetch();
@@ -776,7 +769,7 @@ export default class Parser {
} else {
// If there exists a function with this name, parse the function.
// Otherwise, just return a nucleus
result = this.parseFunction(breakOnTokenText, name, greediness) ||
result = this.parseFunction(breakOnTokenText, name) ||
this.parseSymbol();
if (result == null && text[0] === "\\" &&
!implicitCommands.hasOwnProperty(text)) {

View File

@@ -52,7 +52,6 @@ export type EnvSpec<NODETYPE: NodeType> = {|
type: NODETYPE, // Need to use the type to avoid error. See NOTES below.
numArgs: number,
argTypes?: ArgType[],
greediness: number,
allowedInText: boolean,
numOptionalArgs: number,
handler: EnvHandler,
@@ -99,7 +98,6 @@ export default function defineEnvironment<NODETYPE: NodeType>({
const data = {
type,
numArgs: props.numArgs || 0,
greediness: 1,
allowedInText: false,
numOptionalArgs: 0,
handler,

View File

@@ -46,26 +46,10 @@ export type FunctionPropSpec = {
// should appear before types for mandatory arguments.
argTypes?: ArgType[],
// 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`
greediness?: number,
// Whether it expands to a single token or a braced group of tokens.
// If it's grouped, it can be used as an argument to primitive commands,
// such as \sqrt (without the optional argument) and super/subscript.
allowedInArgument?: boolean,
// Whether or not the function is allowed inside text mode
// (default false)
@@ -126,7 +110,7 @@ export type FunctionSpec<NODETYPE: NodeType> = {|
type: NODETYPE, // Need to use the type to avoid error. See NOTES below.
numArgs: number,
argTypes?: ArgType[],
greediness: number,
allowedInArgument: boolean,
allowedInText: boolean,
allowedInMath: boolean,
numOptionalArgs: number,
@@ -183,7 +167,7 @@ export default function defineFunction<NODETYPE: NodeType>({
type,
numArgs: props.numArgs,
argTypes: props.argTypes,
greediness: (props.greediness === undefined) ? 1 : props.greediness,
allowedInArgument: !!props.allowedInArgument,
allowedInText: !!props.allowedInText,
allowedInMath: (props.allowedInMath === undefined)
? true

View File

@@ -38,7 +38,6 @@ defineFunction({
props: {
numArgs: 2,
allowedInText: true,
greediness: 3,
argTypes: ["color", "original"],
},
handler({parser}, args) {
@@ -61,7 +60,6 @@ defineFunction({
props: {
numArgs: 1,
allowedInText: true,
greediness: 3,
argTypes: ["color"],
},
handler({parser, breakOnTokenText}, args) {

View File

@@ -225,7 +225,6 @@ defineFunction({
props: {
numArgs: 2,
allowedInText: true,
greediness: 3,
argTypes: ["color", "text"],
},
handler({parser, funcName}, args, optArgs) {
@@ -249,7 +248,6 @@ defineFunction({
props: {
numArgs: 3,
allowedInText: true,
greediness: 3,
argTypes: ["color", "color", "text"],
},
handler({parser, funcName}, args, optArgs) {

View File

@@ -44,7 +44,7 @@ defineFunction({
],
props: {
numArgs: 1,
greediness: 2,
allowedInArgument: true,
},
handler: ({parser, funcName}, args) => {
const body = normalizeArgument(args[0]);
@@ -68,7 +68,6 @@ defineFunction({
names: ["\\boldsymbol", "\\bm"],
props: {
numArgs: 1,
greediness: 2,
},
handler: ({parser}, args) => {
const body = args[0];

View File

@@ -241,14 +241,14 @@ const mathmlBuilder = (group, options) => {
defineFunction({
type: "genfrac",
names: [
"\\cfrac", "\\dfrac", "\\frac", "\\tfrac",
"\\dfrac", "\\frac", "\\tfrac",
"\\dbinom", "\\binom", "\\tbinom",
"\\\\atopfrac", // cant be entered directly
"\\\\bracefrac", "\\\\brackfrac", // ditto
],
props: {
numArgs: 2,
greediness: 2,
allowedInArgument: true,
},
handler: ({parser, funcName}, args) => {
const numer = args[0];
@@ -259,7 +259,6 @@ defineFunction({
let size = "auto";
switch (funcName) {
case "\\cfrac":
case "\\dfrac":
case "\\frac":
case "\\tfrac":
@@ -290,7 +289,6 @@ defineFunction({
}
switch (funcName) {
case "\\cfrac":
case "\\dfrac":
case "\\dbinom":
size = "display";
@@ -304,7 +302,7 @@ defineFunction({
return {
type: "genfrac",
mode: parser.mode,
continued: funcName === "\\cfrac",
continued: false,
numer,
denom,
hasBarLine,
@@ -319,6 +317,31 @@ defineFunction({
mathmlBuilder,
});
defineFunction({
type: "genfrac",
names: ["\\cfrac"],
props: {
numArgs: 2,
},
handler: ({parser, funcName}, args) => {
const numer = args[0];
const denom = args[1];
return {
type: "genfrac",
mode: parser.mode,
continued: true,
numer,
denom,
hasBarLine: true,
leftDelim: null,
rightDelim: null,
size: "display",
barSize: null,
};
},
});
// Infix generalized fractions -- these are not rendered directly, but replaced
// immediately by one of the variants above.
defineFunction({
@@ -374,7 +397,7 @@ defineFunction({
names: ["\\genfrac"],
props: {
numArgs: 6,
greediness: 6,
allowedInArgument: true,
argTypes: ["math", "math", "size", "text", "math", "math"],
},
handler({parser}, args) {

View File

@@ -48,7 +48,7 @@ defineFunction({
props: {
numArgs: 1,
argTypes: ["text"],
greediness: 2,
allowedInArgument: true,
allowedInText: true,
},
handler({parser, funcName}, args) {

View File

@@ -849,13 +849,6 @@ describe("A color parser", function() {
expect(newColorExpression).toParse();
});
it("should have correct greediness", function() {
expect`\textcolor{red}a`.toParse();
expect`\textcolor{red}{\text{a}}`.toParse();
expect`\textcolor{red}\text{a}`.not.toParse();
expect`\textcolor{red}\frac12`.not.toParse();
});
it("should use one-argument \\color by default", function() {
expect(oldColorExpression).toParseLike`\textcolor{#fA6}{xy}`;
});
@@ -1619,7 +1612,7 @@ describe("A font parser", function() {
expect(bf.body.body[2].text).toEqual("c");
});
it("should have the correct greediness", function() {
it("should be allowed in the argument", function() {
expect`e^\mathbf{x}`.toParse();
});