big refactor -- pulled all helper functions into helpers.coffee to facilitate sharing.

This commit is contained in:
Jeremy Ashkenas 2010-03-09 21:24:30 -05:00
parent 4932d25540
commit b297510d2b
9 changed files with 225 additions and 240 deletions

View File

@ -23,7 +23,7 @@ end
desc "Build the single concatenated and minified script for the browser"
task :browser do
sources = %w(rewriter.js lexer.js parser.js scope.js nodes.js coffee-script.js)
sources = %w(helpers.js rewriter.js lexer.js parser.js scope.js nodes.js coffee-script.js)
code = sources.map {|s| File.read('lib/' + s) }.join('')
code = YUI::JavaScriptCompressor.new.compress(code)
File.open('extras/coffee-script.js', 'w+') {|f| f.write(code) }

125
lib/helpers.js Normal file
View File

@ -0,0 +1,125 @@
(function(){
var __hasProp = Object.prototype.hasOwnProperty;
// Set up exported variables for both Node.js and the browser.
if (!((typeof process !== "undefined" && process !== null))) {
this.exports = this;
}
// Does a list include a value?
exports.include = function include(list, value) {
return list.indexOf(value) >= 0;
};
// Peek at the beginning of a given string to see if it matches a sequence.
exports.starts = function starts(string, literal, start) {
return string.substring(start, (start || 0) + literal.length) === literal;
};
// Trim out all falsy values from an array.
exports.compact = function compact(array) {
var _a, _b, _c, _d, item;
_a = []; _b = array;
for (_c = 0, _d = _b.length; _c < _d; _c++) {
item = _b[_c];
if (item) {
_a.push(item);
}
}
return _a;
};
// Count the number of occurences of a character in a string.
exports.count = function count(string, letter) {
var num, pos;
num = 0;
pos = string.indexOf(letter);
while (pos !== -1) {
num += 1;
pos = string.indexOf(letter, pos + 1);
}
return num;
};
// Merge objects, returning a fresh copy with attributes from both sides.
// Used every time `compile` is called, to allow properties in the options hash
// to propagate down the tree without polluting other branches.
exports.merge = function merge(options, overrides) {
var _a, _b, fresh, key, val;
fresh = {};
_a = options;
for (key in _a) { if (__hasProp.call(_a, key)) {
val = _a[key];
((fresh[key] = val));
}}
if (overrides) {
_b = overrides;
for (key in _b) { if (__hasProp.call(_b, key)) {
val = _b[key];
((fresh[key] = val));
}}
}
return fresh;
};
// Return a completely flattened version of an array. Handy for getting a
// list of `children`.
exports.flatten = function flatten(array) {
var _a, _b, _c, item, memo;
memo = [];
_a = array;
for (_b = 0, _c = _a.length; _b < _c; _b++) {
item = _a[_b];
item instanceof Array ? (memo = memo.concat(item)) : memo.push(item);
}
return memo;
};
// Delete a key from an object, returning the value. Useful when a node is
// looking for a particular method in an options hash.
exports.del = function del(obj, key) {
var val;
val = obj[key];
delete obj[key];
return val;
};
// Matches a balanced group such as a single or double-quoted string. Pass in
// a series of delimiters, all of which must be nested correctly within the
// contents of the string. This method allows us to have strings within
// interpolations within strings etc...
exports.balanced_string = function balanced_string(str, delimited, options) {
var _a, _b, _c, _d, close, i, levels, open, pair, slash;
options = options || {};
slash = delimited[0][0] === '/';
levels = [];
i = 0;
while (i < str.length) {
if (levels.length && exports.starts(str, '\\', i)) {
i += 1;
} else {
_a = delimited;
for (_b = 0, _c = _a.length; _b < _c; _b++) {
pair = _a[_b];
_d = pair;
open = _d[0];
close = _d[1];
if (levels.length && exports.starts(str, close, i) && levels[levels.length - 1] === pair) {
levels.pop();
i += close.length - 1;
if (!(levels.length)) {
i += 1;
}
break;
} else if (exports.starts(str, open, i)) {
levels.push(pair);
i += open.length - 1;
break;
}
}
}
if (!levels.length || slash && exports.starts(str, '\n', i)) {
break;
}
i += 1;
}
if (levels.length) {
if (slash) {
return false;
}
throw new Error("SyntaxError: Unterminated " + (levels.pop()[0]) + " starting on line " + (this.line + 1));
}
return !i ? false : str.substring(0, i);
};
})();

