From 590c069158867db9b4a3e2e6c70c92e93ab0ab7a Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas text/coffeescript tags.
The current CoffeeScript version number.
exports.VERSION: '0.5.5'Instantiate a Lexer for our use here.
lexer: new Lexer()Compile a string of CoffeeScript code to JavaScript, using the Coffee/Jison + helpers: this.helpers
The current CoffeeScript version number.
exports.VERSION: '0.5.6'Instantiate a Lexer for our use here.
lexer: new Lexer()Compile a string of CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
exports.compile: compile: (code, options) ->
- options ||= {}
+ options: or {}
try
(parser.parse lexer.tokenize code).compile options
catch err
@@ -28,10 +28,11 @@ compiler. .traverse() with a callback. exports.nodes: (code) ->
parser.parse lexer.tokenize codeCompile and execute a string of CoffeeScript (on the server), correctly
-setting __filename, __dirname, and relative require().
exports.run: (code, options) ->
+setting __filename, __dirname, and relative require(). exports.run: ((code, options) ->
module.filename: __filename: options.source
__dirname: path.dirname __filename
- eval exports.compile code, optionsExtend CoffeeScript with a custom language extension. It should hook in to + eval exports.compile code, options +)
Extend CoffeeScript with a custom language extension. It should hook in to the Lexer (as a peer of any of the lexer's tokenizing methods), and push a token on to the stack that contains a Node as the value (as a peer of the nodes in nodes.coffee).
exports.extend: (func) ->
@@ -40,7 +41,7 @@ thin wrapper around it, compatible with the Jison API. We can then pass it
directly as a "Jison lexer". parser.lexer: {
lex: ->
token: @tokens[@pos] or [""]
- @pos += 1
+ @pos: + 1
this.yylineno: token[2]
this.yytext: token[1]
token[0]
diff --git a/documentation/docs/command.html b/documentation/docs/command.html
index 0fa50397..5e4ea7c7 100644
--- a/documentation/docs/command.html
+++ b/documentation/docs/command.html
@@ -70,7 +70,7 @@ and write them back to stdout. Watch a list of source CoffeeScript files using fs.watchFile, recompiling
them every time the files are updated. May be used in combination with other
diff --git a/documentation/docs/grammar.html b/documentation/docs/grammar.html
index eed142f7..f7538f49 100644
--- a/documentation/docs/grammar.html
+++ b/documentation/docs/grammar.html
@@ -48,6 +48,7 @@ CoffeeScript is the Expression -- you'll notice that there is n
of many other rules, making them somewhat circular.
Expression: [
o "Value"
o "Call"
+ o "Curry"
o "Code"
o "Operation"
o "Assign"
@@ -169,6 +170,10 @@ and calling super() Extending an object by setting its prototype chain to reference a parent object.
Extends: [
o "Value EXTENDS Value", -> new ExtendsNode $1, $3
@@ -284,7 +289,6 @@ rules are necessary. 2 + 3 * 4 parse as:
(2 + 3) * 4
operators: [
["left", '?']
- ["nonassoc", 'UMINUS', 'UPLUS', 'NOT', '!', '!!', '~', '++', '--']
+ ["nonassoc", 'UMINUS', 'UPLUS', '!', '!!', '~', '++', '--']
["left", '*', '/', '%']
["left", '+', '-']
["left", '<<', '>>', '>>>']
["left", '&', '|', '^']
["left", '<=', '<', '>', '>=']
["right", 'DELETE', 'INSTANCEOF', 'TYPEOF']
- ["left", '==', '!=', 'IS', 'ISNT']
- ["left", '&&', '||', 'AND', 'OR']
+ ["left", '==', '!=']
+ ["left", '&&', '||']
["right", '-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=']
["left", '.']
["right", 'INDENT']
@@ -364,7 +364,7 @@ down. Following these rules is what makes 2 + 3 * 4 parse as:
["right", 'FOR', 'NEW', 'SUPER', 'CLASS']
["left", 'EXTENDS']
["right", 'ASSIGN', 'RETURN']
- ["right", '->', '=>', 'UNLESS', 'IF', 'ELSE', 'WHILE']
+ ["right", '->', '=>', '<-', 'UNLESS', 'IF', 'ELSE', 'WHILE']
]Finally, now what we have our grammar and our operators, we can create our Jison.Parser. We do this by processing all of our rules, recording all terminals (every symbol which does not appear as the name of a rule above) diff --git a/documentation/docs/helpers.html b/documentation/docs/helpers.html index 474157e1..f02682fa 100644 --- a/documentation/docs/helpers.html +++ b/documentation/docs/helpers.html @@ -7,7 +7,7 @@ arrays, count characters, that sort of thing.
Merge objects, returning a fresh copy with attributes from both sides.
Used every time BaseNode#compile is called, to allow properties in the
@@ -30,27 +30,27 @@ looking for a particular method in an options hash.
helpers.balanced_string: balanced_string: (str, delimited, options) ->
- options ||= {}
+ options: or {}
slash: delimited[0][0] is '/'
levels: []
i: 0
while i < str.length
if levels.length and starts str, '\\', i
- i += 1
+ 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
+ i: + close.length - 1
+ i: + 1 unless levels.length
break
else if starts str, open, i
levels.push(pair)
- i += open.length - 1
+ i: + open.length - 1
break
break if not levels.length or slash and starts str, '\n', i
- i += 1
+ i: + 1
if levels.length
return false if slash
throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}"
diff --git a/documentation/docs/lexer.html b/documentation/docs/lexer.html
index 539fce29..ab619d5b 100644
--- a/documentation/docs/lexer.html
+++ b/documentation/docs/lexer.html
@@ -68,17 +68,20 @@ referenced as property names here, so you can still do jQuery.is()
though is means === otherwise. identifier_token: ->
return false unless id: @match IDENTIFIER, 1
@name_access_type()
+ accessed: include ACCESSORS, @tag(0)
tag: 'IDENTIFIER'
- tag: id.toUpperCase() if include(KEYWORDS, id) and
- not (include(ACCESSORS, @tag(0)) and not @prev().spaced)
- @identifier_error id if include RESERVED, id
- tag: 'LEADING_WHEN' if tag is 'WHEN' and include BEFORE_WHEN, @tag()
+ tag: id.toUpperCase() if not accessed and include(KEYWORDS, id)
+ @identifier_error id if include RESERVED, id
+ tag: 'LEADING_WHEN' if tag is 'WHEN' and include LINE_BREAK, @tag()
+ @i: + id.length
+ if not accessed
+ tag: id: CONVERSIONS[id] if include COFFEE_ALIASES, id
+ return @tag_half_assignment(tag) if @prev() and @prev()[0] is 'ASSIGN' and include HALF_ASSIGNMENTS, tag
@token(tag, id)
- @i += id.length
trueMatches numbers, including decimals, hex, and exponential notation.
number_token: ->
return false unless number: @match NUMBER, 1
@token 'NUMBER', number
- @i += number.length
+ @i: + number.length
trueMatches strings, including multi-line strings. Ensures that quotation marks are balanced within the string's contents, and within nested interpolations.
string_token: ->
return false unless starts(@chunk, '"') or starts(@chunk, "'")
@@ -86,20 +89,21 @@ are balanced within the string's contents, and within nested interpolations.
@balanced_token(['"', '"'], ['${', '}']) or
@balanced_token ["'", "'"]
@interpolate_string string.replace(STRING_NEWLINES, " \\\n")
- @line += count string, "\n"
- @i += string.length
+ @line: + count string, "\n"
+ @i: + string.length
trueMatches heredocs, adjusting indentation to the correct level, as heredocs preserve whitespace, but ignore indentation to the left.
heredoc_token: ->
- return false unless match = @chunk.match(HEREDOC)
- doc: @sanitize_heredoc match[2] or match[4]
- @token 'STRING', "\"$doc\""
- @line += count match[1], "\n"
- @i += match[1].length
+ return false unless match: @chunk.match(HEREDOC)
+ quote: match[1].substr(0, 1)
+ doc: @sanitize_heredoc match[2] or match[4], quote
+ @interpolate_string "$quote$doc$quote"
+ @line: + count match[1], "\n"
+ @i: + match[1].length
trueMatches JavaScript interpolated directly into the source via backticks.
js_token: ->
return false unless starts @chunk, '`'
return false unless script: @balanced_token ['`', '`']
@token 'JS', script.replace(JS_CLEANER, '')
- @i += script.length
+ @i: + script.length
trueMatches regular expression literals. Lexing regular expressions is difficult
to distinguish from division, so we borrow some basic heuristics from
JavaScript and Ruby, borrow slash balancing from @balanced_token, and
@@ -107,26 +111,28 @@ borrow interpolation from @interpolate_string.
Matches a token in which which the passed delimiter pairs must be correctly balanced (ie. strings, JS literals).
balanced_token: (delimited...) ->
balanced_string @chunk, delimitedMatches and conumes comments. We pass through comments into JavaScript, so they're treated as real tokens, like any other part of the language.
comment_token: ->
return false unless comment: @match COMMENT, 1
- @line += (comment.match(MULTILINER) or []).length
- lines: comment.replace(COMMENT_CLEANER, '').split(MULTILINER)
- @token 'COMMENT', compact lines
- @token 'TERMINATOR', "\n"
- @i += comment.length
+ @line: + (comment.match(MULTILINER) or []).length
+ lines: compact comment.replace(COMMENT_CLEANER, '').split(MULTILINER)
+ i: @tokens.length - 1
+ if @unfinished()
+ i: - 1 while @tokens[i] and not include LINE_BREAK, @tokens[i][0]
+ @tokens.splice(i + 1, 0, ['COMMENT', lines, @line], ['TERMINATOR', '\n', @line])
+ @i: + comment.length
trueMatches newlines, indents, and outdents, and determines which is which. If we can detect that the current line is continued onto the the next line, then the newline is suppressed:
@@ -139,13 +145,12 @@ then the newline is suppressed:Keeps track of the level of indentation, because a single outdent token can close multiple indents, so we need to know how far in we happen to be.
line_token: ->
return false unless indent: @match MULTI_DENT, 1
- @line += indent.match(MULTILINER).length
- @i += indent.length
+ @line: + indent.match(MULTILINER).length
+ @i : + indent.length
prev: @prev(2)
size: indent.match(LAST_DENTS).reverse()[0].match(LAST_DENT)[1].length
next_character: @chunk.match(MULTI_DENT)[4]
- no_newlines: next_character is '.' or (@value() and @value().match(NO_NEWLINE) and
- prev and (prev[0] isnt '.') and not @value().match(CODE))
+ no_newlines: next_character is '.' or @unfinished()
if size is @indent
return @suppress_newlines() if no_newlines
return @newline_token(indent)
@@ -162,14 +167,14 @@ inwards past several recorded indents. Matches and consumes non-meaningful whitespace. Tag the previous token as being "spaced", because there are some cases where it makes a difference.
whitespace_token: ->
return false unless space: @match WHITESPACE, 1
prev: @prev()
prev.spaced: true if prev
- @i += space.length
+ @i: + space.length
trueGenerate a newline token. Consecutive newlines get merged together.
newline_token: (newlines) ->
@token 'TERMINATOR', "\n" unless @tag() is 'TERMINATOR'
trueUse a \ at a line-ending to suppress the newline.
@@ -182,27 +187,29 @@ here. ; and newlines are both treated as a TERMINATOR,
parentheses that indicate a method call from regular parentheses, and so on.
literal_token: ->
match: @chunk.match(OPERATOR)
value: match and match[1]
+ space: match and match[2]
@tag_parameters() if value and value.match(CODE)
- value ||= @chunk.substr(0, 1)
- not_spaced: not @prev() or not @prev().spaced
+ value: or @chunk.substr(0, 1)
+ prev_spaced: @prev() and @prev().spaced
tag: value
if value.match(ASSIGNMENT)
tag: 'ASSIGN'
@assignment_error() if include JS_FORBIDDEN, @value
else if value is ';'
tag: 'TERMINATOR'
- else if value is '[' and @tag() is '?' and not_spaced
+ else if value is '[' and @tag() is '?' and not prev_spaced
tag: 'SOAKED_INDEX_START'
@soaked_index: true
@tokens.pop()
else if value is ']' and @soaked_index
tag: 'SOAKED_INDEX_END'
@soaked_index: false
- else if include(CALLABLE, @tag()) and not_spaced
+ else if include(CALLABLE, @tag()) and not prev_spaced
tag: 'CALL_START' if value is '('
tag: 'INDEX_START' if value is '['
+ @i: + value.length
+ return @tag_half_assignment(tag) if space and prev_spaced and @prev()[0] is 'ASSIGN' and include HALF_ASSIGNMENTS, tag
@token tag, value
- @i += value.length
trueAs we consume a new IDENTIFIER, look at the previous token to determine
if it's a special kind of accessor.
name_access_type: ->
@tag(1, 'PROTOTYPE_ACCESS') if @value() is '::'
@@ -212,29 +219,32 @@ if it's a special kind of accessor. Sanitize a heredoc by escaping internal double quotes and erasing all -external indentation on the left-hand side.
sanitize_heredoc: (doc) ->
+external indentation on the left-hand side. sanitize_heredoc: (doc, quote) ->
indent: (doc.match(HEREDOC_INDENT) or ['']).sort()[0]
doc.replace(new RegExp("^" +indent, 'gm'), '')
.replace(MULTILINER, "\\n")
- .replace(/"/g, '\\"')A source of ambiguity in our grammar used to be parameter lists in function + .replace(new RegExp(quote, 'g'), '\\"')
Tag a half assignment.
tag_half_assignment: (tag) ->
+ last: @tokens.pop()
+ @tokens.push ["$tag=", "$tag=", last[2]]
+ trueA source of ambiguity in our grammar used to be parameter lists in function definitions versus argument lists in function calls. Walk backwards, tagging parameters specially in order to make things easier for the parser.
tag_parameters: ->
return if @tag() isnt ')'
i: 0
while true
- i += 1
+ i: + 1
tok: @prev(i)
return if not tok
switch tok[0]
when 'IDENTIFIER' then tok[0]: 'PARAM'
when ')' then tok[0]: 'PARAM_END'
when '(' then return tok[0]: 'PARAM_START'
- trueClose up all remaining open blocks at the end of the file.
close_indentation: ->
- @outdent_token(@indent)The error for when you try to use a forbidden word in JavaScript as + true
Close up all remaining open blocks at the end of the file.
close_indentation: ->
+ @outdent_token(@indent)The error for when you try to use a forbidden word in JavaScript as an identifier.
identifier_error: (word) ->
- throw new Error "SyntaxError: Reserved word \"$word\" on line ${@line + 1}"The error for when you try to assign to a reserved word in JavaScript, + throw new Error "SyntaxError: Reserved word \"$word\" on line ${@line + 1}"
The error for when you try to assign to a reserved word in JavaScript, like "function" or "default".
assignment_error: ->
- throw new Error "SyntaxError: Reserved word \"${@value()}\" on line ${@line + 1} can't be assigned"Expand variables and expressions inside double-quoted strings using + throw new Error "SyntaxError: Reserved word \"${@value()}\" on line ${@line + 1} can't be assigned"
Expand variables and expressions inside double-quoted strings using ECMA Harmony's interpolation syntax for substitution of bare variables as well as arbitrary expressions.
@@ -254,13 +264,13 @@ token stream.Add a token to the results, taking note of the line number.
token: (tag, value) ->
- @tokens.push([tag, value, @line])Peek at a tag in the current token stream.
tag: (index, tag) ->
+ tokensAdd a token to the results, taking note of the line number.
token: (tag, value) ->
+ @tokens.push([tag, value, @line])Peek at a tag in the current token stream.
tag: (index, tag) ->
return unless tok: @prev(index)
return tok[0]: tag if tag?
- tok[0]Peek at a value in the current token stream.
value: (index, val) ->
+ tok[0]Peek at a value in the current token stream.
value: (index, val) ->
return unless tok: @prev(index)
return tok[1]: val if val?
- tok[1]Peek at a previous token, entire.
prev: (index) ->
- @tokens[@tokens.length - (index or 1)]Attempt to match a string against the current chunk, returning the indexed + tok[1]
Peek at a previous token, entire.
prev: (index) ->
+ @tokens[@tokens.length - (index or 1)]Attempt to match a string against the current chunk, returning the indexed
match if successful, and false otherwise.
match: (regex, index) ->
return false unless m: @chunk.match(regex)
- if m then m[index] else falseThere are no exensions to the core lexer by default.
Lexer.extensions: []Keywords that CoffeeScript shares in common with JavaScript.
JS_KEYWORDS: [
+ if m then m[index] else falseAre we in the midst of an unfinished expression?
unfinished: ->
+ prev: @prev(2)
+ @value() and @value().match and @value().match(NO_NEWLINE) and
+ prev and (prev[0] isnt '.') and not @value().match(CODE)There are no exensions to the core lexer by default.
Lexer.extensions: []Keywords that CoffeeScript shares in common with JavaScript.
JS_KEYWORDS: [
"if", "else",
"true", "false",
"new", "return",
@@ -306,51 +319,57 @@ match if successful, and false otherwise. CoffeeScript-only keywords, which we're more relaxed about allowing. They can't -be used standalone, but you can reference them as an attached property.
COFFEE_KEYWORDS: [
+]CoffeeScript-only keywords, which we're more relaxed about allowing. They can't +be used standalone, but you can reference them as an attached property.
COFFEE_ALIASES: ["and", "or", "is", "isnt", "not"]
+COFFEE_KEYWORDS: COFFEE_ALIASES.concat [
"then", "unless",
"yes", "no", "on", "off",
- "and", "or", "is", "isnt", "not",
"of", "by", "where", "when"
-]The combined list of keywords is the superset that gets passed verbatim to -the parser.
KEYWORDS: JS_KEYWORDS.concat COFFEE_KEYWORDSThe list of keywords that are reserved by JavaScript, but not used, or are +]
The combined list of keywords is the superset that gets passed verbatim to +the parser.
KEYWORDS: JS_KEYWORDS.concat COFFEE_KEYWORDSThe list of keywords that are reserved by JavaScript, but not used, or are used by CoffeeScript internally. We throw an error when these are encountered, to avoid having a JavaScript error at runtime.
RESERVED: [
"case", "default", "do", "function", "var", "void", "with"
"const", "let", "debugger", "enum", "export", "import", "native",
"__extends", "__hasProp"
-]The superset of both JavaScript keywords and reserved words, none of which may -be used as identifiers or properties.
JS_FORBIDDEN: JS_KEYWORDS.concat RESERVEDToken matching regexes.
IDENTIFIER : /^([a-zA-Z\$_](\w|\$)*)/
+]The superset of both JavaScript keywords and reserved words, none of which may +be used as identifiers or properties.
JS_FORBIDDEN: JS_KEYWORDS.concat RESERVEDToken matching regexes.
IDENTIFIER : /^([a-zA-Z\$_](\w|\$)*)/
NUMBER : /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i
HEREDOC : /^("{6}|'{6}|"{3}\n?([\s\S]*?)\n?([ \t]*)"{3}|'{3}\n?([\s\S]*?)\n?([ \t]*)'{3})/
INTERPOLATION : /^\$([a-zA-Z_@]\w*(\.\w+)*)/
-OPERATOR : /^([+\*&|\/\-%=<>:!?]+)/
+OPERATOR : /^([+\*&|\/\-%=<>:!?]+)([ \t]*)/
WHITESPACE : /^([ \t]+)/
COMMENT : /^(((\n?[ \t]*)?#[^\n]*)+)/
CODE : /^((-|=)>)/
MULTI_DENT : /^((\n([ \t]*))+)(\.)?/
LAST_DENTS : /\n([ \t]*)/g
LAST_DENT : /\n([ \t]*)/
-ASSIGNMENT : /^(:|=)$/Regex-matching-regexes.
REGEX_START : /^\/[^\/ ]/
+ASSIGNMENT : /^(:|=)$/Regex-matching-regexes.
REGEX_START : /^\/[^\/ ]/
REGEX_INTERPOLATION: /([^\\]\$[a-zA-Z_@]|[^\\]\$\{.*[^\\]\})/
REGEX_FLAGS : /^[imgy]{0,4}/
-REGEX_ESCAPE : /\\[^\$]/gToken cleaning regexes.
JS_CLEANER : /(^`|`$)/g
+REGEX_ESCAPE : /\\[^\$]/gToken cleaning regexes.
JS_CLEANER : /(^`|`$)/g
MULTILINER : /\n/g
STRING_NEWLINES : /\n[ \t]*/g
COMMENT_CLEANER : /(^[ \t]*#|\n[ \t]*$)/mg
NO_NEWLINE : /^([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)$/
-HEREDOC_INDENT : /^[ \t]+/mgTokens which a regular expression will never immediately follow, but which +HEREDOC_INDENT : /^[ \t]+/mg
Tokens which a regular expression will never immediately follow, but which a division operator might.
See: http://www.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions
Our list is shorter, due to sans-parentheses method calls.
NOT_REGEX: [
'NUMBER', 'REGEX', '++', '--', 'FALSE', 'NULL', 'TRUE'
-]Tokens which could legitimately be invoked or indexed. A opening +]
Tokens which could legitimately be invoked or indexed. A opening parentheses or bracket following these tokens will be recorded as the start -of a function invocation or indexing operation.
CALLABLE: ['IDENTIFIER', 'SUPER', ')', ']', '}', 'STRING', '@']Tokens that indicate an access -- keywords immediately following will be -treated as identifiers.
ACCESSORS: ['PROPERTY_ACCESS', 'PROTOTYPE_ACCESS', 'SOAK_ACCESS', '@']Tokens that, when immediately preceding a WHEN, indicate that the WHEN
+of a function invocation or indexing operation.
CALLABLE: ['IDENTIFIER', 'SUPER', ')', ']', '}', 'STRING', '@']Tokens that indicate an access -- keywords immediately following will be +treated as identifiers.
ACCESSORS: ['PROPERTY_ACCESS', 'PROTOTYPE_ACCESS', 'SOAK_ACCESS', '@']Tokens that, when immediately preceding a WHEN, indicate that the WHEN
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']
+avoid an ambiguity in the grammar. LINE_BREAK: ['INDENT', 'OUTDENT', 'TERMINATOR']Half-assignments...
HALF_ASSIGNMENTS: ['-', '+', '/', '*', '%', '||', '&&', '?']Conversions from CoffeeScript operators into JavaScript ones.
CONVERSIONS: {
+ 'and': '&&'
+ 'or': '||'
+ 'is': '=='
+ 'isnt': '!='
+ 'not': '!'
+}