MacroExpander.js (5589B)
1 /** 2 * This file contains the “gullet” where macros are expanded 3 * until only non-macro tokens remain. 4 */ 5 6 const Lexer = require("./Lexer"); 7 const builtinMacros = require("./macros"); 8 const ParseError = require("./ParseError"); 9 const objectAssign = require("object-assign"); 10 11 function MacroExpander(input, macros) { 12 this.lexer = new Lexer(input); 13 this.macros = objectAssign({}, builtinMacros, macros); 14 this.stack = []; // contains tokens in REVERSE order 15 this.discardedWhiteSpace = []; 16 } 17 18 /** 19 * Recursively expand first token, then return first non-expandable token. 20 * 21 * At the moment, macro expansion doesn't handle delimited macros, 22 * i.e. things like those defined by \def\foo#1\end{…}. 23 * See the TeX book page 202ff. for details on how those should behave. 24 */ 25 MacroExpander.prototype.nextToken = function() { 26 for (;;) { 27 if (this.stack.length === 0) { 28 this.stack.push(this.lexer.lex()); 29 } 30 const topToken = this.stack.pop(); 31 const name = topToken.text; 32 if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) { 33 return topToken; 34 } 35 let tok; 36 let expansion = this.macros[name]; 37 if (typeof expansion === "string") { 38 let numArgs = 0; 39 if (expansion.indexOf("#") !== -1) { 40 const stripped = expansion.replace(/##/g, ""); 41 while (stripped.indexOf("#" + (numArgs + 1)) !== -1) { 42 ++numArgs; 43 } 44 } 45 const bodyLexer = new Lexer(expansion); 46 expansion = []; 47 tok = bodyLexer.lex(); 48 while (tok.text !== "EOF") { 49 expansion.push(tok); 50 tok = bodyLexer.lex(); 51 } 52 expansion.reverse(); // to fit in with stack using push and pop 53 expansion.numArgs = numArgs; 54 this.macros[name] = expansion; 55 } 56 if (expansion.numArgs) { 57 const args = []; 58 let i; 59 // obtain arguments, either single token or balanced {…} group 60 for (i = 0; i < expansion.numArgs; ++i) { 61 const startOfArg = this.get(true); 62 if (startOfArg.text === "{") { 63 const arg = []; 64 let depth = 1; 65 while (depth !== 0) { 66 tok = this.get(false); 67 arg.push(tok); 68 if (tok.text === "{") { 69 ++depth; 70 } else if (tok.text === "}") { 71 --depth; 72 } else if (tok.text === "EOF") { 73 throw new ParseError( 74 "End of input in macro argument", 75 startOfArg); 76 } 77 } 78 arg.pop(); // remove last } 79 arg.reverse(); // like above, to fit in with stack order 80 args[i] = arg; 81 } else if (startOfArg.text === "EOF") { 82 throw new ParseError( 83 "End of input expecting macro argument", topToken); 84 } else { 85 args[i] = [startOfArg]; 86 } 87 } 88 // paste arguments in place of the placeholders 89 expansion = expansion.slice(); // make a shallow copy 90 for (i = expansion.length - 1; i >= 0; --i) { 91 tok = expansion[i]; 92 if (tok.text === "#") { 93 if (i === 0) { 94 throw new ParseError( 95 "Incomplete placeholder at end of macro body", 96 tok); 97 } 98 tok = expansion[--i]; // next token on stack 99 if (tok.text === "#") { // ## → # 100 expansion.splice(i + 1, 1); // drop first # 101 } else if (/^[1-9]$/.test(tok.text)) { 102 // expansion.splice(i, 2, arg[0], arg[1], …) 103 // to replace placeholder with the indicated argument. 104 // TODO: use spread once we move to ES2015 105 expansion.splice.apply( 106 expansion, 107 [i, 2].concat(args[tok.text - 1])); 108 } else { 109 throw new ParseError( 110 "Not a valid argument number", 111 tok); 112 } 113 } 114 } 115 } 116 this.stack = this.stack.concat(expansion); 117 } 118 }; 119 120 MacroExpander.prototype.get = function(ignoreSpace) { 121 this.discardedWhiteSpace = []; 122 let token = this.nextToken(); 123 if (ignoreSpace) { 124 while (token.text === " ") { 125 this.discardedWhiteSpace.push(token); 126 token = this.nextToken(); 127 } 128 } 129 return token; 130 }; 131 132 /** 133 * Undo the effect of the preceding call to the get method. 134 * A call to this method MUST be immediately preceded and immediately followed 135 * by a call to get. Only used during mode switching, i.e. after one token 136 * was got in the old mode but should get got again in a new mode 137 * with possibly different whitespace handling. 138 */ 139 MacroExpander.prototype.unget = function(token) { 140 this.stack.push(token); 141 while (this.discardedWhiteSpace.length !== 0) { 142 this.stack.push(this.discardedWhiteSpace.pop()); 143 } 144 }; 145 146 module.exports = MacroExpander;