prepr
Version:
Preprocess string in C/GLSL-preprocessor fashion
344 lines (273 loc) • 8.26 kB
JavaScript
/**
* Preprocess in C-preprocessor fashion
* @module prepr
*/
import paren from 'parenthesis';
import balanced from 'balanced-match';
import extend from 'object-assign';
import escaper from 'escaper';
import createFn from 'subscript/justin.js';
import stripComments from 'strip-json-comments';
/**
* Main processing function
*/
function preprocess (what, how) {
var result = '';
var source = what + '';
//defined macros
//FIXME: provide real values here
var macros = extend({
__LINE__: 0,
__FILE__: '_',
__VERSION__: 100,
defined: function (arg) {
return [].slice.call(arguments).every(function (arg) {
return macros[arg] != null;
}) ? 1 : 0;
}
}, how);
return process(source);
//process chunk of a string by finding out macros and replacing them
function process (str) {
if (!str) return '';
var arr = [];
var chunk = str;
//find next directive, get chunk to process before it
var directive = /#[A-Za-z0-9_$]+/ig.exec(str);
//get chunk to process - before next call
if (directive) {
chunk = chunk.slice(0, directive.index);
str = str.slice(directive.index);
}
//escape bad things
chunk = escape(chunk, arr);
//replace all defined X to defined (X)
chunk = chunk.replace(/\bdefined\s*([A-Za-z0-9_$]+)/g, 'defined($1)');
//for each registered macro do it’s call
for (var name in macros) {
//fn macro
if (macros[name] instanceof Function) {
chunk = processFunction(chunk, name, macros[name]);
}
}
chunk = escape(chunk, arr);
//for each defined var do replacement
for (var name in macros) {
//value replacement
if (!(macros[name] instanceof Function)) {
chunk = processDefinition(chunk, name, macros[name]);
}
}
chunk = unescape(chunk, arr);
//process directive
if (directive) {
if (/^#def/.test(directive[0])) {
str = define(str);
}
else if (/^#undef/.test(directive[0])) {
str = undefine(str);
}
else if (/^#if/.test(directive[0])) {
str = processIf(str);
}
else if (/^#line/.test(directive[0])) {
var data = /#[A-Za-z0-9_]+\s*([-0-9]+)?[^\n]*/.exec(str);
macros.__LINE__ = parseInt(data[1]);
str = str.slice(data.index + data[0].length);
}
else if (/^#version/.test(directive[0])) {
var data = /#[A-Za-z0-9_]+\s*([-0-9]+)?[^\n]*/.exec(str);
macros.__VERSION__ = parseInt(data[1]);
str = str.slice(data.index + data[0].length);
}
else {
//drop directive line
var directiveDecl = /\n/m.exec(str);
chunk += str.slice(0, directiveDecl.index) + '\n';
str = str.slice(directiveDecl.index)
}
return chunk + process(str);
}
return chunk;
}
//replace defined macros from a string
function processFunction (str, name, fn) {
var arr = [];
str = escape(str, arr);
var parts = paren(str, {
flat: true,
brackets: '()',
escape: '___'
});
var re = new RegExp(name + '\\s*\\(___([0-9]+)___\\)', 'g');
//replace each macro call with result
parts = parts.map(function (part) {
return part.replace(re, function (match, argsPartIdx) {
//parse arguments
var args = parts[argsPartIdx].trim();
if (args.length) {
args = args.split(/\s*,\s*/);
args = args.map(function (arg) {
var argParts = parts.slice();
argParts[0] = arg;
return paren.stringify(argParts, {flat: true, escape: '___'});
}).map(function (arg) {
return arg;
});
} else {
args = [];
}
if (args.length != fn.length) throw Error(`macro "${name}" requires ${fn.length} arguments, but ${args.length} given`);
//apply macro call with args
return fn.apply(null, args);
});
});
str = paren.stringify(parts, {flat: true, escape: '___'});
str = unescape(str, arr);
return str;
}
//replace defined variables from a string
function processDefinition (str, name, value) {
var arr = [];
str = escape(str, arr);
//apply concatenation ENTRY ## something → valueSomething
str = str.replace(new RegExp(`([^#A-Za-z0-9_$]|^)${name}\\s*##\\s*([A-Za-z0-9_$]*)`, 'g'), function (match, pre, post) {
return pre + value + post;
});
str = str.replace(new RegExp(`([A-Za-z0-9_$]*)\\s*##\\s*${name}([^A-Za-z0-9_$]|$)`, 'g'), function (match, pre, post) {
return pre + value + post;
});
//replace definition entries
str = str.replace(new RegExp(`([^#A-Za-z0-9_$]|^)${name}([^A-Za-z0-9_$]|$)`, 'g'), function (match, pre, post) {
//insert definition
if (macros[value] != null && !(macros[value] instanceof Function)) value = macros[value];
return pre + value + post;
});
//replace stringifications
str = str.replace(new RegExp(`#${name}([^A-Za-z0-9_$]|$)`, 'g'), function (match, post) {
return '"' + value + '"' + post;
});
str = unescape(str, arr);
return str;
}
//helpers to escape unfoldable things in strings
function escape (str, arr) {
return escaper.replace(str, true, arr)
}
function unescape (str, arr) {
if (!arr || !arr.length) return str
return escaper.paste(str, arr)
}
//register macro, #define directive
function define (str) {
var data = /#[A-Za-z]+[ ]*([A-Za-z0-9_$]*)(?:\(([^\(\)]*)\))?[ \r]*([^\n]*)$/m.exec(str);
str = str.slice(data.index + data[0].length);
var name = data[1];
var args = data[2];
var value = data[3] || true;
if (!name || !value) throw Error(`Macro definition "${data[0]}" is malformed`);
//register function macro
//#define FOO(A, B) (expr)
if (args != null) {
if (args.trim().length) {
args = args.split(/\s*,\s*/);
}
else {
args = [];
}
function fn () {
var result = value;
//for each arg - replace it’s occurence in `result`
for (var i = 0; i < args.length; i++) {
result = processDefinition(result, args[i], arguments[i]);
}
result = process(result);
return result;
};
Object.defineProperty(fn, 'length', {
value: args.length
});
macros[name] = fn;
}
//register value macro
//#define FOO insertion
//#define FOO (expr)
else {
macros[name] = value;
}
return str;
}
//unregister macro, #undef directive
function undefine (str) {
var data = /#[A-Za-z0-9_]+[ ]*([A-Za-z0-9_$]+)/.exec(str);
delete macros[data[1]];
return str.slice(data.index + data[0].length);
}
//process if/else/ifdef/elif/ifndef/defined
function processIf (str) {
var match = balanced('#if', '#endif', str)
//if no nested ifs - means we are in clause, return as is
if (!match) return str;
var body = match.body;
var post = match.post;
var elseBody = '';
//find else part
var matchElse = balanced('#if', '#else', str)
if (matchElse && matchElse.end <= match.end) {
elseBody = matchElse.post.slice(matchElse.post.indexOf('\n'), -match.post.length - 6);
body = matchElse.body;
}
//ifdef
if(/^def/.test(body)) {
body = body.slice(3);
var nameMatch = /[A-Za-z0-9_$]+/.exec(body);
var name = nameMatch[0];
body = body.slice(name.length + nameMatch.index);
if (macros[name] != null) str = process(body);
else str = process(elseBody);
}
//ifndef
else if (/^ndef/.test(body)) {
body = body.slice(4);
var nameMatch = /[A-Za-z0-9_$]+/.exec(body);
var name = nameMatch[0];
body = body.slice(name.length + nameMatch.index);
if (macros[name] == null) str = process(body);
else str = process(elseBody);
}
//if
else {
//split elifs
var clauses = body.split(/^\s*#elif\s+/m);
var result = false;
//find first triggered clause
for (var i = 0; i < clauses.length; i++) {
var clause = clauses[i];
var exprMatch = /\s*(.*)/.exec(clause);
var expr = exprMatch[0];
clause = clause.slice(expr.length + exprMatch.index);
//eval expression
expr = stripComments(process(expr));
try {
var evaluate = createFn(expr);
result = evaluate(expr, macros);
} catch (e) {
result = false;
}
if (result) {
str = process(clause);
break;
}
}
//else clause
if (!result) {
str = process(elseBody);
}
}
//trim post till the first endline, because there may be comments after #endif
var match = /[\n\r]/.exec(post);
if (match) post = post.slice(match.index);
return str + post;
}
}
export default preprocess;