View File

@ -1,5 +1,5 @@
(function(){
var ACCESSORS, ASSIGNMENT, BEFORE_WHEN, CALLABLE, CODE, COFFEE_KEYWORDS, COMMENT, COMMENT_CLEANER, HEREDOC, HEREDOC_INDENT, IDENTIFIER, INTERPOLATION, JS_CLEANER, JS_FORBIDDEN, JS_KEYWORDS, KEYWORDS, LAST_DENT, LAST_DENTS, Lexer, MULTILINER, MULTI_DENT, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, REGEX_ESCAPE, REGEX_FLAGS, REGEX_INTERPOLATION, REGEX_START, RESERVED, Rewriter, STRING_NEWLINES, WHITESPACE, compact, count, include, starts;
var ACCESSORS, ASSIGNMENT, BEFORE_WHEN, CALLABLE, CODE, COFFEE_KEYWORDS, COMMENT, COMMENT_CLEANER, HEREDOC, HEREDOC_INDENT, IDENTIFIER, INTERPOLATION, JS_CLEANER, JS_FORBIDDEN, JS_KEYWORDS, KEYWORDS, LAST_DENT, LAST_DENTS, Lexer, MULTILINER, MULTI_DENT, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, REGEX_ESCAPE, REGEX_FLAGS, REGEX_INTERPOLATION, REGEX_START, RESERVED, Rewriter, STRING_NEWLINES, WHITESPACE;
// The CoffeeScript Lexer. Uses a series of token-matching regexes to attempt
// matches against the beginning of the source code. When a match is found,
// a token is produced, we consume the match, and start again. Tokens are in the
@ -9,9 +9,9 @@
// Set up the Lexer for both Node.js and the browser, depending on where we are.
if ((typeof process !== "undefined" && process !== null)) {
Rewriter = require('./rewriter').Rewriter;
process.mixin(require('./helpers'));
} else {
this.exports = this;
Rewriter = this.Rewriter;
}
// The Lexer Class
// ---------------
@ -203,7 +203,7 @@
Lexer.prototype.balanced_token = function balanced_token() {
var delimited;
delimited = Array.prototype.slice.call(arguments, 0);
return this.balanced_string(this.chunk, delimited);
return balanced_string(this.chunk, delimited);
};
// Matches and conumes comments. We pass through comments into JavaScript,
// so they're treated as real tokens, like any other part of the language.
@ -403,52 +403,6 @@
Lexer.prototype.assignment_error = function assignment_error() {
throw new Error("SyntaxError: Reserved word \"" + (this.value()) + "\" on line " + (this.line + 1) + " can't be assigned");
};
// Matches a balanced group such as a single or double-quoted string. Pass in
// a series of delimiters, all of which must be nested correctly within the
// contents of the string. This method allows us to have strings within
// interpolations within strings etc...
Lexer.prototype.balanced_string = function balanced_string(str, delimited) {
var _a, _b, _c, _d, close, i, levels, open, pair, slash;
slash = delimited[0][0] === '/';
levels = [];
i = 0;
while (i < str.length) {
if (levels.length && starts(str, '\\', i)) {
i += 1;
} else {
_a = delimited;
for (_b = 0, _c = _a.length; _b < _c; _b++) {
pair = _a[_b];
_d = pair;
open = _d[0];
close = _d[1];
if (levels.length && starts(str, close, i) && levels[levels.length - 1] === pair) {
levels.pop();
i += close.length - 1;
if (!(levels.length)) {
i += 1;
}
break;
} else if (starts(str, open, i)) {
levels.push(pair);
i += open.length - 1;
break;
}
}
}
if (!levels.length || slash && starts(str, '\n', i)) {
break;
}
i += 1;
}
if (levels.length) {
if (slash) {
return false;
}
throw new Error("SyntaxError: Unterminated " + (levels.pop()[0]) + " starting on line " + (this.line + 1));
}
return !i ? false : str.substring(0, i);
};
// Expand variables and expressions inside double-quoted strings using
// [ECMA Harmony's interpolation syntax](http://wiki.ecmascript.org/doku.php?id=strawman:string_interpolation)
// for substitution of bare variables as well as arbitrary expressions.
@ -484,7 +438,7 @@
tokens.push(['IDENTIFIER', interp]);
i += group.length - 1;
pi = i + 1;
} else if (((expr = this.balanced_string(str.substring(i), [['${', '}']])))) {
} else if (((expr = balanced_string(str.substring(i), [['${', '}']])))) {
if (pi < i) {
tokens.push(['STRING', '' + quote + (str.substring(pi, i)) + quote]);
}
@ -632,37 +586,4 @@
// occurs at the start of a line. We disambiguate these from trailing whens to
// avoid an ambiguity in the grammar.
BEFORE_WHEN = ['INDENT', 'OUTDENT', 'TERMINATOR'];
// Utility Functions
// -----------------
// Does a list include a value?
include = function include(list, value) {
return list.indexOf(value) >= 0;
};
// Peek at the beginning of a given string to see if it matches a sequence.
starts = function starts(string, literal, start) {
return string.substring(start, (start || 0) + literal.length) === literal;
};
// Trim out all falsy values from an array.
compact = function compact(array) {
var _a, _b, _c, _d, item;
_a = []; _b = array;
for (_c = 0, _d = _b.length; _c < _d; _c++) {
item = _b[_c];
if (item) {
_a.push(item);
}
}
return _a;
};
// Count the number of occurences of a character in a string.
count = function count(string, letter) {
var num, pos;
num = 0;
pos = string.indexOf(letter);
while (pos !== -1) {
num += 1;
pos = string.indexOf(letter, pos + 1);
}
return num;
};
})();

