123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- "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*\/\*|<!--)\s*#(if|elif|else|endif|expand|include|error)\b(?:\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, " ")
- .replace(/(^\s*)\/\*/g, "$1 ")
- .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;
|