jscc
Version:
Tiny and powerful preprocessor for conditional comments and replacement of compile-time variables in text files
239 lines • 7.77 kB
JavaScript
"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