mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-20 22:39:50 -05:00
Switch to using Mozilla's standardized AST model for PaperScript parsing though Acorn.js or Esprima.js and their support for ranges.
No more AST mingling but direct code modification means we're finally getting accurate error messages! Sticking to Esprima for now since Acorn still has some issues with ranges: https://github.com/marijnh/acorn/issues/14
This commit is contained in:
parent
67dca29009
commit
34819e6a73
6 changed files with 3981 additions and 77 deletions
|
@ -42,7 +42,7 @@ case $1 in
|
||||||
;;
|
;;
|
||||||
compressed)
|
compressed)
|
||||||
eval $COMMAND > temp.js
|
eval $COMMAND > temp.js
|
||||||
../../uglifyjs/bin/uglifyjs temp.js --extra --unsafe --reserved-names "$eval,$sign" > $5
|
../../uglifyjs/bin/uglifyjs temp.js --extra --unsafe --reserved-names "_$_,$_" > $5
|
||||||
rm temp.js
|
rm temp.js
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
2
lib/acorn-min.js
vendored
2
lib/acorn-min.js
vendored
File diff suppressed because one or more lines are too long
16
lib/acorn.js
16
lib/acorn.js
|
@ -444,7 +444,8 @@
|
||||||
++tokPos;
|
++tokPos;
|
||||||
ch = input.charCodeAt(tokPos);
|
ch = input.charCodeAt(tokPos);
|
||||||
}
|
}
|
||||||
(tokComments || (tokComments = [])).push(input.slice(start, tokPos));
|
if (options.trackComments)
|
||||||
|
(tokComments || (tokComments = [])).push(input.slice(start, tokPos));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called at the start of the parse and after every token. Skips
|
// Called at the start of the parse and after every token. Skips
|
||||||
|
@ -878,7 +879,8 @@
|
||||||
if (tokCommentsAfter) {
|
if (tokCommentsAfter) {
|
||||||
node.commentsAfter = tokCommentsAfter;
|
node.commentsAfter = tokCommentsAfter;
|
||||||
tokCommentsAfter = null;
|
tokCommentsAfter = null;
|
||||||
} else if (lastFinishedNode && lastFinishedNode.end === lastEnd) {
|
} else if (lastFinishedNode && lastFinishedNode.end === lastEnd &&
|
||||||
|
lastFinishedNode.commentsAfter) {
|
||||||
node.commentsAfter = lastFinishedNode.commentsAfter;
|
node.commentsAfter = lastFinishedNode.commentsAfter;
|
||||||
lastFinishedNode.commentsAfter = null;
|
lastFinishedNode.commentsAfter = null;
|
||||||
}
|
}
|
||||||
|
@ -911,9 +913,8 @@
|
||||||
// Test whether a semicolon can be inserted at the current position.
|
// Test whether a semicolon can be inserted at the current position.
|
||||||
|
|
||||||
function canInsertSemicolon() {
|
function canInsertSemicolon() {
|
||||||
return tokType === _eof || tokType === _braceR ||
|
return !options.strictSemicolons &&
|
||||||
!options.strictSemicolons &&
|
(tokType === _eof || tokType === _braceR || newline.test(input.slice(lastEnd, tokStart)));
|
||||||
newline.test(input.slice(lastEnd, tokStart));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume a semicolon, or, failing that, see if we are allowed to
|
// Consume a semicolon, or, failing that, see if we are allowed to
|
||||||
|
@ -1421,6 +1422,7 @@
|
||||||
case _num: case _string: case _regexp:
|
case _num: case _string: case _regexp:
|
||||||
var node = startNode();
|
var node = startNode();
|
||||||
node.value = tokVal;
|
node.value = tokVal;
|
||||||
|
node.raw = input.slice(tokStart, tokEnd);
|
||||||
next();
|
next();
|
||||||
return finishNode(node, "Literal");
|
return finishNode(node, "Literal");
|
||||||
|
|
||||||
|
@ -1585,9 +1587,9 @@
|
||||||
|
|
||||||
function parseIdent(liberal) {
|
function parseIdent(liberal) {
|
||||||
var node = startNode();
|
var node = startNode();
|
||||||
node.name = tokType === _name ? tokVal : (liberal && tokType.keyword) || unexpected();
|
node.name = tokType === _name ? tokVal : (liberal && !options.forbidReserved && tokType.keyword) || unexpected();
|
||||||
next();
|
next();
|
||||||
return finishNode(node, "Identifier");
|
return finishNode(node, "Identifier");
|
||||||
}
|
}
|
||||||
|
|
||||||
})(typeof exports === "undefined" ? (window.acorn = {}) : exports);
|
})(typeof exports === "undefined" ? (window.acorn = {}) : exports);
|
36
lib/esprima-min.js
vendored
Normal file
36
lib/esprima-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3809
lib/esprima.js
Normal file
3809
lib/esprima.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -18,12 +18,17 @@
|
||||||
* @name PaperScript
|
* @name PaperScript
|
||||||
* @namespace
|
* @namespace
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*#*/ if (options.parser == 'acorn') {
|
||||||
|
/*#*/ include('../../lib/acorn-min.js');
|
||||||
|
/*#*/ } else {
|
||||||
|
/*#*/ include('../../lib/esprima-min.js');
|
||||||
|
/*#*/ }
|
||||||
|
|
||||||
var PaperScript = this.PaperScript = new function() {
|
var PaperScript = this.PaperScript = new function() {
|
||||||
/*#*/ include('../../lib/parse-js-min.js');
|
// Operators to overload
|
||||||
|
|
||||||
// Math Operators
|
var binaryOperators = {
|
||||||
|
|
||||||
var operators = {
|
|
||||||
'+': 'add',
|
'+': 'add',
|
||||||
'-': 'subtract',
|
'-': 'subtract',
|
||||||
'*': 'multiply',
|
'*': 'multiply',
|
||||||
|
@ -33,8 +38,18 @@ var PaperScript = this.PaperScript = new function() {
|
||||||
'!=': 'equals'
|
'!=': 'equals'
|
||||||
};
|
};
|
||||||
|
|
||||||
function $eval(left, operator, right) {
|
var unaryOperators = {
|
||||||
var handler = operators[operator];
|
'-': 'negate',
|
||||||
|
'+': null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use very short name for the binary operator (_$_) as well as the
|
||||||
|
// unary operator ($_), as operations will be replaced with then.
|
||||||
|
// The underscores stands for the values, and the $ for the operators.
|
||||||
|
|
||||||
|
// Binary Operator Handler
|
||||||
|
function _$_(left, operator, right) {
|
||||||
|
var handler = binaryOperators[operator];
|
||||||
if (left && left[handler]) {
|
if (left && left[handler]) {
|
||||||
var res = left[handler](right);
|
var res = left[handler](right);
|
||||||
return operator === '!=' ? !res : res;
|
return operator === '!=' ? !res : res;
|
||||||
|
@ -52,41 +67,21 @@ var PaperScript = this.PaperScript = new function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign Operators
|
// Unary Operator Handler
|
||||||
|
function $_(operator, value) {
|
||||||
var signOperators = {
|
var handler = unaryOperators[operator];
|
||||||
'-': 'negate'
|
if (handler && value && value[handler])
|
||||||
};
|
|
||||||
|
|
||||||
function $sign(operator, value) {
|
|
||||||
var handler = signOperators[operator];
|
|
||||||
if (value && value[handler]) {
|
|
||||||
return value[handler]();
|
return value[handler]();
|
||||||
}
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case '+': return +value;
|
case '+': return +value;
|
||||||
case '-': return -value;
|
case '-': return -value;
|
||||||
default:
|
default:
|
||||||
throw new Error('Implement Sign Operator: ' + operator);
|
throw new Error('Implement Unary Operator: ' + operator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AST Helpers
|
// AST Helpers
|
||||||
|
|
||||||
function isDynamic(exp) {
|
|
||||||
var type = exp[0];
|
|
||||||
return type != 'num' && type != 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOperator(operator, left, right) {
|
|
||||||
// Only replace operators with calls to $operator if the left hand side
|
|
||||||
// is potentially an object.
|
|
||||||
if (operators[operator] && isDynamic(left)) {
|
|
||||||
// Replace with call to $operator(left, operator, right):
|
|
||||||
return ['call', ['name', '$eval'],
|
|
||||||
[left, ['string', operator], right]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compiles PaperScript code into JavaScript code.
|
* Compiles PaperScript code into JavaScript code.
|
||||||
|
@ -97,49 +92,111 @@ var PaperScript = this.PaperScript = new function() {
|
||||||
* @return {String} The compiled PaperScript as JavaScript code.
|
* @return {String} The compiled PaperScript as JavaScript code.
|
||||||
*/
|
*/
|
||||||
function compile(code) {
|
function compile(code) {
|
||||||
// Use parse-js to translate the code into a AST structure which is then
|
// Use Acorn or Esprima to translate the code into an AST structure
|
||||||
// walked and parsed for operators to overload. The resulting AST is
|
// which is then walked and parsed for operators to overload.
|
||||||
// translated back to code and evaluated.
|
// Instead of modifying the AST and converting back to code, we directly
|
||||||
var ast = parse_js.parse(code),
|
// change the source code based on the parser's range information, so we
|
||||||
walker = parse_js.ast_walker(),
|
// can preserve line-numbers in syntax errors and remove the need for
|
||||||
walk = walker.walk;
|
// Escodegen.
|
||||||
|
|
||||||
ast = walker.with_walkers({
|
// Tracks code insertions so we can add their differences to the
|
||||||
'binary': function(operator, left, right) {
|
// original offsets.
|
||||||
// Handle simple mathematical operators here:
|
var insertions = [];
|
||||||
return handleOperator(operator, left = walk(left),
|
|
||||||
right = walk(right))
|
|
||||||
// Always return a new AST for this node, since we have
|
|
||||||
// processed left and right int he call above!
|
|
||||||
|| [this[0], operator, left, right];
|
|
||||||
},
|
|
||||||
|
|
||||||
'assign': function(operator, left, right) {
|
// Converts an original offset to the one in the current state of the
|
||||||
// Handle assignments like +=, -=, etc:
|
// modified code.
|
||||||
// Check if the assignment operator needs to be handled by paper
|
function getOffset(offset) {
|
||||||
// if so, convert the assignment to a simple = and use result of
|
var start = offset;
|
||||||
// of handleOperator on the right hand side.
|
// Add all insertions before this location together to calculate
|
||||||
var res = handleOperator(operator, left = walk(left),
|
// the current offset
|
||||||
right = walk(right));
|
for (var i = 0, l = insertions.length; i < l; i++) {
|
||||||
return res
|
var insertion = insertions[i];
|
||||||
? [this[0], true, left, res]
|
if (insertion[0] >= offset)
|
||||||
// Always return a new AST for the same reason as in binary
|
break;
|
||||||
: [this[0], operator, left, right];
|
offset += insertion[1];
|
||||||
},
|
}
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
'unary-prefix': function(operator, exp) {
|
// Returns the node's code as a string, taking insertions into account.
|
||||||
if (signOperators[operator] && isDynamic(exp)) {
|
function getCode(node) {
|
||||||
return ['call', ['name', '$sign'],
|
return code.substring(getOffset(node.range[0]),
|
||||||
[['string', operator], walk(exp)]];
|
getOffset(node.range[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replaces the node's code with a new version and keeps insertions
|
||||||
|
// information up-to-date.
|
||||||
|
function replaceCode(node, str) {
|
||||||
|
var start = getOffset(node.range[0]),
|
||||||
|
end = getOffset(node.range[1]);
|
||||||
|
var insert = 0;
|
||||||
|
// Sort insertions by their offset, so getOffest() can do its thing
|
||||||
|
for (var i = insertions.length - 1; i >= 0; i--) {
|
||||||
|
if (start > insertions[i][0]) {
|
||||||
|
insert = i + 1;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, function() {
|
insertions.splice(insert, 0, [start, str.length - end + start]);
|
||||||
return walk(ast);
|
code = code.substring(0, start) + str + code.substring(end);
|
||||||
});
|
}
|
||||||
|
|
||||||
return parse_js.gen_code(ast, {
|
// Recursively walks the AST and replaces the code of certain nodes
|
||||||
beautify: true
|
function walkAst(node) {
|
||||||
});
|
for (var key in node) {
|
||||||
|
if (key === 'range')
|
||||||
|
continue;
|
||||||
|
var value = node[key];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (var i = 0, l = value.length; i < l; i++)
|
||||||
|
walkAst(value[i]);
|
||||||
|
} else if (Base.isObject(value)) {
|
||||||
|
walkAst(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (node.type) {
|
||||||
|
case 'BinaryExpression':
|
||||||
|
if (node.operator in binaryOperators
|
||||||
|
&& node.left.type !== 'Literal') {
|
||||||
|
var left = getCode(node.left),
|
||||||
|
right = getCode(node.right);
|
||||||
|
replaceCode(node, '_$_(' + left + ', "' + node.operator
|
||||||
|
+ '", ' + right + ')');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'AssignmentExpression':
|
||||||
|
if (/^.=$/.test(node.operator)
|
||||||
|
&& node.left.type !== 'Literal') {
|
||||||
|
var left = getCode(node.left),
|
||||||
|
right = getCode(node.right);
|
||||||
|
replaceCode(node, left + ' = _$_(' + left + ', "'
|
||||||
|
+ node.operator[0] + '", ' + right + ')');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'UpdateExpression':
|
||||||
|
if (!node.prefix) {
|
||||||
|
var arg = getCode(node.argument);
|
||||||
|
replaceCode(node, arg + ' = _$_(' + arg + ', "'
|
||||||
|
+ node.operator[0] + '", 1)');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'UnaryExpression':
|
||||||
|
if (node.operator in unaryOperators
|
||||||
|
&& node.argument.type !== 'Literal') {
|
||||||
|
var arg = getCode(node.argument);
|
||||||
|
replaceCode(node, '$_("' + node.operator + '", '
|
||||||
|
+ arg + ')');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now do the parsing magic
|
||||||
|
/*#*/ if (options.parser == 'acorn') {
|
||||||
|
walkAst(acorn.parse(code, { ranges: true }));
|
||||||
|
/*#*/ } else {
|
||||||
|
walkAst(esprima.parse(code, { range: true }));
|
||||||
|
/*#*/ }
|
||||||
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue