www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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;