"use strict"; const fs = require("fs"), path = require("path"), vm = require("vm"); const AllWhitespaceRegexp = /^\s+$/g; /** * A simple preprocessor that is based on the Firefox preprocessor * (https://dxr.mozilla.org/mozilla-central/source/build/docs/preprocessor.rst). * The main difference is that this supports a subset of the commands and it * supports preprocessor commands in HTML-style comments. * * Currently supported commands: * - if * - elif * - else * - endif * - include * - expand * - error * * Every #if must be closed with an #endif. Nested conditions are supported. * * Within an #if or #else block, one level of comment tokens is stripped. This * allows us to write code that can run even without preprocessing. For example: * * //#if SOME_RARE_CONDITION * // // Decrement by one * // --i; * //#else * // // Increment by one. * ++i; * //#endif */ function preprocess(inFilename, outFilename, defines) { let lineNumber = 0; function loc() { return fs.realpathSync(inFilename) + ":" + lineNumber; } function expandCssImports(content, baseUrl) { return content.replace( /^\s*@import\s+url\(([^)]+)\);\s*$/gm, function (all, url) { const file = path.join(path.dirname(baseUrl), url); const imported = fs.readFileSync(file, "utf8").toString(); return expandCssImports(imported, file); } ); } // TODO make this really read line by line. let content = fs.readFileSync(inFilename, "utf8").toString(); // Handle CSS-imports first, when necessary. if (/\.css$/i.test(inFilename)) { content = expandCssImports(content, inFilename); } const lines = content.split("\n"), totalLines = lines.length; const out = []; let i = 0; function readLine() { if (i < totalLines) { return lines[i++]; } return null; } const writeLine = typeof outFilename === "function" ? outFilename : function (line) { if (!line || AllWhitespaceRegexp.test(line)) { const prevLine = out[out.length - 1]; if (!prevLine || AllWhitespaceRegexp.test(prevLine)) { return; // Avoid adding consecutive blank lines. } } out.push(line); }; function evaluateCondition(code) { if (!code || !code.trim()) { throw new Error("No JavaScript expression given at " + loc()); } try { return vm.runInNewContext(code, defines, { displayErrors: false }); } catch (e) { throw new Error( 'Could not evaluate "' + code + '" at ' + loc() + "\n" + e.name + ": " + e.message ); } } function include(file) { const realPath = fs.realpathSync(inFilename); const dir = path.dirname(realPath); try { let fullpath; if (file.indexOf("$ROOT/") === 0) { fullpath = path.join( __dirname, "../..", file.substring("$ROOT/".length) ); } else { fullpath = path.join(dir, file); } preprocess(fullpath, writeLine, defines); } catch (e) { if (e.code === "ENOENT") { throw new Error('Failed to include "' + file + '" at ' + loc()); } throw e; // Some other error } } function expand(line) { line = line.replace(/__[\w]+__/g, function (variable) { variable = variable.substring(2, variable.length - 2); if (variable in defines) { return defines[variable]; } return ""; }); writeLine(line); } // not inside if or else (process lines) const STATE_NONE = 0; // inside if, condition false (ignore until #else or #endif) const STATE_IF_FALSE = 1; // inside else, #if was false, so #else is true (process lines until #endif) const STATE_ELSE_TRUE = 2; // inside if, condition true (process lines until #else or #endif) const STATE_IF_TRUE = 3; // inside else or elif, #if/#elif was true, so following #else or #elif is // false (ignore lines until #endif) const STATE_ELSE_FALSE = 4; let line; let state = STATE_NONE; const stack = []; const control = /^(?:\/\/|\s*\/\*|)?$)?/; while ((line = readLine()) !== null) { ++lineNumber; const m = control.exec(line); if (m) { switch (m[1]) { case "if": stack.push(state); state = evaluateCondition(m[2]) ? STATE_IF_TRUE : STATE_IF_FALSE; break; case "elif": if (state === STATE_IF_TRUE || state === STATE_ELSE_FALSE) { state = STATE_ELSE_FALSE; } else if (state === STATE_IF_FALSE) { state = evaluateCondition(m[2]) ? STATE_IF_TRUE : STATE_IF_FALSE; } else if (state === STATE_ELSE_TRUE) { throw new Error("Found #elif after #else at " + loc()); } else { throw new Error("Found #elif without matching #if at " + loc()); } break; case "else": if (state === STATE_IF_TRUE || state === STATE_ELSE_FALSE) { state = STATE_ELSE_FALSE; } else if (state === STATE_IF_FALSE) { state = STATE_ELSE_TRUE; } else { throw new Error("Found #else without matching #if at " + loc()); } break; case "endif": if (state === STATE_NONE) { throw new Error("Found #endif without #if at " + loc()); } state = stack.pop(); break; case "expand": if (state !== STATE_IF_FALSE && state !== STATE_ELSE_FALSE) { expand(m[2]); } break; case "include": if (state !== STATE_IF_FALSE && state !== STATE_ELSE_FALSE) { include(m[2]); } break; case "error": if (state !== STATE_IF_FALSE && state !== STATE_ELSE_FALSE) { throw new Error("Found #error " + m[2] + " at " + loc()); } break; } } else { if (state === STATE_NONE) { writeLine(line); } else if ( (state === STATE_IF_TRUE || state === STATE_ELSE_TRUE) && !stack.includes(STATE_IF_FALSE) && !stack.includes(STATE_ELSE_FALSE) ) { writeLine( line .replace(/^\/\/|^$/g, "") ); } } } if (state !== STATE_NONE || stack.length !== 0) { throw new Error( "Missing #endif in preprocessor for " + fs.realpathSync(inFilename) ); } if (typeof outFilename !== "function") { fs.writeFileSync(outFilename, out.join("\n")); } } exports.preprocess = preprocess; /** * Merge two defines arrays. Values in the second param will override values in * the first. */ function merge(defaults, defines) { const ret = Object.create(null); for (const key in defaults) { ret[key] = defaults[key]; } for (const key in defines) { ret[key] = defines[key]; } return ret; } exports.merge = merge;