mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-26 01:58:06 +02:00
Fix preprocessor: nesting, error & tests
Features / bug fixes in the preprocessor: - Add word boundary after regex for preprocessor token matching. Previously, when you mistakenly used "#ifdef" instead of "#if", the line would be parsed as a preprocessor directive (because "#ifdef" starts with "#if"), but without condition (because "def" does not start with a space). Consequently, the condition would always be false and anything between "#ifdef" and "#endif" would not be included. - Add validation and error reporting everywhere, to aid debugging. - Support nested comments (by accounting for the whole stack of conditions, instead of only the current one). - Add #elif preprocessor command. Could be used as follows: //#if !FEATURE_ENABLED //#error FEATURE_ENABLED must be set //#endif - Add #error preprocessor command. - Add end-of-line word boundary after "-->" in the comment trimmer. Otherwise the pattern would also match "-->" in the middle of a line, and incorrectly convert something like "while(i-->0)" to "while(i0)". Code health: - Add unit tests for the preprocessor (run external/builder/test.js). - Fix broken link to MDN (resolved to DXR). - Refactor to use STATE_* names instead of magic numbers (the original meaning of the numbers is preserved, with one exception). - State 3 has been split in two states, to distinguish between being in an #if and #else. This is needed to ensure that #else cannot be started without an #if.
This commit is contained in:
parent
1d4e450a79
commit
f8af4d6567
38 changed files with 296 additions and 25 deletions
124
external/builder/builder.js
vendored
124
external/builder/builder.js
vendored
|
@ -10,16 +10,32 @@ var fs = require('fs'),
|
|||
vm = require('vm');
|
||||
|
||||
/**
|
||||
* A simple preprocessor that is based on the firefox preprocessor
|
||||
* see (https://developer.mozilla.org/en/Build/Text_Preprocessor). The main
|
||||
* difference is that this supports a subset of the commands and it supports
|
||||
* preproccesor commands in html style comments.
|
||||
* Currently Supported commands:
|
||||
* 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) {
|
||||
// TODO make this really read line by line.
|
||||
|
@ -37,10 +53,28 @@ function preprocess(inFilename, outFilename, defines) {
|
|||
function(line) {
|
||||
out += line + '\n';
|
||||
});
|
||||
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) {
|
||||
var realPath = fs.realpathSync(inFilename);
|
||||
var dir = path.dirname(realPath);
|
||||
preprocess(path.join(dir, file), writeLine, defines);
|
||||
try {
|
||||
preprocess(path.join(dir, file), 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) {
|
||||
|
@ -53,52 +87,92 @@ function preprocess(inFilename, outFilename, defines) {
|
|||
writeLine(line);
|
||||
}
|
||||
|
||||
var s, state = 0, stack = [];
|
||||
// not inside if or else (process lines)
|
||||
var STATE_NONE = 0;
|
||||
// inside if, condition false (ignore until #else or #endif)
|
||||
var STATE_IF_FALSE = 1;
|
||||
// inside else, #if was false, so #else is true (process lines until #endif)
|
||||
var STATE_ELSE_TRUE = 2;
|
||||
// inside if, condition true (process lines until #else or #endif)
|
||||
var STATE_IF_TRUE = 3;
|
||||
// inside else, #if was true, so #else is false (ignore lines until #endif)
|
||||
var STATE_ELSE_FALSE = 4;
|
||||
|
||||
var line;
|
||||
var state = STATE_NONE;
|
||||
var stack = [];
|
||||
var control =
|
||||
/^(?:\/\/|<!--)\s*#(if|else|endif|expand|include)(?:\s+(.*?)(?:-->)?$)?/;
|
||||
/* jshint -W101 */
|
||||
/^(?:\/\/|<!--)\s*#(if|elif|else|endif|expand|include|error)\b(?:\s+(.*?)(?:-->)?$)?/;
|
||||
/* jshint +W101 */
|
||||
var lineNumber = 0;
|
||||
while ((s = readLine()) !== null) {
|
||||
var loc = function() {
|
||||
return fs.realpathSync(inFilename) + ':' + lineNumber;
|
||||
};
|
||||
while ((line = readLine()) !== null) {
|
||||
++lineNumber;
|
||||
var m = control.exec(s);
|
||||
var m = control.exec(line);
|
||||
if (m) {
|
||||
switch (m[1]) {
|
||||
case 'if':
|
||||
stack.push(state);
|
||||
try {
|
||||
state = vm.runInNewContext(m[2], defines) ? 3 : 1;
|
||||
} catch (e) {
|
||||
console.error('Could not evalute line \'' + m[2] + '\' at ' +
|
||||
fs.realpathSync(inFilename) + ':' + lineNumber);
|
||||
throw e;
|
||||
state = evaluateCondition(m[2]) ? STATE_IF_TRUE : STATE_IF_FALSE;
|
||||
break;
|
||||
case 'elif':
|
||||
if (state === STATE_IF_TRUE) {
|
||||
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 || state === STATE_ELSE_FALSE) {
|
||||
throw new Error('Found #elif after #else at ' + loc());
|
||||
} else {
|
||||
throw new Error('Found #elif without matching #if at ' + loc());
|
||||
}
|
||||
break;
|
||||
case 'else':
|
||||
state = state === 1 ? 3 : 2;
|
||||
if (state === STATE_IF_TRUE) {
|
||||
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 === 0 || state === 3) {
|
||||
if (state !== STATE_IF_FALSE && state !== STATE_ELSE_FALSE) {
|
||||
expand(m[2]);
|
||||
}
|
||||
break;
|
||||
case 'include':
|
||||
if (state === 0 || state === 3) {
|
||||
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 === 0) {
|
||||
writeLine(s);
|
||||
} else if (state === 3) {
|
||||
writeLine(s.replace(/^\/\/|^<!--|-->/g, ' '));
|
||||
if (state === STATE_NONE) {
|
||||
writeLine(line);
|
||||
} else if ((state === STATE_IF_TRUE || state === STATE_ELSE_TRUE) &&
|
||||
stack.indexOf(STATE_IF_FALSE) === -1 &&
|
||||
stack.indexOf(STATE_ELSE_FALSE) === -1) {
|
||||
writeLine(line.replace(/^\/\/|^<!--|-->$/g, ' '));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state !== 0 || stack.length !== 0) {
|
||||
throw new Error('Missing endif in preprocessor.');
|
||||
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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue