1 | // @flow
|
2 | /**
|
3 | * This file contains the “gullet” where macros are expanded
|
4 | * until only non-macro tokens remain.
|
5 | */
|
6 |
|
7 | import functions from "./functions";
|
8 | import symbols from "./symbols";
|
9 | import Lexer from "./Lexer";
|
10 | import {Token} from "./Token";
|
11 | import type {Mode} from "./types";
|
12 | import ParseError from "./ParseError";
|
13 | import Namespace from "./Namespace";
|
14 | import builtinMacros from "./macros";
|
15 |
|
16 | import type {MacroContextInterface, MacroDefinition, MacroExpansion}
|
17 | from "./macros";
|
18 | import type Settings from "./Settings";
|
19 |
|
20 | // List of commands that act like macros but aren't defined as a macro,
|
21 | // function, or symbol. Used in `isDefined`.
|
22 | export const implicitCommands = {
|
23 | "\\relax": true, // MacroExpander.js
|
24 | "^": true, // Parser.js
|
25 | "_": true, // Parser.js
|
26 | "\\limits": true, // Parser.js
|
27 | "\\nolimits": true, // Parser.js
|
28 | };
|
29 |
|
30 | export default class MacroExpander implements MacroContextInterface {
|
31 | settings: Settings;
|
32 | expansionCount: number;
|
33 | lexer: Lexer;
|
34 | macros: Namespace<MacroDefinition>;
|
35 | stack: Token[];
|
36 | mode: Mode;
|
37 |
|
38 | constructor(input: string, settings: Settings, mode: Mode) {
|
39 | this.settings = settings;
|
40 | this.expansionCount = 0;
|
41 | this.feed(input);
|
42 | // Make new global namespace
|
43 | this.macros = new Namespace(builtinMacros, settings.macros);
|
44 | this.mode = mode;
|
45 | this.stack = []; // contains tokens in REVERSE order
|
46 | }
|
47 |
|
48 | /**
|
49 | * Feed a new input string to the same MacroExpander
|
50 | * (with existing macros etc.).
|
51 | */
|
52 | feed(input: string) {
|
53 | this.lexer = new Lexer(input, this.settings);
|
54 | }
|
55 |
|
56 | /**
|
57 | * Switches between "text" and "math" modes.
|
58 | */
|
59 | switchMode(newMode: Mode) {
|
60 | this.mode = newMode;
|
61 | }
|
62 |
|
63 | /**
|
64 | * Start a new group nesting within all namespaces.
|
65 | */
|
66 | beginGroup() {
|
67 | this.macros.beginGroup();
|
68 | }
|
69 |
|
70 | /**
|
71 | * End current group nesting within all namespaces.
|
72 | */
|
73 | endGroup() {
|
74 | this.macros.endGroup();
|
75 | }
|
76 |
|
77 | /**
|
78 | * Returns the topmost token on the stack, without expanding it.
|
79 | * Similar in behavior to TeX's `\futurelet`.
|
80 | */
|
81 | future(): Token {
|
82 | if (this.stack.length === 0) {
|
83 | this.pushToken(this.lexer.lex());
|
84 | }
|
85 | return this.stack[this.stack.length - 1];
|
86 | }
|
87 |
|
88 | /**
|
89 | * Remove and return the next unexpanded token.
|
90 | */
|
91 | popToken(): Token {
|
92 | this.future(); // ensure non-empty stack
|
93 | return this.stack.pop();
|
94 | }
|
95 |
|
96 | /**
|
97 | * Add a given token to the token stack. In particular, this get be used
|
98 | * to put back a token returned from one of the other methods.
|
99 | */
|
100 | pushToken(token: Token) {
|
101 | this.stack.push(token);
|
102 | }
|
103 |
|
104 | /**
|
105 | * Append an array of tokens to the token stack.
|
106 | */
|
107 | pushTokens(tokens: Token[]) {
|
108 | this.stack.push(...tokens);
|
109 | }
|
110 |
|
111 | /**
|
112 | * Consume all following space tokens, without expansion.
|
113 | */
|
114 | consumeSpaces() {
|
115 | for (;;) {
|
116 | const token = this.future();
|
117 | if (token.text === " ") {
|
118 | this.stack.pop();
|
119 | } else {
|
120 | break;
|
121 | }
|
122 | }
|
123 | }
|
124 |
|
125 | /**
|
126 | * Consume the specified number of arguments from the token stream,
|
127 | * and return the resulting array of arguments.
|
128 | */
|
129 | consumeArgs(numArgs: number): Token[][] {
|
130 | const args: Token[][] = [];
|
131 | // obtain arguments, either single token or balanced {…} group
|
132 | for (let i = 0; i < numArgs; ++i) {
|
133 | this.consumeSpaces(); // ignore spaces before each argument
|
134 | const startOfArg = this.popToken();
|
135 | if (startOfArg.text === "{") {
|
136 | const arg: Token[] = [];
|
137 | let depth = 1;
|
138 | while (depth !== 0) {
|
139 | const tok = this.popToken();
|
140 | arg.push(tok);
|
141 | if (tok.text === "{") {
|
142 | ++depth;
|
143 | } else if (tok.text === "}") {
|
144 | --depth;
|
145 | } else if (tok.text === "EOF") {
|
146 | throw new ParseError(
|
147 | "End of input in macro argument",
|
148 | startOfArg);
|
149 | }
|
150 | }
|
151 | arg.pop(); // remove last }
|
152 | arg.reverse(); // like above, to fit in with stack order
|
153 | args[i] = arg;
|
154 | } else if (startOfArg.text === "EOF") {
|
155 | throw new ParseError(
|
156 | "End of input expecting macro argument");
|
157 | } else {
|
158 | args[i] = [startOfArg];
|
159 | }
|
160 | }
|
161 | return args;
|
162 | }
|
163 |
|
164 | /**
|
165 | * Expand the next token only once if possible.
|
166 | *
|
167 | * If the token is expanded, the resulting tokens will be pushed onto
|
168 | * the stack in reverse order and will be returned as an array,
|
169 | * also in reverse order.
|
170 | *
|
171 | * If not, the next token will be returned without removing it
|
172 | * from the stack. This case can be detected by a `Token` return value
|
173 | * instead of an `Array` return value.
|
174 | *
|
175 | * In either case, the next token will be on the top of the stack,
|
176 | * or the stack will be empty.
|
177 | *
|
178 | * Used to implement `expandAfterFuture` and `expandNextToken`.
|
179 | *
|
180 | * At the moment, macro expansion doesn't handle delimited macros,
|
181 | * i.e. things like those defined by \def\foo#1\end{…}.
|
182 | * See the TeX book page 202ff. for details on how those should behave.
|
183 | */
|
184 | expandOnce(): Token | Token[] {
|
185 | const topToken = this.popToken();
|
186 | const name = topToken.text;
|
187 | const expansion = this._getExpansion(name);
|
188 | if (expansion == null) { // mainly checking for undefined here
|
189 | // Fully expanded
|
190 | this.pushToken(topToken);
|
191 | return topToken;
|
192 | }
|
193 | this.expansionCount++;
|
194 | if (this.expansionCount > this.settings.maxExpand) {
|
195 | throw new ParseError("Too many expansions: infinite loop or " +
|
196 | "need to increase maxExpand setting");
|
197 | }
|
198 | let tokens = expansion.tokens;
|
199 | if (expansion.numArgs) {
|
200 | const args = this.consumeArgs(expansion.numArgs);
|
201 | // paste arguments in place of the placeholders
|
202 | tokens = tokens.slice(); // make a shallow copy
|
203 | for (let i = tokens.length - 1; i >= 0; --i) {
|
204 | let tok = tokens[i];
|
205 | if (tok.text === "#") {
|
206 | if (i === 0) {
|
207 | throw new ParseError(
|
208 | "Incomplete placeholder at end of macro body",
|
209 | tok);
|
210 | }
|
211 | tok = tokens[--i]; // next token on stack
|
212 | if (tok.text === "#") { // ## → #
|
213 | tokens.splice(i + 1, 1); // drop first #
|
214 | } else if (/^[1-9]$/.test(tok.text)) {
|
215 | // replace the placeholder with the indicated argument
|
216 | tokens.splice(i, 2, ...args[+tok.text - 1]);
|
217 | } else {
|
218 | throw new ParseError(
|
219 | "Not a valid argument number",
|
220 | tok);
|
221 | }
|
222 | }
|
223 | }
|
224 | }
|
225 | // Concatenate expansion onto top of stack.
|
226 | this.pushTokens(tokens);
|
227 | return tokens;
|
228 | }
|
229 |
|
230 | /**
|
231 | * Expand the next token only once (if possible), and return the resulting
|
232 | * top token on the stack (without removing anything from the stack).
|
233 | * Similar in behavior to TeX's `\expandafter\futurelet`.
|
234 | * Equivalent to expandOnce() followed by future().
|
235 | */
|
236 | expandAfterFuture(): Token {
|
237 | this.expandOnce();
|
238 | return this.future();
|
239 | }
|
240 |
|
241 | /**
|
242 | * Recursively expand first token, then return first non-expandable token.
|
243 | */
|
244 | expandNextToken(): Token {
|
245 | for (;;) {
|
246 | const expanded = this.expandOnce();
|
247 | // expandOnce returns Token if and only if it's fully expanded.
|
248 | if (expanded instanceof Token) {
|
249 | // \relax stops the expansion, but shouldn't get returned (a
|
250 | // null return value couldn't get implemented as a function).
|
251 | if (expanded.text === "\\relax") {
|
252 | this.stack.pop();
|
253 | } else {
|
254 | return this.stack.pop(); // === expanded
|
255 | }
|
256 | }
|
257 | }
|
258 |
|
259 | // Flow unable to figure out that this pathway is impossible.
|
260 | // https://github.com/facebook/flow/issues/4808
|
261 | throw new Error(); // eslint-disable-line no-unreachable
|
262 | }
|
263 |
|
264 | /**
|
265 | * Fully expand the given macro name and return the resulting list of
|
266 | * tokens, or return `undefined` if no such macro is defined.
|
267 | */
|
268 | expandMacro(name: string): Token[] | void {
|
269 | if (!this.macros.get(name)) {
|
270 | return undefined;
|
271 | }
|
272 | const output = [];
|
273 | const oldStackLength = this.stack.length;
|
274 | this.pushToken(new Token(name));
|
275 | while (this.stack.length > oldStackLength) {
|
276 | const expanded = this.expandOnce();
|
277 | // expandOnce returns Token if and only if it's fully expanded.
|
278 | if (expanded instanceof Token) {
|
279 | output.push(this.stack.pop());
|
280 | }
|
281 | }
|
282 | return output;
|
283 | }
|
284 |
|
285 | /**
|
286 | * Fully expand the given macro name and return the result as a string,
|
287 | * or return `undefined` if no such macro is defined.
|
288 | */
|
289 | expandMacroAsText(name: string): string | void {
|
290 | const tokens = this.expandMacro(name);
|
291 | if (tokens) {
|
292 | return tokens.map((token) => token.text).join("");
|
293 | } else {
|
294 | return tokens;
|
295 | }
|
296 | }
|
297 |
|
298 | /**
|
299 | * Returns the expanded macro as a reversed array of tokens and a macro
|
300 | * argument count. Or returns `null` if no such macro.
|
301 | */
|
302 | _getExpansion(name: string): ?MacroExpansion {
|
303 | const definition = this.macros.get(name);
|
304 | if (definition == null) { // mainly checking for undefined here
|
305 | return definition;
|
306 | }
|
307 | const expansion =
|
308 | typeof definition === "function" ? definition(this) : definition;
|
309 | if (typeof expansion === "string") {
|
310 | let numArgs = 0;
|
311 | if (expansion.indexOf("#") !== -1) {
|
312 | const stripped = expansion.replace(/##/g, "");
|
313 | while (stripped.indexOf("#" + (numArgs + 1)) !== -1) {
|
314 | ++numArgs;
|
315 | }
|
316 | }
|
317 | const bodyLexer = new Lexer(expansion, this.settings);
|
318 | const tokens = [];
|
319 | let tok = bodyLexer.lex();
|
320 | while (tok.text !== "EOF") {
|
321 | tokens.push(tok);
|
322 | tok = bodyLexer.lex();
|
323 | }
|
324 | tokens.reverse(); // to fit in with stack using push and pop
|
325 | const expanded = {tokens, numArgs};
|
326 | return expanded;
|
327 | }
|
328 |
|
329 | return expansion;
|
330 | }
|
331 |
|
332 | /**
|
333 | * Determine whether a command is currently "defined" (has some
|
334 | * functionality), meaning that it's a macro (in the current group),
|
335 | * a function, a symbol, or one of the special commands listed in
|
336 | * `implicitCommands`.
|
337 | */
|
338 | isDefined(name: string): boolean {
|
339 | return this.macros.has(name) ||
|
340 | functions.hasOwnProperty(name) ||
|
341 | symbols.math.hasOwnProperty(name) ||
|
342 | symbols.text.hasOwnProperty(name) ||
|
343 | implicitCommands.hasOwnProperty(name);
|
344 | }
|
345 | }
|