mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-05 19:28:39 +00:00
\hspace*, \@ifstar, \@ifnextchar, \@firstoftwo (#975)
* \hspace*, \@ifstar, \@ifnextchar, \@firstoftwo * Support both \hspace and \hspace* (still aliasing to \kern) using new \@ifstar * \@ifstar is a macro using new \@ifnextchar and \@firstoftwo (same definition as LaTeX) * \@firstoftwo and \@ifnextchar use available MacroParser features to act as they do in LaTeX. * Also new method pushTokens (which I almost used but didn't end up), and moved pushToken next to it and popToken. * Fix flow errors; generalize MacroDefinition * Add tests for macros
This commit is contained in:
committed by
Kevin Barabash
parent
c8249c389f
commit
e93668c666
@@ -42,6 +42,21 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
return this.stack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a given token to the token stack. In particular, this get be used
|
||||
* to put back a token returned from one of the other methods.
|
||||
*/
|
||||
pushToken(token: Token) {
|
||||
this.stack.push(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append an array of tokens to the token stack.
|
||||
*/
|
||||
pushTokens(tokens: Token[]) {
|
||||
this.stack.push(...tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume all following space tokens, without expansion.
|
||||
*/
|
||||
@@ -56,6 +71,45 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the specified number of arguments from the token stream,
|
||||
* and return the resulting array of arguments.
|
||||
*/
|
||||
consumeArgs(numArgs: number): Token[][] {
|
||||
const args: Token[][] = [];
|
||||
// obtain arguments, either single token or balanced {…} group
|
||||
for (let i = 0; i < numArgs; ++i) {
|
||||
this.consumeSpaces(); // ignore spaces before each argument
|
||||
const startOfArg = this.popToken();
|
||||
if (startOfArg.text === "{") {
|
||||
const arg: Token[] = [];
|
||||
let depth = 1;
|
||||
while (depth !== 0) {
|
||||
const tok = this.popToken();
|
||||
arg.push(tok);
|
||||
if (tok.text === "{") {
|
||||
++depth;
|
||||
} else if (tok.text === "}") {
|
||||
--depth;
|
||||
} else if (tok.text === "EOF") {
|
||||
throw new ParseError(
|
||||
"End of input in macro argument",
|
||||
startOfArg);
|
||||
}
|
||||
}
|
||||
arg.pop(); // remove last }
|
||||
arg.reverse(); // like above, to fit in with stack order
|
||||
args[i] = arg;
|
||||
} else if (startOfArg.text === "EOF") {
|
||||
throw new ParseError(
|
||||
"End of input expecting macro argument");
|
||||
} else {
|
||||
args[i] = [startOfArg];
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the next token only once if possible.
|
||||
*
|
||||
@@ -92,37 +146,7 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
const {tokens, numArgs} = this._getExpansion(name);
|
||||
let expansion = tokens;
|
||||
if (numArgs) {
|
||||
const args: Token[][] = [];
|
||||
// obtain arguments, either single token or balanced {…} group
|
||||
for (let i = 0; i < numArgs; ++i) {
|
||||
this.consumeSpaces(); // ignore spaces before each argument
|
||||
const startOfArg = this.popToken();
|
||||
if (startOfArg.text === "{") {
|
||||
const arg: Token[] = [];
|
||||
let depth = 1;
|
||||
while (depth !== 0) {
|
||||
const tok = this.popToken();
|
||||
arg.push(tok);
|
||||
if (tok.text === "{") {
|
||||
++depth;
|
||||
} else if (tok.text === "}") {
|
||||
--depth;
|
||||
} else if (tok.text === "EOF") {
|
||||
throw new ParseError(
|
||||
"End of input in macro argument",
|
||||
startOfArg);
|
||||
}
|
||||
}
|
||||
arg.pop(); // remove last }
|
||||
arg.reverse(); // like above, to fit in with stack order
|
||||
args[i] = arg;
|
||||
} else if (startOfArg.text === "EOF") {
|
||||
throw new ParseError(
|
||||
"End of input expecting macro argument", topToken);
|
||||
} else {
|
||||
args[i] = [startOfArg];
|
||||
}
|
||||
}
|
||||
const args = this.consumeArgs(numArgs);
|
||||
// paste arguments in place of the placeholders
|
||||
expansion = expansion.slice(); // make a shallow copy
|
||||
for (let i = expansion.length - 1; i >= 0; --i) {
|
||||
@@ -148,7 +172,7 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
}
|
||||
}
|
||||
// Concatenate expansion onto top of stack.
|
||||
this.stack.push(...expansion);
|
||||
this.pushTokens(expansion);
|
||||
return expansion;
|
||||
}
|
||||
|
||||
@@ -222,13 +246,5 @@ export default class MacroExpander implements MacroContextInterface {
|
||||
|
||||
return expansion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a given token to the token stack. In particular, this get be used
|
||||
* to put back a token returned from one of the other methods.
|
||||
*/
|
||||
pushToken(token: Token) {
|
||||
this.stack.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -25,22 +25,50 @@ export interface MacroContextInterface {
|
||||
* Similar in behavior to TeX's `\expandafter\futurelet`.
|
||||
*/
|
||||
expandAfterFuture(): Token;
|
||||
|
||||
/**
|
||||
* Consume the specified number of arguments from the token stream,
|
||||
* and return the resulting array of arguments.
|
||||
*/
|
||||
consumeArgs(numArgs: number): Token[][];
|
||||
}
|
||||
|
||||
/** Macro tokens (in reverse order). */
|
||||
export type MacroExpansion = {tokens: Token[], numArgs: number};
|
||||
|
||||
type MacroDefinition = string | (MacroContextInterface => string) | MacroExpansion;
|
||||
type MacroDefinition = string | MacroExpansion |
|
||||
(MacroContextInterface => (string | MacroExpansion));
|
||||
export type MacroMap = {[string]: MacroDefinition};
|
||||
|
||||
const builtinMacros: MacroMap = {};
|
||||
export default builtinMacros;
|
||||
|
||||
// This function might one day accept an additional argument and do more things.
|
||||
function defineMacro(name: string, body: string | MacroContextInterface => string) {
|
||||
function defineMacro(name: string, body: MacroDefinition) {
|
||||
builtinMacros[name] = body;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// macro tools
|
||||
|
||||
defineMacro("\\@firstoftwo", function(context) {
|
||||
const args = context.consumeArgs(2);
|
||||
return {tokens: args[0], numArgs: 0};
|
||||
});
|
||||
|
||||
defineMacro("\\@ifnextchar", function(context) {
|
||||
const args = context.consumeArgs(3); // symbol, if, else
|
||||
const nextToken = context.future();
|
||||
if (args[0].length === 1 && args[0][0].text === nextToken.text) {
|
||||
return {tokens: args[1], numArgs: 0};
|
||||
} else {
|
||||
return {tokens: args[2], numArgs: 0};
|
||||
}
|
||||
});
|
||||
|
||||
// \def\@ifstar#1{\@ifnextchar *{\@firstoftwo{#1}}}
|
||||
defineMacro("\\@ifstar", "\\@ifnextchar *{\\@firstoftwo{#1}}");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// basics
|
||||
defineMacro("\\bgroup", "{");
|
||||
@@ -244,8 +272,8 @@ defineMacro("\\thickspace", "\\;"); // \let\thickspace\;
|
||||
|
||||
// \DeclareRobustCommand\hspace{\@ifstar\@hspacer\@hspace}
|
||||
// \def\@hspace#1{\hskip #1\relax}
|
||||
// KaTeX doesn't do line breaks, so \hspace is the same as \kern
|
||||
defineMacro("\\hspace", "\\kern{#1}");
|
||||
// KaTeX doesn't do line breaks, so \hspace and \hspace* are the same as \kern
|
||||
defineMacro("\\hspace", "\\@ifstar\\kern\\kern");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// mathtools.sty
|
||||
|
@@ -2639,6 +2639,33 @@ describe("A macro expander", function() {
|
||||
"’": "'",
|
||||
});
|
||||
});
|
||||
|
||||
it("\\@firstoftwo should consume both, and avoid errors", function() {
|
||||
expect("\\@firstoftwo{yes}{no}").toParseLike("yes");
|
||||
expect("\\@firstoftwo{yes}{1'_2^3}").toParseLike("yes");
|
||||
});
|
||||
|
||||
it("\\@ifstar should consume star but nothing else", function() {
|
||||
expect("\\@ifstar{yes}{no}*!").toParseLike("yes!");
|
||||
expect("\\@ifstar{yes}{no}?!").toParseLike("no?!");
|
||||
});
|
||||
|
||||
it("\\@ifnextchar should not consume anything", function() {
|
||||
expect("\\@ifnextchar!{yes}{no}!!").toParseLike("yes!!");
|
||||
expect("\\@ifnextchar!{yes}{no}?!").toParseLike("no?!");
|
||||
});
|
||||
|
||||
it("\\@firstoftwwo should consume star but nothing else", function() {
|
||||
expect("\\@ifstar{yes}{no}*!").toParseLike("yes!");
|
||||
expect("\\@ifstar{yes}{no}?!").toParseLike("no?!");
|
||||
});
|
||||
|
||||
// This may change in the future, if we support the extra features of
|
||||
// \hspace.
|
||||
it("should treat \\hspace, \\hspace*, \\hskip like \\kern", function() {
|
||||
expect("\\hspace{1em}").toParseLike("\\kern1em");
|
||||
expect("\\hspace*{1em}").toParseLike("\\kern1em");
|
||||
});
|
||||
});
|
||||
|
||||
describe("A parser taking String objects", function() {
|
||||
|
Reference in New Issue
Block a user