Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
if (typeof define !== 'function') { var define = require('amdefine')(module); }
define(['./narcissus', './fmt', './ref', './log', './assert', './comments', './tkn'], function(Narcissus, Fmt, Ref, Log, Assert, Comments, tkn) {
"use strict"; "use restrict";
var log = (typeof console !== "undefined") && console.log || print;
var Shaper = (function() {
Array.isArray = Array.isArray || function(o) {
return Object.prototype.toString.call(o) === "[object Array]";
};
function error(node, msg) {
var str = Fmt("{0}:{1} error: {2}", node.tokenizer.filename, node.lineno, msg);
if (typeof process !== "undefined") {
log(str);
process.exit(-1);
}
else {
throw new Error(str);
}
}
function deprecated(obj, params) {
obj[params.was] = function() {
throw new Error(Fmt.obj((params.now ?
"{was} is deprecated since version {since}, use {now} instead" :
"{was} is deprecated since version {since}"), params));
};
}
var traverseData = (function() {
var o = [];
o[tkn.ARRAY_COMP] = [/*expr*/"expression", /*COMP_TAIL*/"tail"];
o[tkn.CASE] = [/*expr*/"caseLabel", /*BLOCK*/"statements"];
o[tkn.CATCH] = [/*IDENTIFIER*/"_name", /*expr*/"guard", /*BLOCK*/"block"];
o[tkn.COMP_TAIL] = [/*[FOR_IN]*/"children", /*expr*/"guard"]; // has children but not last
o[tkn.DEFAULT] = [/*BLOCK*/"statements"];
o[tkn.DO] = [/*stmt*/"body", /*expr*/"condition"];
o[tkn.FOR] = [/*expr*/"setup", /*expr*/"condition", /*expr*/"update", /*stmt*/"body"];
o[tkn.FOR_IN] = [/*IDENTIFIER|VAR*/"_iterator", /*expr*/"object", /*stmt*/"body"];
o[tkn.FUNCTION] = [/*IDENTIFIER*/"_name", /*[IDENTIFIER]*/"_params", /*SCRIPT*/"body"];
o[tkn.GENERATOR] = [/*stmt*/"body", /*expr*/"expression", /*COMP_TAIL*/"tail"];
o[tkn.GETTER] = [/*SCRIPT*/"body"];
o[tkn.IF] = [/*expr*/"condition", /*stmt*/"thenPart", /*stmt*/"elsePart"];
o[tkn.LABEL] = [/*IDENTIFIER*/"_label", /*stmt*/"statement"];
o[tkn.LET_BLOCK] = [/*LET*/"variables", /*expr*/"expression", /*BLOCK*/"block"];
o[tkn.RETURN] = [/*expr*/"value"];
o[tkn.SEMICOLON] = [/*expr*/"expression"];
o[tkn.SETTER] = [/*SCRIPT*/"body"];
o[tkn.SWITCH] = [/*expr*/"discriminant", /*[CASE|DEFAULT]*/"cases"];
o[tkn.THROW] = [/*expr*/"exception"];
o[tkn.TRY] = [/*BLOCK*/"tryBlock", /*[CATCH]*/"catchClauses", /*BLOCK*/"finallyBlock"];
o[tkn.WHILE] = [/*expr*/"condition", /*stmt*/"body"];
o[tkn.WITH] = [/*expr*/"object", /*stmt*/"body"];
o[tkn.YIELD] = [/*expr*/"value"];
var c = [
/*[stmt]*/
tkn.SCRIPT, tkn.BLOCK,
/*[expr]*/
tkn.COMMA,
/*expr*/
tkn.GROUP,
/*[expr]*/
tkn.ARRAY_INIT,
/*[PROPERTY_INIT]*/
tkn.OBJECT_INIT,
/*IDENTIFIER, expr*/
tkn.PROPERTY_INIT,
/*[ASSIGN|IDENTIFIER]*/
tkn.LET, tkn.VAR, tkn.CONST,
/*expr*/
tkn.NEW,
/*expr, LIST*/
tkn.NEW_WITH_ARGS,
/*expr, LIST*/
tkn.CALL,
/*[expr]*/
tkn.LIST,
/*expr, expr, expr (ternary operator)*/
tkn.HOOK,
/*expr, expr (binary operator)*/
tkn.PLUS, tkn.MINUS, tkn.MUL, tkn.DIV, tkn.MOD,
tkn.LSH, tkn.RSH, tkn.URSH,
tkn.OR, tkn.AND,
tkn.BITWISE_OR, tkn.BITWISE_XOR, tkn.BITWISE_AND,
tkn.EQ, tkn.NE, tkn.STRICT_EQ, tkn.STRICT_NE,
tkn.LT, tkn.LE, tkn.GE, tkn.GT,
tkn.IN, tkn.INSTANCEOF,
tkn.INDEX,
/*IDENTIFIER|DOT|INDEX, expr (binary operator)*/
tkn.ASSIGN,
/*expr, IDENTIFIER (binary operator)*/
tkn.DOT,
/*IDENTIFIER|DOT|INDEX (unary operator)*/
tkn.INCREMENT, tkn.DECREMENT,
/*expr (unary operator)*/
tkn.UNARY_PLUS, tkn.UNARY_MINUS,
tkn.NOT, tkn.BITWISE_NOT,
tkn.DELETE, tkn.VOID, tkn.TYPEOF
];
// add "children" to all tokens enumerated in c
for (var i = 0; i < c.length; i++) {
if (o[c[i]]) {
throw new Error("createTraverseData: don't know ordering so "+
"can't add 'children' to existing traverseData");
}
o[c[i]] = ["children"];
}
return o;
})();
var extraTraverseData = (function() {
var x = {};
// These properties aren't nodes but may still be relevant
x[tkn.ASSIGN] = ["assignOp"]; // number ("value" has string representation)
x[tkn.INCREMENT] = ["postfix"]; // boolean ("value" is just "++")
x[tkn.DECREMENT] = ["postfix"]; // boolean
x[tkn.FUNCTION] = ["functionForm"]; // number
x[tkn.FOR_IN] = ["isEach"]; // boolean
x[tkn.SWITCH] = ["defaultIndex"]; // number
x[tkn.IDENTIFIER] = ["value"]; // string, same as "name" when part of VAR
x[tkn.NUMBER] = ["value"]; // number (can differ from srcs)
x[tkn.REGEXP] = ["value"]; // string
x[tkn.STRING] = ["value"]; // string (can differ from srcs)
return x;
})();
//// generic traverse
// visitfns: {pre: function, post: function}
// visit function signature: function(node, ref)
function traverse(node, visitfns, ref) {
// preconditions
if (!node) {
return node;
}
if (!(node instanceof Narcissus.parser.Node)) {
throw new Error(Fmt("traverse: expected Node, got {0}. {1}",
typeof node, ref));
}
ref = ref || new Ref();
// call pre callback, if any
if (visitfns.pre) {
var old = node;
node = visitfns.pre(node, ref) || node;
if (node === "break") {
return old;
}
else if (!(node instanceof Narcissus.parser.Node)) {
throw new Error("traverse: visitfns.pre invalid return type");
}
}
// traverse descendants
var subprops = traverseData[node.type] || [];
for (var i = 0; i < subprops.length; i++) {
var prop = subprops[i];
if (Array.isArray(node[prop])) {
for (var j = 0; j < node[prop].length; j++) {
traverse(node[prop][j], visitfns, new Ref(node, prop, j));
}
}
else {
traverse(node[prop], visitfns, new Ref(node, prop));
}
}
// call post callback, if any
if (visitfns.post) {
node = visitfns.post(node, ref) || node;
if (!(node instanceof Narcissus.parser.Node)) {
throw new Error("traverse: visitfns.post invalid return type");
}
}
return node;
}
var MISMATCH = 0;
var MATCH = 1;
var MATCH_REST = 2;
function matchCondition(node, cond) {
// TODO cond.capture invokes callback or stores in array?
// cond is a function
if (typeof cond === "function") {
return cond(node) ? cond : false;
}
// cond is an object
if (typeof cond !== "object") {
throw new Error("matchCondition: expected function or object, got "+ typeof cond);
}
for (var key in cond) {
var condVal = cond[key];
// special
if (key === "rest" && condVal) {
continue;
}
var nodeVal = node[key];
// cond.key is a function
if (typeof condVal === "function") {
if (!condVal(nodeVal)) {
return false;
}
}
// cond.key is a value
else if (!(condVal === nodeVal || isNaN(condVal) && isNaN(nodeVal))) {
return false;
}
}
return cond;
}
match.debug = false;
function match(t, n, conds) {
var i;
if (typeof t === "string") {
t = Shaper.parse(t);
}
if (typeof n === "string") {
throw new Error("match: expected second argument of type Node, got string");
}
conds = conds || {$: {}, $$: {rest: true}};
if (t && t.type === tkn.IDENTIFIER) {
var cond = conds[t.value];
// todo should conds match null/undefined?
if (cond !== undefined) {
if (matchCondition(n, cond)) {
return cond.rest ? MATCH_REST : MATCH;
}
}
}
if (!t || !n) {
match.debug && Log("{2} {0} {1}", t, n, !t === !n ? "match" : "mismatch");
return !t === !n ? MATCH : MISMATCH;
}
if (t.type !== n.type) {
// fail (type mismatch)
match.debug && Log("mismatch {0} {1}", t, n);
return MISMATCH;
}
if (t.type === tkn.IDENTIFIER ||
t.type === tkn.NUMBER ||
t.type === tkn.REGEXP ||
t.type === tkn.STRING) {
if (t.value === n.value ||
(t.type === tkn.NUMBER && isNaN(t.value) && isNaN(n.value))) {
// ok (terminals with matching values)
match.debug && Log("match {0} {1}", t, n);
return MATCH;
}
else {
// fail (terminals with different values)
match.debug && Log("mismatch {0} {1}", t, n);
return MISMATCH;
}
}
var extraprops = extraTraverseData[t.type] || [];
for (i = 0; i < extraprops.length; i++) {
var extra = extraprops[i];
if (extra === 'value') {
// handled above.
continue;
}
if (t[extra] !== n[extra]) {
match.debug && Log("mismatch {0} {1} {2}",
extra, t[extra], n[extra]);
return MISMATCH;
}
}
// traverse descendants
var subprops = traverseData[t.type] || [];
var res;
for (i = 0; i < subprops.length; i++) {
var prop = subprops[i];
// t[prop] is an array, such as BLOCK.children
if (Array.isArray(t[prop])) {
var rest = null; // bound to MATCH_REST node, if any
for (var j = 0, k = Math.max(t[prop].length, n[prop].length);
j < k; j++) {
var tt = rest || t[prop][j];
var nn = n[prop][j];
if (!tt || !nn) { // nodes or template starved (both can't be)
match.debug && Log("mismatch {0} {1}", tt, nn);
return MISMATCH;
}
res = match(tt, nn, conds);
if (res === MISMATCH) {
match.debug && Log("mismatch {0} {1}", tt, nn);
return MISMATCH;
}
else if (res === MATCH_REST) {
rest = tt;
match.debug && Log("match_rest {0} {1}", tt, nn);
}
}
}
// t[prop] is a regular node, such as IF.thenPart
else {
res = match(t[prop], n[prop], conds);
if (res === MISMATCH) {
match.debug && Log("mismatch {0} {1}", t[prop], n[prop]);
return MISMATCH;
}
// MATCH or MATCH_REST matches this node
}
}
match.debug && Log("match {0} {1}", t, n);
return MATCH;
}
//// mutate nodes
function replace(node, var_args) {
if (typeof node === "string") {
node = Shaper.parse(node);
}
var placeholders = [];
//collect all $ nodes into placeholders array
traverse(node, {pre: function(node, ref) {
if (node.type === tkn.IDENTIFIER && node.value === "$") {
placeholders.push(ref);
}
}});
var args = arguments.length === 2 && Array.isArray(var_args) ?
var_args :
Array.prototype.slice.call(arguments, 1);
if (args.length !== placeholders.length) {
throw new Error("replace: placeholders.length mismatch");
}
// replace placeholders with new nodes
for (var i = 0; i < placeholders.length; i++) {
placeholders[i].set(args[i]);
}
return node;
}
function renameIdentifier(node, name) {
Assert(node.type === tkn.IDENTIFIER);
node.value = node.srcs[0] = name;
}
function remove(ref) {
Assert(ref.properties.length === 2);
var node = ref.base;
var prop = ref.properties[0];
var index = Number(ref.properties[1]);
var len = node[prop].length;
Assert(index >= 0 && index < len);
if (len === 1) {
node.srcs[0] += node.srcs.pop();
}
else {
node.srcs.splice(index === len - 1 ? index : index + 1, 1);
}
node[prop].splice(index, 1);
}
// When the delimiter is (e.g.) a comma, like when adding elements to a
// list, we don't want to add the delimiter if the new element would be
// the last child (or if the parent initially had no children). But if
// the delimiter is whitespace or newlines, we want to always add the
// delimiter. Use the optional 'alwaysDelimit' parameter to distinguish
// these two cases.
function insertBefore(ref, node, delimiter, alwaysDelimit) {
Assert(ref.properties.length === 2);
_insert(ref.base, node, ref.properties[0], Number(ref.properties[1]), delimiter, alwaysDelimit);
}
function insertAfter(ref, node, delimiter, alwaysDelimit) {
Assert(ref.properties.length === 2);
_insert(ref.base, node, ref.properties[0], Number(ref.properties[1]) + 1, delimiter, alwaysDelimit);
}
function _insert(node, child, prop, pos, delimiter, alwaysDelimit) {
var srcs = node.srcs;
var children = node[prop];
if (pos === -1) {
pos = children.length;
}
Assert(pos >= 0 && pos <= children.length);
// no children thus srcs could be in style "(/*comments, whitespace*/ )"
// -> srcs: ["(/*comments, whitespace*/ ", ")"]
if (children.length === 0) {
var parens = srcs.pop();
var last = parens.length - 1;
var d = (alwaysDelimit && delimiter) ? delimiter : '';
srcs.push(parens.slice(0, last), d + parens.slice(last));
children.push(child);
}
// has children already, insert new delimiter in srcs
else {
// create default delimiter if possible
if (delimiter === undefined) {
// get indentation from first node in SCRIPT, minus { character if any
if (node.type === tkn.SCRIPT || node.type === tkn.BLOCK) {
delimiter = (srcs[0][0] === "{" ? srcs[0].slice(1) : srcs[0]);
}
else if (node.type === tkn.LIST) {
delimiter = ", ";
}
else {
throw new Error("_insert: Can't create default delimiter for node "+ node.toString(false));
}
}
// temporary hardcoded workaround for semicolon issues
// `{ var x; }` BLOCK has no SEMICOLON, block.srcs is { @; }
var splicePos = (pos === children.length ? pos : pos + 1);
if (splicePos === children.length && (node.type === tkn.SCRIPT || node.type === tkn.BLOCK)) {
if (srcs[children.length][0] === ";") {
srcs[children.length] = srcs[children.length].slice(1);
delimiter = ";"+ delimiter;
}
}
srcs.splice(splicePos, 0, delimiter);
children.splice(pos, 0, child);
}
}
function cloneComments(dst, src) {
if (src.leadingComment !== undefined) {
dst.leadingComment = src.leadingComment;
}
if (src.trailingComment !== undefined) {
dst.trailingComment = src.trailingComment;
}
}
//// printers
var Node = Narcissus.parser.Node;
Node.prototype.verboseString = (function(oldToString) {
return function(recurse) {
if (recurse === undefined) {
recurse = true;
}
var res;
var newToString = Node.prototype.toString;
if (recurse === true) {
Node.prototype.toString = oldToString;
res = this.toString();
}
else {
Node.prototype.toString = function() {
return newToString.call(this, false);
};
res = oldToString.call(this);
}
Node.prototype.toString = newToString;
return res;
};
})(Node.prototype.toString);
Node.prototype.tknString = function() {
var tt = this.type;
var defs = Narcissus.definitions;
var t = defs.tokens[tt];
return /^\W/.test(t) ? defs.opTypeNames[t] : t.toUpperCase();
};
Node.prototype.toString = function(recurse) {
if (recurse === undefined) {
recurse = true;
}
return recurse ? treeString(this) : nodeString(this);
};
function nodeString(node) {
function strPos(pos) {
return pos === undefined ? "?" : String(pos);
}
var src = node.tokenizer.source;
return node.tknString() +": "+
("srcs" in node ? Fmt.abbrev(JSON.stringify(node.srcs.join("@")).slice(1,-1), 60) :
"start" in node && "end" in node ?
Fmt(" '{0}'", JSON.stringify(Fmt.abbrev(src.slice(node.start, node.end), 30))) :
(node.value !== undefined ? Fmt(" ({0})", node.value) : "")) +
("start" in node || "end" in node ?
Fmt(" ({0}..{1})", strPos(node.start), strPos(node.end)) : "");
};
function treeString(node) {
var level = 0;
var lines = [];
traverse(node, {
pre: function(node, ref) {
var comments = [];
if (node.leadingComment) {
comments.push("leadingComment: "+ (Fmt.abbrev(node.leadingComment, 20) || ""));
}
if (node.trailingComment) {
comments.push("trailingComment: "+ (Fmt.abbrev(node.trailingComment, 20) || ""));
}
comments = comments.join(", ");
lines.push(Fmt("{0}{1} < {2}{3}",
Fmt.repeat(" ", level * 2),
nodeString(node),
ref.base ? ref.toString(ref.base.tknString()) : "root",
comments ? " "+ JSON.stringify(comments).slice(1, -1) : ""));
++level;
},
post: function(node, ref) {
--level;
}
});
return lines.join("\n");
}
Node.prototype.getSrc = function() {
var srcs = [];
traverse(this, {
pre: function(node, ref) {
var parent = ref.base;
if (parent) {
srcs.push(parent.srcs[parent.nPushed++]);
}
node.nPushed = 0;
if (node.leadingComment !== undefined) {
srcs.push(node.leadingComment);
}
},
post: function(node, ref) {
srcs.push(node.srcs[node.nPushed++]);
if (node.trailingComment !== undefined) {
srcs.push(node.trailingComment);
}
delete node.nPushed;
}
});
return srcs.join("");
};
//// parse and adjust
function parseScript(str, filename) {
return srcsify(adjustStartEnd(adjustComments(adjustStartEnd(Narcissus.parser.parse(str, filename || "<no filename>", 1)))));
}
function parse(str) {
var script = parseScript(str);
// only one statement/expression so skip SCRIPT node
if (script.children.length === 1) {
return script.children[0];
}
// SCRIPT contains multiple statements/expressions so return as-is
return script;
}
function adjustStartEnd(root) {
root.start = 0;
root.end = root.tokenizer.source.length;
return traverse(root, {post: function(node, ref) {
var parent = ref.base;
if (parent) {
if (parent.start === undefined || parent.end === undefined ||
node.start === undefined || node.end === undefined) {
throw new Error("adjustStartEnd: undefined start/end");
}
parent.start = Math.min(parent.start, node.start);
parent.end = Math.max(parent.end, node.end);
}
}});
}
function adjustComments(root) {
var comments = Comments.indexArray(root.tokenizer.source, root.tokenizer.comments);
// extend node.start to left to cover leading comment
// before: /*c*/ x*y+z, after: /*c*/ x*y+z
// ----- -----------
var i = 0;
try {
traverse(root, {pre: function(node, ref) {
while (true) {
if (i === comments.length) {
throw true; // abort traversal
}
else if (comments[i].next > node.start) {
return undefined;
}
else if (comments[i].next === node.start) {
node.origStart = node.start;
node.start = comments[i].start;
comments[i] = null;
}
++i;
}
}});
} catch (e) {}
// extend node.end to right to cover trailing comment
// before: x*y+z /*c*/, after: x*y+z /*c*/
// - -------
i = 0;
try {
traverse(root, {post: function(node, ref) {
while (true) {
while (i < comments.length && comments[i] === null) {
++i;
}
if (i === comments.length) {
throw true; // abort traversal
}
if (comments[i].prev > node.end) {
return undefined;
}
if (comments[i].prev === node.end) {
node.origEnd = node.end;
node.end = comments[i].end;
comments[i] = null;
}
++i;
}
}});
} catch (e) {}
return root;
}
function srcsify(root) {
var tokenizer = {
source: "",
filename: root.tokenizer.filename,
comments: root.tokenizer.comments
};
return traverse(root, {
pre: function(node, ref) {
var parent = ref.base;
node.pos = node.start;
node.srcs = [];
var src;
if (parent) {
if (parent.pos > node.start ||
node.start === undefined || node.end === undefined) {
throw new Error(Fmt("srcsify: src already covered. parent: {0} {1}:{2}",
parent, ref, node.toString(false)));
}
src = parent.tokenizer.source;
var frag = src.slice(parent.pos, node.start);
parent.srcs.push(frag);
parent.pos = node.end;
}
if (node.origStart !== undefined) { // has leadingComment
src = node.tokenizer.source;
node.leadingComment = src.slice(node.pos, node.origStart);
node.pos = node.origStart;
}
},
post: function(node, ref) {
var src = node.tokenizer.source;
if (node.origEnd !== undefined) { // has trailingComment
node.srcs.push(src.slice(node.pos, node.origEnd));
node.trailingComment = src.slice(node.origEnd, node.end);
}
else {
node.srcs.push(src.slice(node.pos, node.end));
}
delete node.pos;
delete node.start;
delete node.end;
delete node.origStart;
delete node.origEnd;
node.tokenizer = tokenizer;
//delete node.tokenizer;
}
});
}
// register shapes and run pipeline
var shapes = {};
function shaper(name, fn) {
return shapes[name] = fn;
}
function get(name) {
return shapes[name];
}
function run(root, pipeline) {
for (var i = 0; i < pipeline.length; i++) {
var shape = pipeline[i];
if (typeof shape !== "function") {
shape = shapes[shape];
}
root = shape(root) || root;
}
return root;
}
shaper("tree", function(root) {
log(root.toString());
});
shaper("source", function(root) {
var write = function(str) {
// log is going to add a trailing newline, so suppress the last one
// from str (if that's actually what it ends with)
if (str[str.length-1]=='\n') {
str = str.substring(0, str.length-1);
}
log(str);
};
// if we have a "print without trailing newline" function available,
// use it instead.
if (typeof process !== 'undefined') {
// node uses process.stdout.write
if (process.stdout && typeof process.stdout.write === 'function') {
write = process.stdout.write.bind(process.stdout);
}
}
var str = root.getSrc();
write(str);
});
shaper("version", function(root) {
log(Fmt("Shaper for JavaScript version {0}", shaper.version));
});
shaper.error = error;
shaper.traverse = traverse;
shaper.match = match;
shaper.replace = replace;
shaper.renameIdentifier = renameIdentifier;
shaper.remove = remove;
shaper.insertBefore = insertBefore;
shaper.insertAfter = insertAfter;
shaper.cloneComments = cloneComments;
shaper.parseScript = parseScript;
shaper.parse = parse;
shaper.get = get;
shaper.run = run;
shaper.tkn = tkn;
deprecated(shaper, {since: "0.1", was: "parseExpression", now: "parse"});
deprecated(shaper, {since: "0.1", was: "insertArgument", now: "insertBefore or insertAfter"});
deprecated(shaper, {since: "0.1", was: "traverseTree", now: "traverse"});
deprecated(Node.prototype, {since: "0.1", was: "printTree", now: "toString"});
shaper.version = "0.1-pre";
return shaper;
})();
return Shaper;
});