UNPKG

11.7 kBJavaScriptView Raw
1// @flow
2/**
3 * This file contains the “gullet” where macros are expanded
4 * until only non-macro tokens remain.
5 */
6
7import functions from "./functions";
8import symbols from "./symbols";
9import Lexer from "./Lexer";
10import {Token} from "./Token";
11import type {Mode} from "./types";
12import ParseError from "./ParseError";
13import Namespace from "./Namespace";
14import builtinMacros from "./macros";
15
16import type {MacroContextInterface, MacroDefinition, MacroExpansion}
17 from "./macros";
18import 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`.
22export 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
30export 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}