mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-07 04:08:43 +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();
|
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.
|
* 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.
|
* Expand the next token only once if possible.
|
||||||
*
|
*
|
||||||
@@ -92,37 +146,7 @@ export default class MacroExpander implements MacroContextInterface {
|
|||||||
const {tokens, numArgs} = this._getExpansion(name);
|
const {tokens, numArgs} = this._getExpansion(name);
|
||||||
let expansion = tokens;
|
let expansion = tokens;
|
||||||
if (numArgs) {
|
if (numArgs) {
|
||||||
const args: Token[][] = [];
|
const args = this.consumeArgs(numArgs);
|
||||||
// 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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// paste arguments in place of the placeholders
|
// paste arguments in place of the placeholders
|
||||||
expansion = expansion.slice(); // make a shallow copy
|
expansion = expansion.slice(); // make a shallow copy
|
||||||
for (let i = expansion.length - 1; i >= 0; --i) {
|
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.
|
// Concatenate expansion onto top of stack.
|
||||||
this.stack.push(...expansion);
|
this.pushTokens(expansion);
|
||||||
return expansion;
|
return expansion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,13 +246,5 @@ export default class MacroExpander implements MacroContextInterface {
|
|||||||
|
|
||||||
return expansion;
|
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`.
|
* Similar in behavior to TeX's `\expandafter\futurelet`.
|
||||||
*/
|
*/
|
||||||
expandAfterFuture(): Token;
|
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). */
|
/** Macro tokens (in reverse order). */
|
||||||
export type MacroExpansion = {tokens: Token[], numArgs: number};
|
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};
|
export type MacroMap = {[string]: MacroDefinition};
|
||||||
|
|
||||||
const builtinMacros: MacroMap = {};
|
const builtinMacros: MacroMap = {};
|
||||||
export default builtinMacros;
|
export default builtinMacros;
|
||||||
|
|
||||||
// This function might one day accept an additional argument and do more things.
|
// 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;
|
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
|
// basics
|
||||||
defineMacro("\\bgroup", "{");
|
defineMacro("\\bgroup", "{");
|
||||||
@@ -244,8 +272,8 @@ defineMacro("\\thickspace", "\\;"); // \let\thickspace\;
|
|||||||
|
|
||||||
// \DeclareRobustCommand\hspace{\@ifstar\@hspacer\@hspace}
|
// \DeclareRobustCommand\hspace{\@ifstar\@hspacer\@hspace}
|
||||||
// \def\@hspace#1{\hskip #1\relax}
|
// \def\@hspace#1{\hskip #1\relax}
|
||||||
// KaTeX doesn't do line breaks, so \hspace is the same as \kern
|
// KaTeX doesn't do line breaks, so \hspace and \hspace* are the same as \kern
|
||||||
defineMacro("\\hspace", "\\kern{#1}");
|
defineMacro("\\hspace", "\\@ifstar\\kern\\kern");
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// mathtools.sty
|
// 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() {
|
describe("A parser taking String objects", function() {
|
||||||
|
Reference in New Issue
Block a user