coffee-script 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/coffee-script.gemspec +2 -2
- data/examples/blocks.coffee +57 -0
- data/extras/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage +6 -0
- data/lib/coffee-script.rb +1 -1
- data/lib/coffee_script/coffee-script.js +50 -0
- data/lib/coffee_script/command_line.rb +17 -11
- data/lib/coffee_script/grammar.y +11 -9
- data/lib/coffee_script/lexer.js +363 -0
- data/lib/coffee_script/lexer.rb +2 -4
- data/lib/coffee_script/narwhal/{lib/coffee-script.js → coffee-script.js} +21 -5
- data/lib/coffee_script/nodes.js +443 -0
- data/lib/coffee_script/nodes.rb +24 -9
- data/lib/coffee_script/parser.js +477 -0
- data/lib/coffee_script/parser.rb +1222 -1140
- data/lib/coffee_script/repl.js +33 -0
- data/lib/coffee_script/rewriter.js +377 -0
- data/lib/coffee_script/rewriter.rb +26 -26
- data/lib/coffee_script/runner.js +11 -0
- data/lib/coffee_script/scope.js +73 -0
- data/package.json +2 -3
- metadata +12 -6
- data/lib/coffee_script/narwhal/coffee-script.coffee +0 -62
- data/lib/coffee_script/narwhal/lib/coffee-script/loader.js +0 -21
- data/lib/coffee_script/narwhal/loader.coffee +0 -19
data/coffee-script.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'coffee-script'
|
3
|
-
s.version = '0.3.
|
4
|
-
s.date = '2010-
|
3
|
+
s.version = '0.3.2' # Keep version in sync with coffee-script.rb
|
4
|
+
s.date = '2010-2-8'
|
5
5
|
|
6
6
|
s.homepage = "http://jashkenas.github.com/coffee-script/"
|
7
7
|
s.summary = "The CoffeeScript Compiler"
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# After wycats' http://yehudakatz.com/2010/02/07/the-building-blocks-of-ruby/
|
2
|
+
|
3
|
+
# Sinatra.
|
4
|
+
get '/hello', ->
|
5
|
+
'Hello World'
|
6
|
+
|
7
|
+
|
8
|
+
# Append.
|
9
|
+
append: (location, data) ->
|
10
|
+
path: new Pathname location
|
11
|
+
throw "Location does not exist" unless path.exists()
|
12
|
+
|
13
|
+
File.open path, 'a', (file) ->
|
14
|
+
file.puts YAML.dump data
|
15
|
+
|
16
|
+
data
|
17
|
+
|
18
|
+
|
19
|
+
# Rubinius' File.open implementation.
|
20
|
+
File.open: (path, mode, block) ->
|
21
|
+
io: new File path, mode
|
22
|
+
|
23
|
+
return io unless block
|
24
|
+
|
25
|
+
try
|
26
|
+
block io
|
27
|
+
finally
|
28
|
+
try
|
29
|
+
io.close() unless io.closed()
|
30
|
+
catch error
|
31
|
+
# nothing, just swallow them.
|
32
|
+
|
33
|
+
|
34
|
+
# Write.
|
35
|
+
write: (location, data) ->
|
36
|
+
path = new Pathname location
|
37
|
+
raise "Location does not exist" unless path.exists()
|
38
|
+
|
39
|
+
File.open path, 'w', (file) ->
|
40
|
+
return false if Digest.MD5.hexdigest(file.read()) is data.hash()
|
41
|
+
file.puts YAML.dump data
|
42
|
+
true
|
43
|
+
|
44
|
+
|
45
|
+
# Rails' respond_to.
|
46
|
+
index: ->
|
47
|
+
people: Person.find 'all'
|
48
|
+
|
49
|
+
respond_to (format) ->
|
50
|
+
format.html()
|
51
|
+
format.xml -> render { xml: people.xml() }
|
52
|
+
|
53
|
+
|
54
|
+
# Synchronization.
|
55
|
+
synchronize: (block) ->
|
56
|
+
lock()
|
57
|
+
try block() finally unlock()
|
@@ -72,6 +72,12 @@
|
|
72
72
|
<key>name</key>
|
73
73
|
<string>constant.numeric.coffee</string>
|
74
74
|
</dict>
|
75
|
+
<dict>
|
76
|
+
<key>match</key>
|
77
|
+
<string>(@)[a-zA-Z_$]\w*</string>
|
78
|
+
<key>name</key>
|
79
|
+
<string>variable.other.readwrite.instance.coffee</string>
|
80
|
+
</dict>
|
75
81
|
<dict>
|
76
82
|
<key>name</key>
|
77
83
|
<string>string.quoted.heredoc.coffee</string>
|
data/lib/coffee-script.rb
CHANGED
@@ -10,7 +10,7 @@ require "coffee_script/parse_error"
|
|
10
10
|
# Namespace for all CoffeeScript internal classes.
|
11
11
|
module CoffeeScript
|
12
12
|
|
13
|
-
VERSION = '0.3.
|
13
|
+
VERSION = '0.3.2' # Keep in sync with the gemspec.
|
14
14
|
|
15
15
|
# Compile a script (String or IO) to JavaScript.
|
16
16
|
def self.compile(script, options={})
|
@@ -0,0 +1,50 @@
|
|
1
|
+
(function(){
|
2
|
+
var compiler, path;
|
3
|
+
// Executes the `coffee` Ruby program to convert from CoffeeScript to JavaScript.
|
4
|
+
path = require('path');
|
5
|
+
// The path to the CoffeeScript executable.
|
6
|
+
compiler = path.normalize(path.dirname(__filename) + '/../../bin/coffee');
|
7
|
+
// Compile a string over stdin, with global variables, for the REPL.
|
8
|
+
exports.compile = function compile(code, callback) {
|
9
|
+
var coffee, js;
|
10
|
+
js = '';
|
11
|
+
coffee = process.createChildProcess(compiler, ['--eval', '--no-wrap', '--globals']);
|
12
|
+
coffee.addListener('output', function(results) {
|
13
|
+
if ((typeof results !== "undefined" && results !== null)) {
|
14
|
+
return js += results;
|
15
|
+
}
|
16
|
+
});
|
17
|
+
coffee.addListener('exit', function() {
|
18
|
+
return callback(js);
|
19
|
+
});
|
20
|
+
coffee.write(code);
|
21
|
+
return coffee.close();
|
22
|
+
};
|
23
|
+
// Compile a list of CoffeeScript files on disk.
|
24
|
+
exports.compile_files = function compile_files(paths, callback) {
|
25
|
+
var coffee, exit_ran, js;
|
26
|
+
js = '';
|
27
|
+
coffee = process.createChildProcess(compiler, ['--print'].concat(paths));
|
28
|
+
coffee.addListener('output', function(results) {
|
29
|
+
if ((typeof results !== "undefined" && results !== null)) {
|
30
|
+
return js += results;
|
31
|
+
}
|
32
|
+
});
|
33
|
+
// NB: we have to add a mutex to make sure it doesn't get called twice.
|
34
|
+
exit_ran = false;
|
35
|
+
coffee.addListener('exit', function() {
|
36
|
+
if (exit_ran) {
|
37
|
+
return null;
|
38
|
+
}
|
39
|
+
exit_ran = true;
|
40
|
+
return callback(js);
|
41
|
+
});
|
42
|
+
return coffee.addListener('error', function(message) {
|
43
|
+
if (!(message)) {
|
44
|
+
return null;
|
45
|
+
}
|
46
|
+
puts(message);
|
47
|
+
throw new Error("CoffeeScript compile error");
|
48
|
+
});
|
49
|
+
};
|
50
|
+
})();
|
@@ -28,8 +28,11 @@ Usage:
|
|
28
28
|
# Path to the root of the CoffeeScript install.
|
29
29
|
ROOT = File.expand_path(File.dirname(__FILE__) + '/../..')
|
30
30
|
|
31
|
-
#
|
32
|
-
|
31
|
+
# Commands to execute CoffeeScripts.
|
32
|
+
RUNNERS = {
|
33
|
+
:node => "node #{ROOT}/lib/coffee_script/runner.js",
|
34
|
+
:narwhal => "narwhal -p #{ROOT} -e 'require(\"coffee-script\").run(system.args);'"
|
35
|
+
}
|
33
36
|
|
34
37
|
# Run the CommandLine off the contents of ARGV.
|
35
38
|
def initialize
|
@@ -114,20 +117,20 @@ Usage:
|
|
114
117
|
puts js
|
115
118
|
end
|
116
119
|
|
117
|
-
# Use Narwhal to run an interactive CoffeeScript session.
|
120
|
+
# Use Node.js or Narwhal to run an interactive CoffeeScript session.
|
118
121
|
def launch_repl
|
119
|
-
exec "#{
|
122
|
+
exec "#{RUNNERS[@options[:runner]]}"
|
120
123
|
rescue Errno::ENOENT
|
121
|
-
puts "Error:
|
124
|
+
puts "Error: #{@options[:runner]} must be installed to use the interactive REPL."
|
122
125
|
exit(1)
|
123
126
|
end
|
124
127
|
|
125
|
-
# Use Narwhal to compile and execute CoffeeScripts.
|
128
|
+
# Use Node.js or Narwhal to compile and execute CoffeeScripts.
|
126
129
|
def run_scripts
|
127
130
|
sources = @sources.join(' ')
|
128
|
-
exec "#{
|
131
|
+
exec "#{RUNNERS[@options[:runner]]} #{sources}"
|
129
132
|
rescue Errno::ENOENT
|
130
|
-
puts "Error:
|
133
|
+
puts "Error: #{@options[:runner]} must be installed in order to execute scripts."
|
131
134
|
exit(1)
|
132
135
|
end
|
133
136
|
|
@@ -166,12 +169,12 @@ Usage:
|
|
166
169
|
|
167
170
|
# Use OptionParser for all the options.
|
168
171
|
def parse_options
|
169
|
-
@options = {}
|
172
|
+
@options = {:runner => :node}
|
170
173
|
@option_parser = OptionParser.new do |opts|
|
171
|
-
opts.on('-i', '--interactive', 'run
|
174
|
+
opts.on('-i', '--interactive', 'run an interactive CoffeeScript REPL') do |i|
|
172
175
|
@options[:interactive] = true
|
173
176
|
end
|
174
|
-
opts.on('-r', '--run', 'compile and run a
|
177
|
+
opts.on('-r', '--run', 'compile and run a CoffeeScript') do |r|
|
175
178
|
@options[:run] = true
|
176
179
|
end
|
177
180
|
opts.on('-o', '--output [DIR]', 'set the directory for compiled JavaScript') do |d|
|
@@ -202,6 +205,9 @@ Usage:
|
|
202
205
|
opts.on('-g', '--globals', 'attach all top-level variable as globals') do |n|
|
203
206
|
@options[:globals] = true
|
204
207
|
end
|
208
|
+
opts.on_tail('--narwhal', 'use Narwhal instead of Node.js') do |n|
|
209
|
+
@options[:runner] = :narwhal
|
210
|
+
end
|
205
211
|
opts.on_tail('--install-bundle', 'install the CoffeeScript TextMate bundle') do |i|
|
206
212
|
install_bundle
|
207
213
|
exit
|
data/lib/coffee_script/grammar.y
CHANGED
@@ -13,7 +13,7 @@ token FOR IN OF BY WHEN WHILE
|
|
13
13
|
token SWITCH LEADING_WHEN
|
14
14
|
token DELETE INSTANCEOF TYPEOF
|
15
15
|
token SUPER EXTENDS
|
16
|
-
token
|
16
|
+
token ASSIGN RETURN
|
17
17
|
token NEWLINE
|
18
18
|
token COMMENT
|
19
19
|
token JS
|
@@ -21,24 +21,20 @@ token INDENT OUTDENT
|
|
21
21
|
|
22
22
|
# Declare order of operations.
|
23
23
|
prechigh
|
24
|
-
left '?'
|
25
24
|
nonassoc UMINUS UPLUS NOT '!' '!!' '~' '++' '--'
|
26
|
-
left '*' '/' '%'
|
25
|
+
left '*' '/' '%' '?' '.'
|
27
26
|
left '+' '-'
|
28
|
-
left '<<' '>>' '>>>'
|
29
|
-
left '&' '|' '^'
|
27
|
+
left '<<' '>>' '>>>' '&' '|' '^'
|
30
28
|
left '<=' '<' '>' '>='
|
31
29
|
right '==' '!=' IS ISNT
|
32
30
|
left '&&' '||' AND OR
|
33
|
-
right '-=' '+=' '/=' '*=' '%='
|
31
|
+
right '-=' '+=' '/=' '*=' '%=' '||=' '&&=' '?='
|
34
32
|
right DELETE INSTANCEOF TYPEOF
|
35
|
-
left '.'
|
36
33
|
right INDENT
|
37
34
|
left OUTDENT
|
38
35
|
right WHEN LEADING_WHEN IN OF BY
|
39
36
|
right THROW FOR NEW SUPER
|
40
37
|
left EXTENDS
|
41
|
-
left '||=' '&&=' '?='
|
42
38
|
right ASSIGN RETURN
|
43
39
|
right '->' '=>' UNLESS IF ELSE WHILE
|
44
40
|
preclow
|
@@ -102,7 +98,6 @@ rule
|
|
102
98
|
| REGEX { result = LiteralNode.new(val[0]) }
|
103
99
|
| BREAK { result = LiteralNode.new(val[0]) }
|
104
100
|
| CONTINUE { result = LiteralNode.new(val[0]) }
|
105
|
-
| ARGUMENTS { result = LiteralNode.new(val[0]) }
|
106
101
|
| TRUE { result = LiteralNode.new(Value.new(true)) }
|
107
102
|
| FALSE { result = LiteralNode.new(Value.new(false)) }
|
108
103
|
| YES { result = LiteralNode.new(Value.new(true)) }
|
@@ -238,6 +233,7 @@ rule
|
|
238
233
|
| Object { result = ValueNode.new(val[0]) }
|
239
234
|
| Parenthetical { result = ValueNode.new(val[0]) }
|
240
235
|
| Range { result = ValueNode.new(val[0]) }
|
236
|
+
| This { result = ValueNode.new(val[0]) }
|
241
237
|
| Value Accessor { result = val[0] << val[1] }
|
242
238
|
| Invocation Accessor { result = ValueNode.new(val[0], [val[1]]) }
|
243
239
|
;
|
@@ -300,6 +296,12 @@ rule
|
|
300
296
|
SUPER CALL_START ArgList CALL_END { result = CallNode.new(Value.new('super'), val[2]) }
|
301
297
|
;
|
302
298
|
|
299
|
+
# This references, either naked or to a property.
|
300
|
+
This:
|
301
|
+
'@' { result = ThisNode.new }
|
302
|
+
| '@' IDENTIFIER { result = ThisNode.new(val[1]) }
|
303
|
+
;
|
304
|
+
|
303
305
|
# The range literal.
|
304
306
|
Range:
|
305
307
|
"[" Expression
|
@@ -0,0 +1,363 @@
|
|
1
|
+
(function(){
|
2
|
+
var ASSIGNMENT, CALLABLE, CODE, COMMENT, COMMENT_CLEANER, HEREDOC, HEREDOC_INDENT, IDENTIFIER, JS, JS_CLEANER, KEYWORDS, LAST_DENT, LAST_DENTS, MULTILINER, MULTI_DENT, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, REGEX, Rewriter, STRING, STRING_NEWLINES, WHITESPACE, lex;
|
3
|
+
Rewriter = require('./rewriter').Rewriter;
|
4
|
+
// The lexer reads a stream of CoffeeScript and divvys it up into tagged
|
5
|
+
// tokens. A minor bit of the ambiguity in the grammar has been avoided by
|
6
|
+
// pushing some extra smarts into the Lexer.
|
7
|
+
exports.Lexer = (lex = function lex() { });
|
8
|
+
// Constants ============================================================
|
9
|
+
// The list of keywords passed verbatim to the parser.
|
10
|
+
KEYWORDS = ["if", "else", "then", "unless", "true", "false", "yes", "no", "on", "off", "and", "or", "is", "isnt", "not", "new", "return", "arguments", "try", "catch", "finally", "throw", "break", "continue", "for", "in", "of", "by", "where", "while", "delete", "instanceof", "typeof", "switch", "when", "super", "extends"];
|
11
|
+
// Token matching regexes.
|
12
|
+
IDENTIFIER = /^([a-zA-Z$_](\w|\$)*)/;
|
13
|
+
NUMBER = /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i;
|
14
|
+
STRING = /^(""|''|"([\s\S]*?)([^\\]|\\\\)"|'([\s\S]*?)([^\\]|\\\\)')/;
|
15
|
+
HEREDOC = /^("{6}|'{6}|"{3}\n?([\s\S]*?)\n?([ \t]*)"{3}|'{3}\n?([\s\S]*?)\n?([ \t]*)'{3})/;
|
16
|
+
JS = /^(``|`([\s\S]*?)([^\\]|\\\\)`)/;
|
17
|
+
OPERATOR = /^([+\*&|\/\-%=<>:!?]+)/;
|
18
|
+
WHITESPACE = /^([ \t]+)/;
|
19
|
+
COMMENT = /^(((\n?[ \t]*)?#.*$)+)/;
|
20
|
+
CODE = /^((-|=)>)/;
|
21
|
+
REGEX = /^(\/(.*?)([^\\]|\\\\)\/[imgy]{0,4})/;
|
22
|
+
MULTI_DENT = /^((\n([ \t]*))+)(\.)?/;
|
23
|
+
LAST_DENTS = /\n([ \t]*)/g;
|
24
|
+
LAST_DENT = /\n([ \t]*)/;
|
25
|
+
ASSIGNMENT = /^(:|=)$/;
|
26
|
+
// Token cleaning regexes.
|
27
|
+
JS_CLEANER = /(^`|`$)/g;
|
28
|
+
MULTILINER = /\n/g;
|
29
|
+
STRING_NEWLINES = /\n[ \t]*/g;
|
30
|
+
COMMENT_CLEANER = /(^[ \t]*#|\n[ \t]*$)/mg;
|
31
|
+
NO_NEWLINE = /^([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)$/;
|
32
|
+
HEREDOC_INDENT = /^[ \t]+/g;
|
33
|
+
// Tokens which a regular expression will never immediately follow, but which
|
34
|
+
// a division operator might.
|
35
|
+
// See: http://www.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions
|
36
|
+
NOT_REGEX = ['IDENTIFIER', 'NUMBER', 'REGEX', 'STRING', ')', '++', '--', ']', '}', 'FALSE', 'NULL', 'TRUE'];
|
37
|
+
// Tokens which could legitimately be invoked or indexed.
|
38
|
+
CALLABLE = ['IDENTIFIER', 'SUPER', ')', ']', '}', 'STRING'];
|
39
|
+
// Scan by attempting to match tokens one character at a time. Slow and steady.
|
40
|
+
lex.prototype.tokenize = function tokenize(code) {
|
41
|
+
this.code = code;
|
42
|
+
// Cleanup code by remove extra line breaks, TODO: chomp
|
43
|
+
this.i = 0;
|
44
|
+
// Current character position we're parsing
|
45
|
+
this.line = 1;
|
46
|
+
// The current line.
|
47
|
+
this.indent = 0;
|
48
|
+
// The current indent level.
|
49
|
+
this.indents = [];
|
50
|
+
// The stack of all indent levels we are currently within.
|
51
|
+
this.tokens = [];
|
52
|
+
// Collection of all parsed tokens in the form [:TOKEN_TYPE, value]
|
53
|
+
this.spaced = null;
|
54
|
+
// The last token that has a space following it.
|
55
|
+
while (this.i < this.code.length) {
|
56
|
+
this.chunk = this.code.slice(this.i);
|
57
|
+
this.extract_next_token();
|
58
|
+
}
|
59
|
+
this.close_indentation();
|
60
|
+
return (new Rewriter()).rewrite(this.tokens);
|
61
|
+
};
|
62
|
+
// At every position, run through this list of attempted matches,
|
63
|
+
// short-circuiting if any of them succeed.
|
64
|
+
lex.prototype.extract_next_token = function extract_next_token() {
|
65
|
+
if (this.identifier_token()) {
|
66
|
+
return null;
|
67
|
+
}
|
68
|
+
if (this.number_token()) {
|
69
|
+
return null;
|
70
|
+
}
|
71
|
+
if (this.heredoc_token()) {
|
72
|
+
return null;
|
73
|
+
}
|
74
|
+
if (this.string_token()) {
|
75
|
+
return null;
|
76
|
+
}
|
77
|
+
if (this.js_token()) {
|
78
|
+
return null;
|
79
|
+
}
|
80
|
+
if (this.regex_token()) {
|
81
|
+
return null;
|
82
|
+
}
|
83
|
+
if (this.indent_token()) {
|
84
|
+
return null;
|
85
|
+
}
|
86
|
+
if (this.comment_token()) {
|
87
|
+
return null;
|
88
|
+
}
|
89
|
+
if (this.whitespace_token()) {
|
90
|
+
return null;
|
91
|
+
}
|
92
|
+
return this.literal_token();
|
93
|
+
};
|
94
|
+
// Tokenizers ==========================================================
|
95
|
+
// Matches identifying literals: variables, keywords, method names, etc.
|
96
|
+
lex.prototype.identifier_token = function identifier_token() {
|
97
|
+
var id, tag;
|
98
|
+
if (!((id = this.match(IDENTIFIER, 1)))) {
|
99
|
+
return false;
|
100
|
+
}
|
101
|
+
// Keywords are special identifiers tagged with their own name,
|
102
|
+
// 'if' will result in an ['IF', "if"] token.
|
103
|
+
tag = KEYWORDS.indexOf(id) >= 0 ? id.toUpperCase() : 'IDENTIFIER';
|
104
|
+
if (tag === 'WHEN' && (this.tag() === 'OUTDENT' || this.tag() === 'INDENT')) {
|
105
|
+
tag = 'LEADING_WHEN';
|
106
|
+
}
|
107
|
+
if (tag === 'IDENTIFIER' && this.value() === '::') {
|
108
|
+
this.tag(-1, 'PROTOTYPE_ACCESS');
|
109
|
+
}
|
110
|
+
if (tag === 'IDENTIFIER' && this.value() === '.' && !(this.value(-2) === '.')) {
|
111
|
+
if (this.tag(-2) === '?') {
|
112
|
+
this.tag(-1, 'SOAK_ACCESS');
|
113
|
+
this.tokens.splice(-2, 1);
|
114
|
+
} else {
|
115
|
+
this.tag(-1, 'PROPERTY_ACCESS');
|
116
|
+
}
|
117
|
+
}
|
118
|
+
this.token(tag, id);
|
119
|
+
this.i += id.length;
|
120
|
+
return true;
|
121
|
+
};
|
122
|
+
// Matches numbers, including decimals, hex, and exponential notation.
|
123
|
+
lex.prototype.number_token = function number_token() {
|
124
|
+
var number;
|
125
|
+
if (!((number = this.match(NUMBER, 1)))) {
|
126
|
+
return false;
|
127
|
+
}
|
128
|
+
this.token('NUMBER', number);
|
129
|
+
this.i += number.length;
|
130
|
+
return true;
|
131
|
+
};
|
132
|
+
// Matches strings, including multi-line strings.
|
133
|
+
lex.prototype.string_token = function string_token() {
|
134
|
+
var escaped, string;
|
135
|
+
if (!((string = this.match(STRING, 1)))) {
|
136
|
+
return false;
|
137
|
+
}
|
138
|
+
escaped = string.replace(STRING_NEWLINES, " \\\n");
|
139
|
+
this.token('STRING', escaped);
|
140
|
+
this.line += this.count(string, "\n");
|
141
|
+
this.i += string.length;
|
142
|
+
return true;
|
143
|
+
};
|
144
|
+
// Matches heredocs, adjusting indentation to the correct level.
|
145
|
+
lex.prototype.heredoc_token = function heredoc_token() {
|
146
|
+
var doc, indent, match;
|
147
|
+
if (!((match = this.chunk.match(HEREDOC)))) {
|
148
|
+
return false;
|
149
|
+
}
|
150
|
+
doc = match[2] || match[4];
|
151
|
+
indent = doc.match(HEREDOC_INDENT).sort()[0];
|
152
|
+
doc = doc.replace(new RegExp("^" + indent, 'g'), '').replace(MULTILINER, "\\n").replace('"', '\\"');
|
153
|
+
this.token('STRING', '"' + doc + '"');
|
154
|
+
this.line += this.count(match[1], "\n");
|
155
|
+
this.i += match[1].length;
|
156
|
+
return true;
|
157
|
+
};
|
158
|
+
// Matches interpolated JavaScript.
|
159
|
+
lex.prototype.js_token = function js_token() {
|
160
|
+
var script;
|
161
|
+
if (!((script = this.match(JS, 1)))) {
|
162
|
+
return false;
|
163
|
+
}
|
164
|
+
this.token('JS', script.replace(JS_CLEANER, ''));
|
165
|
+
this.i += script.length;
|
166
|
+
return true;
|
167
|
+
};
|
168
|
+
// Matches regular expression literals.
|
169
|
+
lex.prototype.regex_token = function regex_token() {
|
170
|
+
var regex;
|
171
|
+
if (!((regex = this.match(REGEX, 1)))) {
|
172
|
+
return false;
|
173
|
+
}
|
174
|
+
if (NOT_REGEX.indexOf(this.tag()) >= 0) {
|
175
|
+
return false;
|
176
|
+
}
|
177
|
+
this.token('REGEX', regex);
|
178
|
+
this.i += regex.length;
|
179
|
+
return true;
|
180
|
+
};
|
181
|
+
// Matches and conumes comments.
|
182
|
+
lex.prototype.comment_token = function comment_token() {
|
183
|
+
var comment;
|
184
|
+
if (!((comment = this.match(COMMENT, 1)))) {
|
185
|
+
return false;
|
186
|
+
}
|
187
|
+
this.line += comment.match(MULTILINER).length;
|
188
|
+
this.token('COMMENT', comment.replace(COMMENT_CLEANER, '').split(MULTILINER));
|
189
|
+
this.token('TERMINATOR', "\n");
|
190
|
+
this.i += comment.length;
|
191
|
+
return true;
|
192
|
+
};
|
193
|
+
// Record tokens for indentation differing from the previous line.
|
194
|
+
lex.prototype.indent_token = function indent_token() {
|
195
|
+
var diff, indent, next_character, no_newlines, size;
|
196
|
+
if (!((indent = this.match(MULTI_DENT, 1)))) {
|
197
|
+
return false;
|
198
|
+
}
|
199
|
+
this.line += indent.match(MULTILINER).length;
|
200
|
+
this.i += indent.length;
|
201
|
+
next_character = this.chunk.match(MULTI_DENT)[4];
|
202
|
+
no_newlines = next_character === '.' || (this.value().match(NO_NEWLINE) && this.tokens[this.tokens.length - 2][0] !== '.' && !this.value().match(CODE));
|
203
|
+
if (no_newlines) {
|
204
|
+
return this.suppress_newlines(indent);
|
205
|
+
}
|
206
|
+
size = indent.match(LAST_DENTS).reverse()[0].match(LAST_DENT)[1].length;
|
207
|
+
if (size === this.indent) {
|
208
|
+
return this.newline_token(indent);
|
209
|
+
}
|
210
|
+
if (size > this.indent) {
|
211
|
+
diff = size - this.indent;
|
212
|
+
this.token('INDENT', diff);
|
213
|
+
this.indents.push(diff);
|
214
|
+
} else {
|
215
|
+
this.outdent_token(this.indent - size);
|
216
|
+
}
|
217
|
+
this.indent = size;
|
218
|
+
return true;
|
219
|
+
};
|
220
|
+
// Record an oudent token or tokens, if we're moving back inwards past
|
221
|
+
// multiple recorded indents.
|
222
|
+
lex.prototype.outdent_token = function outdent_token(move_out) {
|
223
|
+
var last_indent;
|
224
|
+
while (move_out > 0 && this.indents.length) {
|
225
|
+
last_indent = this.indents.pop();
|
226
|
+
this.token('OUTDENT', last_indent);
|
227
|
+
move_out -= last_indent;
|
228
|
+
}
|
229
|
+
this.token('TERMINATOR', "\n");
|
230
|
+
return true;
|
231
|
+
};
|
232
|
+
// Matches and consumes non-meaningful whitespace.
|
233
|
+
lex.prototype.whitespace_token = function whitespace_token() {
|
234
|
+
var space;
|
235
|
+
if (!((space = this.match(WHITESPACE, 1)))) {
|
236
|
+
return false;
|
237
|
+
}
|
238
|
+
this.spaced = this.value();
|
239
|
+
this.i += space.length;
|
240
|
+
return true;
|
241
|
+
};
|
242
|
+
// Multiple newlines get merged together.
|
243
|
+
// Use a trailing \ to escape newlines.
|
244
|
+
lex.prototype.newline_token = function newline_token(newlines) {
|
245
|
+
if (!(this.value() === "\n")) {
|
246
|
+
this.token('TERMINATOR', "\n");
|
247
|
+
}
|
248
|
+
return true;
|
249
|
+
};
|
250
|
+
// Tokens to explicitly escape newlines are removed once their job is done.
|
251
|
+
lex.prototype.suppress_newlines = function suppress_newlines(newlines) {
|
252
|
+
if (this.value() === "\\") {
|
253
|
+
this.tokens.pop();
|
254
|
+
}
|
255
|
+
return true;
|
256
|
+
};
|
257
|
+
// We treat all other single characters as a token. Eg.: ( ) , . !
|
258
|
+
// Multi-character operators are also literal tokens, so that Racc can assign
|
259
|
+
// the proper order of operations.
|
260
|
+
lex.prototype.literal_token = function literal_token() {
|
261
|
+
var match, tag, value;
|
262
|
+
match = this.chunk.match(OPERATOR);
|
263
|
+
value = match && match[1];
|
264
|
+
if (value && value.match(CODE)) {
|
265
|
+
this.tag_parameters();
|
266
|
+
}
|
267
|
+
value = value || this.chunk.substr(0, 1);
|
268
|
+
tag = value.match(ASSIGNMENT) ? 'ASSIGN' : value;
|
269
|
+
if (value === ';') {
|
270
|
+
tag = 'TERMINATOR';
|
271
|
+
}
|
272
|
+
if (this.value() !== this.spaced && CALLABLE.indexOf(this.tag()) >= 0) {
|
273
|
+
if (value === '(') {
|
274
|
+
tag = 'CALL_START';
|
275
|
+
}
|
276
|
+
if (value === '[') {
|
277
|
+
tag = 'INDEX_START';
|
278
|
+
}
|
279
|
+
}
|
280
|
+
this.token(tag, value);
|
281
|
+
this.i += value.length;
|
282
|
+
return true;
|
283
|
+
};
|
284
|
+
// Helpers =============================================================
|
285
|
+
// Add a token to the results, taking note of the line number.
|
286
|
+
lex.prototype.token = function token(tag, value) {
|
287
|
+
return this.tokens.push([tag, value]);
|
288
|
+
// this.tokens.push([tag, Value.new(value, @line)])
|
289
|
+
};
|
290
|
+
// Look at a tag in the current token stream.
|
291
|
+
lex.prototype.tag = function tag(index, tag) {
|
292
|
+
var tok;
|
293
|
+
if (!((tok = this.tokens[this.tokens.length - (index || 1)]))) {
|
294
|
+
return null;
|
295
|
+
}
|
296
|
+
if ((typeof tag !== "undefined" && tag !== null)) {
|
297
|
+
return (tok[0] = tag);
|
298
|
+
}
|
299
|
+
return tok[0];
|
300
|
+
};
|
301
|
+
// Look at a value in the current token stream.
|
302
|
+
lex.prototype.value = function value(index, val) {
|
303
|
+
var tok;
|
304
|
+
if (!((tok = this.tokens[this.tokens.length - (index || 1)]))) {
|
305
|
+
return null;
|
306
|
+
}
|
307
|
+
if ((typeof val !== "undefined" && val !== null)) {
|
308
|
+
return (tok[1] = val);
|
309
|
+
}
|
310
|
+
return tok[1];
|
311
|
+
};
|
312
|
+
// Count the occurences of a character in a string.
|
313
|
+
lex.prototype.count = function count(string, letter) {
|
314
|
+
var num, pos;
|
315
|
+
num = 0;
|
316
|
+
pos = string.indexOf(letter);
|
317
|
+
while (pos !== -1) {
|
318
|
+
count += 1;
|
319
|
+
pos = string.indexOf(letter, pos + 1);
|
320
|
+
}
|
321
|
+
return count;
|
322
|
+
};
|
323
|
+
// Attempt to match a string against the current chunk, returning the indexed
|
324
|
+
// match.
|
325
|
+
lex.prototype.match = function match(regex, index) {
|
326
|
+
var m;
|
327
|
+
if (!((m = this.chunk.match(regex)))) {
|
328
|
+
return false;
|
329
|
+
}
|
330
|
+
return m ? m[index] : false;
|
331
|
+
};
|
332
|
+
// A source of ambiguity in our grammar was parameter lists in function
|
333
|
+
// definitions (as opposed to argument lists in function calls). Tag
|
334
|
+
// parameter identifiers in order to avoid this. Also, parameter lists can
|
335
|
+
// make use of splats.
|
336
|
+
lex.prototype.tag_parameters = function tag_parameters() {
|
337
|
+
var i, tok;
|
338
|
+
if (this.tag() !== ')') {
|
339
|
+
return null;
|
340
|
+
}
|
341
|
+
i = 0;
|
342
|
+
while (true) {
|
343
|
+
i += 1;
|
344
|
+
tok = this.tokens[this.tokens.length - i];
|
345
|
+
if (!tok) {
|
346
|
+
return null;
|
347
|
+
}
|
348
|
+
if (tok[0] === 'IDENTIFIER') {
|
349
|
+
tok[0] = 'PARAM';
|
350
|
+
} else if (tok[0] === ')') {
|
351
|
+
tok[0] = 'PARAM_END';
|
352
|
+
} else if (tok[0] === '(') {
|
353
|
+
return (tok[0] = 'PARAM_START');
|
354
|
+
}
|
355
|
+
}
|
356
|
+
return true;
|
357
|
+
};
|
358
|
+
// Close up all remaining open blocks. IF the first token is an indent,
|
359
|
+
// axe it.
|
360
|
+
lex.prototype.close_indentation = function close_indentation() {
|
361
|
+
return this.outdent_token(this.indent);
|
362
|
+
};
|
363
|
+
})();
|