UNPKG

jscc

Version:

Tiny and powerful preprocessor for conditional comments and replacement of compile-time variables in text files

239 lines 7.77 kB
"use strict"; /* Parser for conditional comments */ const evalExpr = require("./eval-expr"); const getExpr = require("./get-expr"); const R = require("./regexes"); // Want this to check endif scope const ENDIF_MASK = 1 /* IF */ | 2 /* ELSE */; /** * Matches a line with a directive without its line-ending because it can be * at the end of the file with no last EOL. * * $1: Directive without the '#' ('if', 'elif', 'else', etc) * $2: Possible expression (can be empty or have a comment) */ const S_RE_BASE = /^[ \t\f\v]*(?:@)#(if|ifn?set|elif|else|endif|set|unset|error)(?:(?=[ \t])(.*)|\/\/.*)?$/.source; /** * Conditional comments parser * * @param {object} props - The global options */ class Parser { constructor(options) { this.options = options; this._cc = [{ block: 0 /* NONE */, state: 0 /* WORKING */, }]; } /** * Returns a regex that matches lines with directives through all the buffer. * * @returns {RegExp} regex with the flags `global` and `multiline` */ getRegex() { return RegExp(S_RE_BASE.replace('@', this.options.prefixes), 'gm'); } /** * Parses conditional comments to determinate if we need disable the output. * * @param {Array} match - Object with the key/value of the directive * @returns {boolean} Output state, `false` to hide the output. */ parse(match) { const key = match[1]; const expr = this._normalize(key, match[2]); let ccInfo = this._cc[this._cc.length - 1]; switch (key) { // #if* pushes WORKING or TESTING, unless the state is ENDING case 'if': case 'ifset': case 'ifnset': ccInfo = this._pushState(ccInfo, key, expr); break; case 'elif': case 'else': // #elif swap the state, unless it is ENDING // #else set the state to WORKING or ENDING this._handleElses(ccInfo, key, expr); break; case 'endif': // #endif pops the state ccInfo = this._popState(ccInfo, key); break; default: // #set #unset #error is processed for working blocks only this._handleInstruction(key, expr, ccInfo.state); } return ccInfo.state === 0 /* WORKING */; } /** * Check unclosed blocks before vanish. * * @returns {boolean} `true` if no error. */ close() { const cc = this._cc; const err = cc.length !== 1 || cc[0].state !== 0 /* WORKING */; if (err) { this._emitError('Unexpected end of file'); } } /** * Internal error handler. * This wrap a call to `options.errorHandler` that throws an exception. * * _NOTE:_ Sending `Error` enhances coverage of errorHandler that must * be prepared to receive Error objects in addition to strings. * * @param {string} message - Description of the error */ _emitError(message) { this.options.errorHandler(new Error(message)); } /** * Retrieve the required expression with the jscc comment removed. * It is necessary to skip quoted strings and avoid truncation * of expressions like "file:///path" * * @param {string} key The key name * @param {string} expr The extracted expression * @returns {string} Normalized expression. */ _normalize(key, expr) { // anything after `#else/#endif` is ignored if (key === 'else' || key === 'endif') { return ''; } // ...other keywords must have an expression if (!expr) { this._emitError(`Expression expected for #${key}`); } // get a normalized expression return getExpr(key, expr); } /** * Throws if the current block is not of the expected type. */ _checkBlock(ccInfo, key) { const block = ccInfo.block; const mask = key === 'endif' ? ENDIF_MASK : 1 /* IF */; if (block === 0 /* NONE */ || block !== (block & mask)) { this._emitError(`Unexpected #${key}`); } } /** * Push a `#if`, `#ifset`, or `#ifnset` directive */ _pushState(ccInfo, key, expr) { ccInfo = { block: 1 /* IF */, state: ccInfo.state === 2 /* ENDING */ ? 2 /* ENDING */ : this._getIfValue(key, expr) ? 0 /* WORKING */ : 1 /* TESTING */, }; this._cc.push(ccInfo); return ccInfo; } /** * Handles `#elif` and `#else` directives. */ _handleElses(ccInfo, key, expr) { this._checkBlock(ccInfo, key); if (key === 'else') { ccInfo.block = 2 /* ELSE */; ccInfo.state = ccInfo.state === 1 /* TESTING */ ? 0 /* WORKING */ : 2 /* ENDING */; } else if (ccInfo.state === 0 /* WORKING */) { ccInfo.state = 2 /* ENDING */; } else if (ccInfo.state === 1 /* TESTING */ && this._getIfValue('if', expr)) { ccInfo.state = 0 /* WORKING */; } } /** * Pop the if, ifset, or ifnset directives after endif. */ _popState(ccInfo, key) { this._checkBlock(ccInfo, key); const cc = this._cc; cc.pop(); return cc[cc.length - 1]; } /** * Handles an instruction that change a varname or emit an error * (currenty #set, #unset, and #error). * * @param expr Normalized expression */ _handleInstruction(key, expr, state) { if (state === 0 /* WORKING */) { switch (key) { case 'set': this._set(expr); break; case 'unset': this._unset(expr); break; case 'error': expr = String(evalExpr(this.options, expr)); this._emitError(expr); } } } /** * Evaluates an expression and add the result to the `values` property. * * @param expr Expression normalized in the "varname=value" format */ _set(expr) { const match = expr.match(R.ASSIGNMENT); if (match) { const varname = match[1]; const exprStr = match[2] || ''; this.options.values[varname] = exprStr ? evalExpr(this.options, exprStr.trim()) : undefined; } else { this._emitError(`Invalid memvar name or assignment: ${expr}`); } } /** * Remove the definition of a variable. * * @param varname Variable name */ _unset(varname) { if (varname.match(R.VARNAME)) { delete this.options.values[varname]; } else { this._emitError(`Invalid memvar name "${varname}"`); } } /** * Evaluates the expression of a `#if`, `#ifset`, or `#ifnset` directive. * * For `#ifset` and #ifnset, the value is evaluated here, * For `#if`, it calls `evalExpr`. * * @param key The key name * @param expr The extracted expression * @returns Evaluated expression. */ _getIfValue(key, expr) { // Returns the raw value for #if expressions if (key === 'if') { return evalExpr(this.options, expr) ? 1 : 0; } // Returns a boolean-like number for ifdef/ifndef let yes = expr in this.options.values ? 1 : 0; if (key === 'ifnset') { yes ^= 1; // invert } return yes; } } module.exports = Parser; //# sourceMappingURL=parser.js.map