View File

@ -1,19 +1,24 @@
(function(){
var AccessorNode, ArrayNode, AssignNode, BaseNode, CallNode, ClassNode, ClosureNode, CodeNode, CommentNode, ExistenceNode, Expressions, ExtendsNode, ForNode, IDENTIFIER, IfNode, IndexNode, LiteralNode, ObjectNode, OpNode, ParentheticalNode, PushNode, RangeNode, ReturnNode, SliceNode, SplatNode, TAB, TRAILING_WHITESPACE, ThrowNode, TryNode, ValueNode, WhileNode, compact, del, flatten, literal, merge, statement;
var AccessorNode, ArrayNode, AssignNode, BaseNode, CallNode, ClassNode, ClosureNode, CodeNode, CommentNode, ExistenceNode, Expressions, ExtendsNode, ForNode, IDENTIFIER, IfNode, IndexNode, LiteralNode, ObjectNode, OpNode, ParentheticalNode, PushNode, RangeNode, ReturnNode, SliceNode, SplatNode, TAB, TRAILING_WHITESPACE, ThrowNode, TryNode, ValueNode, WhileNode, literal, statement;
var __extends = function(child, parent) {
var ctor = function(){ };
ctor.prototype = parent.prototype;
child.__superClass__ = parent.prototype;
child.prototype = new ctor();
child.prototype.constructor = child;
}, __hasProp = Object.prototype.hasOwnProperty;
};
// `nodes.coffee` contains all of the node classes for the syntax tree. Most
// nodes are created as the result of actions in the [grammar](grammar.html),
// but some are created by other nodes as a method of code generation. To convert
// the syntax tree into a string of JavaScript code, call `compile()` on the root.
// Set up for both **Node.js** and the browser, by
// including the [Scope](scope.html) class.
(typeof process !== "undefined" && process !== null) ? process.mixin(require('scope')) : (this.exports = this);
if ((typeof process !== "undefined" && process !== null)) {
process.mixin(require('scope'));
process.mixin(require('helpers'));
} else {
this.exports = this;
}
// Helper function that marks a node as a JavaScript *statement*, or as a
// *pure_statement*. Statements must be wrapped in a closure when used as an
// expression, and nodes tagged as *pure_statement* cannot be closure-wrapped
@ -1562,58 +1567,6 @@ idt += TAB
IDENTIFIER = /^[a-zA-Z\$_](\w|\$)*$/;
// Utility Functions
// -----------------
// Merge objects, returning a fresh copy with attributes from both sides.
// Used every time `compile` is called, to allow properties in the options hash
// to propagate down the tree without polluting other branches.
merge = function merge(options, overrides) {
var _a, _b, fresh, key, val;
fresh = {};
_a = options;
for (key in _a) { if (__hasProp.call(_a, key)) {
val = _a[key];
((fresh[key] = val));
}}
if (overrides) {
_b = overrides;
for (key in _b) { if (__hasProp.call(_b, key)) {
val = _b[key];
((fresh[key] = val));
}}
}
return fresh;
};
// Trim out all falsy values from an array.
compact = function compact(array) {
var _a, _b, _c, _d, item;
_a = []; _b = array;
for (_c = 0, _d = _b.length; _c < _d; _c++) {
item = _b[_c];
if (item) {
_a.push(item);
}
}
return _a;
};
// Return a completely flattened version of an array. Handy for getting a
// list of `children`.
flatten = function flatten(array) {
var _a, _b, _c, item, memo;
memo = [];
_a = array;
for (_b = 0, _c = _a.length; _b < _c; _b++) {
item = _a[_b];
item instanceof Array ? (memo = memo.concat(item)) : memo.push(item);
}
return memo;
};
// Delete a key from an object, returning the value. Useful when a node is
// looking for a particular method in an options hash.
del = function del(obj, key) {
var val;
val = obj[key];
delete obj[key];
return val;
};
// Handy helper for a generating LiteralNode.
literal = function literal(name) {
return new LiteralNode(name);

View File

@ -1,5 +1,5 @@
(function(){
var BALANCED_PAIRS, EXPRESSION_CLOSE, EXPRESSION_END, EXPRESSION_START, IMPLICIT_BLOCK, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, INVERSES, Rewriter, SINGLE_CLOSERS, SINGLE_LINERS, _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, include, pair;
var BALANCED_PAIRS, EXPRESSION_CLOSE, EXPRESSION_END, EXPRESSION_START, IMPLICIT_BLOCK, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, INVERSES, Rewriter, SINGLE_CLOSERS, SINGLE_LINERS, _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, pair;
var __hasProp = Object.prototype.hasOwnProperty;
// The CoffeeScript language has a decent amount of optional syntax,
// implicit syntax, and shorthand syntax. These things can greatly complicate a
@ -9,9 +9,7 @@
// add implicit indentation and parentheses, balance incorrect nestings, and
// generally clean things up.
// Set up exported variables for both Node.js and the browser.
if (!((typeof process !== "undefined" && process !== null))) {
this.exports = this;
}
(typeof process !== "undefined" && process !== null) ? process.mixin(require('./helpers')) : (this.exports = this);
// The **Rewriter** class is used by the [Lexer](lexer.html), directly against
// its internal array of tokens.
exports.Rewriter = (function() {
@ -374,10 +372,4 @@
// The grammar can't disambiguate them, so we insert the implicit indentation.
SINGLE_LINERS = ['ELSE', "->", "=>", 'TRY', 'FINALLY', 'THEN'];
SINGLE_CLOSERS = ['TERMINATOR', 'CATCH', 'FINALLY', 'ELSE', 'OUTDENT', 'LEADING_WHEN'];
// Utility Functions
// -----------------
// Does a list include a value?
include = function include(list, value) {
return list.indexOf(value) >= 0;
};
})();

77
src/helpers.coffee Normal file
View File

@ -0,0 +1,77 @@
# Set up exported variables for both Node.js and the browser.
this.exports: this unless process?
# Does a list include a value?
exports.include: (list, value) ->
list.indexOf(value) >= 0
# Peek at the beginning of a given string to see if it matches a sequence.
exports.starts: (string, literal, start) ->
string.substring(start, (start or 0) + literal.length) is literal
# Trim out all falsy values from an array.
exports.compact: (array) -> item for item in array when item
# Count the number of occurences of a character in a string.
exports.count: (string, letter) ->
num: 0
pos: string.indexOf(letter)
while pos isnt -1
num += 1
pos: string.indexOf(letter, pos + 1)
num
# Merge objects, returning a fresh copy with attributes from both sides.
# Used every time `compile` is called, to allow properties in the options hash
# to propagate down the tree without polluting other branches.
exports.merge: (options, overrides) ->
fresh: {}
(fresh[key]: val) for key, val of options
(fresh[key]: val) for key, val of overrides if overrides
fresh
# Return a completely flattened version of an array. Handy for getting a
# list of `children`.
exports.flatten: (array) ->
memo: []
for item in array
if item instanceof Array then memo: memo.concat(item) else memo.push(item)
memo
# Delete a key from an object, returning the value. Useful when a node is
# looking for a particular method in an options hash.
exports.del: (obj, key) ->
val: obj[key]
delete obj[key]
val
# Matches a balanced group such as a single or double-quoted string. Pass in
# a series of delimiters, all of which must be nested correctly within the
# contents of the string. This method allows us to have strings within
# interpolations within strings etc...
exports.balanced_string: (str, delimited, options) ->
options ||= {}
slash: delimited[0][0] is '/'
levels: []
i: 0
while i < str.length
if levels.length and exports.starts str, '\\', i
i += 1
else
for pair in delimited
[open, close]: pair
if levels.length and exports.starts(str, close, i) and levels[levels.length - 1] is pair
levels.pop()
i += close.length - 1
i += 1 unless levels.length
break
else if exports.starts str, open, i
levels.push(pair)
i += open.length - 1
break
break if not levels.length or slash and exports.starts str, '\n', i
i += 1
if levels.length
return false if slash
throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}"
if not i then false else str.substring(0, i)

View File

@ -10,9 +10,9 @@
# Set up the Lexer for both Node.js and the browser, depending on where we are.
if process?
Rewriter: require('./rewriter').Rewriter
process.mixin require './helpers'
else
this.exports: this
Rewriter: this.Rewriter
# The Lexer Class
# ---------------
@ -146,7 +146,7 @@ exports.Lexer: class Lexer
# Matches a token in which which the passed delimiter pairs must be correctly
# balanced (ie. strings, JS literals).
balanced_token: (delimited...) ->
@balanced_string @chunk, delimited
balanced_string @chunk, delimited
# Matches and conumes comments. We pass through comments into JavaScript,
# so they're treated as real tokens, like any other part of the language.
@ -304,36 +304,6 @@ exports.Lexer: class Lexer
assignment_error: ->
throw new Error "SyntaxError: Reserved word \"${@value()}\" on line ${@line + 1} can't be assigned"
# Matches a balanced group such as a single or double-quoted string. Pass in
# a series of delimiters, all of which must be nested correctly within the
# contents of the string. This method allows us to have strings within
# interpolations within strings etc...
balanced_string: (str, delimited) ->
slash: delimited[0][0] is '/'
levels: []
i: 0
while i < str.length
if levels.length and starts str, '\\', i
i += 1
else
for pair in delimited
[open, close]: pair
if levels.length and starts(str, close, i) and levels[levels.length - 1] is pair
levels.pop()
i += close.length - 1
i += 1 unless levels.length
break
else if starts str, open, i
levels.push(pair)
i += open.length - 1
break
break if not levels.length or slash and starts str, '\n', i
i += 1
if levels.length
return false if slash
throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}"
if not i then false else str.substring(0, i)
# Expand variables and expressions inside double-quoted strings using
# [ECMA Harmony's interpolation syntax](http://wiki.ecmascript.org/doku.php?id=strawman:string_interpolation)
# for substitution of bare variables as well as arbitrary expressions.
@ -362,7 +332,7 @@ exports.Lexer: class Lexer
tokens.push ['IDENTIFIER', interp]
i += group.length - 1
pi: i + 1
else if (expr: @balanced_string str.substring(i), [['${', '}']])
else if (expr: balanced_string str.substring(i), [['${', '}']])
tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i
inner: expr.substring(2, expr.length - 1)
if inner.length
@ -509,26 +479,3 @@ ACCESSORS: ['PROPERTY_ACCESS', 'PROTOTYPE_ACCESS', 'SOAK_ACCESS', '@']
# occurs at the start of a line. We disambiguate these from trailing whens to
# avoid an ambiguity in the grammar.
BEFORE_WHEN: ['INDENT', 'OUTDENT', 'TERMINATOR']
# Utility Functions
# -----------------
# Does a list include a value?
include: (list, value) ->
list.indexOf(value) >= 0
# Peek at the beginning of a given string to see if it matches a sequence.
starts: (string, literal, start) ->
string.substring(start, (start or 0) + literal.length) is literal
# Trim out all falsy values from an array.
compact: (array) -> item for item in array when item
# Count the number of occurences of a character in a string.
count: (string, letter) ->
num: 0
pos: string.indexOf(letter)
while pos isnt -1
num += 1
pos: string.indexOf(letter, pos + 1)
num

View File

@ -7,6 +7,7 @@
# including the [Scope](scope.html) class.
if process?
process.mixin require 'scope'
process.mixin require 'helpers'
else
this.exports: this
@ -1197,33 +1198,6 @@ IDENTIFIER: /^[a-zA-Z\$_](\w|\$)*$/
# Utility Functions
# -----------------
# Merge objects, returning a fresh copy with attributes from both sides.
# Used every time `compile` is called, to allow properties in the options hash
# to propagate down the tree without polluting other branches.
merge: (options, overrides) ->
fresh: {}
(fresh[key]: val) for key, val of options
(fresh[key]: val) for key, val of overrides if overrides
fresh
# Trim out all falsy values from an array.
compact: (array) -> item for item in array when item
# Return a completely flattened version of an array. Handy for getting a
# list of `children`.
flatten: (array) ->
memo: []
for item in array
if item instanceof Array then memo: memo.concat(item) else memo.push(item)
memo
# Delete a key from an object, returning the value. Useful when a node is
# looking for a particular method in an options hash.
del: (obj, key) ->
val: obj[key]
delete obj[key]
val
# Handy helper for a generating LiteralNode.
literal: (name) ->
new LiteralNode(name)

View File

@ -7,7 +7,10 @@
# generally clean things up.
# Set up exported variables for both Node.js and the browser.
this.exports: this unless process?
if process?
process.mixin require './helpers'
else
this.exports: this
# The **Rewriter** class is used by the [Lexer](lexer.html), directly against
# its internal array of tokens.
@ -253,10 +256,3 @@ IMPLICIT_END: ['IF', 'UNLESS', 'FOR', 'WHILE', 'TERMINATOR', 'INDENT', 'OUTDEN
# The grammar can't disambiguate them, so we insert the implicit indentation.
SINGLE_LINERS: ['ELSE', "->", "=>", 'TRY', 'FINALLY', 'THEN']
SINGLE_CLOSERS: ['TERMINATOR', 'CATCH', 'FINALLY', 'ELSE', 'OUTDENT', 'LEADING_WHEN']
# Utility Functions
# -----------------
# Does a list include a value?
include: (list, value) ->
list.indexOf(value) >= 0