2009-12-17 22:13:29 -05:00
|
|
|
module CoffeeScript
|
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# The lexer reads a stream of CoffeeScript and divvys it up into tagged
|
|
|
|
# tokens. A minor bit of the ambiguity in the grammar has been avoided by
|
|
|
|
# pushing some extra smarts into the Lexer.
|
2009-12-17 22:13:29 -05:00
|
|
|
class Lexer
|
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# The list of keywords passed verbatim to the parser.
|
2009-12-17 22:13:29 -05:00
|
|
|
KEYWORDS = ["if", "else", "then", "unless",
|
2009-12-23 20:24:55 -05:00
|
|
|
"true", "false", "yes", "no", "on", "off",
|
2009-12-17 22:13:29 -05:00
|
|
|
"and", "or", "is", "aint", "not",
|
|
|
|
"new", "return",
|
|
|
|
"try", "catch", "finally", "throw",
|
|
|
|
"break", "continue",
|
|
|
|
"for", "in", "while",
|
2009-12-24 04:33:59 -05:00
|
|
|
"switch", "when",
|
2009-12-22 12:08:29 -05:00
|
|
|
"super",
|
2009-12-24 14:50:44 -05:00
|
|
|
"delete", "instanceof", "typeof"]
|
2009-12-17 22:13:29 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Token matching regexes.
|
2009-12-17 22:13:29 -05:00
|
|
|
IDENTIFIER = /\A([a-zA-Z$_]\w*)/
|
|
|
|
NUMBER = /\A\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?))\b/i
|
2009-12-18 09:55:31 -05:00
|
|
|
STRING = /\A(""|''|"(.*?)[^\\]"|'(.*?)[^\\]')/m
|
2009-12-23 20:24:55 -05:00
|
|
|
JS = /\A(``|`(.*?)[^\\]`)/m
|
2009-12-23 21:00:04 -05:00
|
|
|
OPERATOR = /\A([+\*&|\/\-%=<>:!]+)/
|
2009-12-17 22:13:29 -05:00
|
|
|
WHITESPACE = /\A([ \t\r]+)/
|
2009-12-22 11:27:19 -05:00
|
|
|
NEWLINE = /\A(\n+)/
|
|
|
|
COMMENT = /\A((#[^\n]*\s*)+)/m
|
2009-12-17 22:13:29 -05:00
|
|
|
CODE = /\A(=>)/
|
|
|
|
REGEX = /\A(\/(.*?)[^\\]\/[imgy]{0,4})/
|
2009-12-24 16:48:46 -05:00
|
|
|
INDENT = /\A\n( *)/
|
2009-12-17 22:13:29 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Token cleaning regexes.
|
2009-12-17 22:13:29 -05:00
|
|
|
JS_CLEANER = /(\A`|`\Z)/
|
2009-12-22 11:27:19 -05:00
|
|
|
MULTILINER = /\n/
|
2009-12-22 11:50:43 -05:00
|
|
|
COMMENT_CLEANER = /(^\s*#|\n\s*$)/
|
2009-12-17 22:13:29 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Tokens that always constitute the start of an expression.
|
2009-12-17 22:13:29 -05:00
|
|
|
EXP_START = ['{', '(', '[']
|
2009-12-17 22:54:24 -05:00
|
|
|
|
|
|
|
# Tokens that always constitute the end of an expression.
|
2009-12-17 22:13:29 -05:00
|
|
|
EXP_END = ['}', ')', ']']
|
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Scan by attempting to match tokens one character at a time. Slow and steady.
|
2009-12-17 22:13:29 -05:00
|
|
|
def tokenize(code)
|
2009-12-17 22:54:24 -05:00
|
|
|
@code = code.chomp # Cleanup code by remove extra line breaks
|
|
|
|
@i = 0 # Current character position we're parsing
|
|
|
|
@line = 1 # The current line.
|
2009-12-24 16:48:46 -05:00
|
|
|
@indent = 0 # The current indent level.
|
|
|
|
@indents = [] # The stack of all indent levels we are currently within.
|
2009-12-17 22:54:24 -05:00
|
|
|
@tokens = [] # Collection of all parsed tokens in the form [:TOKEN_TYPE, value]
|
2009-12-17 22:13:29 -05:00
|
|
|
while @i < @code.length
|
|
|
|
@chunk = @code[@i..-1]
|
|
|
|
extract_next_token
|
|
|
|
end
|
|
|
|
@tokens
|
2009-12-13 17:07:16 -05:00
|
|
|
end
|
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# At every position, run this list of match attempts, short-circuiting if
|
|
|
|
# any of them succeed.
|
2009-12-17 22:13:29 -05:00
|
|
|
def extract_next_token
|
|
|
|
return if identifier_token
|
|
|
|
return if number_token
|
|
|
|
return if string_token
|
|
|
|
return if js_token
|
|
|
|
return if regex_token
|
2009-12-22 11:27:19 -05:00
|
|
|
return if comment_token
|
2009-12-24 16:48:46 -05:00
|
|
|
return if indent_token
|
2009-12-17 22:13:29 -05:00
|
|
|
return if whitespace_token
|
|
|
|
return literal_token
|
|
|
|
end
|
2009-12-13 17:07:16 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Matches identifying literals: variables, keywords, method names, etc.
|
2009-12-17 22:13:29 -05:00
|
|
|
def identifier_token
|
|
|
|
return false unless identifier = @chunk[IDENTIFIER, 1]
|
|
|
|
# Keywords are special identifiers tagged with their own name, 'if' will result
|
|
|
|
# in an [:IF, "if"] token
|
|
|
|
tag = KEYWORDS.include?(identifier) ? identifier.upcase.to_sym : :IDENTIFIER
|
|
|
|
@tokens[-1][0] = :PROPERTY_ACCESS if tag == :IDENTIFIER && last_value == '.'
|
|
|
|
token(tag, identifier)
|
|
|
|
@i += identifier.length
|
|
|
|
end
|
2009-12-13 17:07:16 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Matches numbers, including decimals, hex, and exponential notation.
|
2009-12-17 22:13:29 -05:00
|
|
|
def number_token
|
|
|
|
return false unless number = @chunk[NUMBER, 1]
|
|
|
|
token(:NUMBER, number)
|
|
|
|
@i += number.length
|
|
|
|
end
|
2009-12-13 17:07:16 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Matches strings, including multi-line strings.
|
2009-12-17 22:13:29 -05:00
|
|
|
def string_token
|
|
|
|
return false unless string = @chunk[STRING, 1]
|
|
|
|
escaped = string.gsub(MULTILINER) do |match|
|
|
|
|
@line += 1
|
2009-12-24 01:22:41 -05:00
|
|
|
" \\\n"
|
2009-12-17 22:13:29 -05:00
|
|
|
end
|
|
|
|
token(:STRING, escaped)
|
|
|
|
@i += string.length
|
2009-12-17 09:29:49 -05:00
|
|
|
end
|
2009-12-13 17:07:16 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Matches interpolated JavaScript.
|
2009-12-17 22:13:29 -05:00
|
|
|
def js_token
|
|
|
|
return false unless script = @chunk[JS, 1]
|
|
|
|
token(:JS, script.gsub(JS_CLEANER, ''))
|
|
|
|
@i += script.length
|
|
|
|
end
|
2009-12-15 09:11:27 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Matches regular expression literals.
|
2009-12-17 22:13:29 -05:00
|
|
|
def regex_token
|
|
|
|
return false unless regex = @chunk[REGEX, 1]
|
|
|
|
token(:REGEX, regex)
|
|
|
|
@i += regex.length
|
|
|
|
end
|
2009-12-13 18:37:29 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Matches and consumes comments.
|
2009-12-22 11:27:19 -05:00
|
|
|
def comment_token
|
2009-12-17 22:13:29 -05:00
|
|
|
return false unless comment = @chunk[COMMENT, 1]
|
2009-12-23 19:42:18 -05:00
|
|
|
@line += comment.scan(MULTILINER).length
|
2009-12-22 11:27:19 -05:00
|
|
|
token(:COMMENT, comment.gsub(COMMENT_CLEANER, '').split(MULTILINER))
|
|
|
|
token("\n", "\n")
|
2009-12-17 22:13:29 -05:00
|
|
|
@i += comment.length
|
|
|
|
end
|
2009-12-13 17:07:16 -05:00
|
|
|
|
2009-12-24 16:48:46 -05:00
|
|
|
def indent_token
|
|
|
|
return false unless indent = @chunk[INDENT, 1]
|
|
|
|
size = indent.size
|
|
|
|
return literal_token if size == @indent
|
|
|
|
if size > @indent
|
|
|
|
tag = :INDENT
|
|
|
|
@indent = size
|
|
|
|
@indents << @indent
|
|
|
|
else
|
|
|
|
tag = :OUTDENT
|
|
|
|
@indents.pop
|
|
|
|
@indent = @indents.first || 0
|
|
|
|
end
|
|
|
|
@i += (size + 1)
|
|
|
|
token(tag, size)
|
|
|
|
end
|
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Matches and consumes non-meaningful whitespace.
|
2009-12-17 22:13:29 -05:00
|
|
|
def whitespace_token
|
|
|
|
return false unless whitespace = @chunk[WHITESPACE, 1]
|
|
|
|
@i += whitespace.length
|
|
|
|
end
|
2009-12-13 17:07:16 -05:00
|
|
|
|
2009-12-17 22:13:29 -05:00
|
|
|
# We treat all other single characters as a token. Eg.: ( ) , . !
|
|
|
|
# Multi-character operators are also literal tokens, so that Racc can assign
|
2009-12-17 22:54:24 -05:00
|
|
|
# the proper order of operations. Multiple newlines get merged together.
|
2009-12-17 22:13:29 -05:00
|
|
|
def literal_token
|
|
|
|
value = @chunk[NEWLINE, 1]
|
|
|
|
if value
|
|
|
|
@line += value.length
|
|
|
|
token("\n", "\n") unless last_value == "\n"
|
|
|
|
return @i += value.length
|
|
|
|
end
|
|
|
|
value = @chunk[OPERATOR, 1]
|
|
|
|
tag_parameters if value && value.match(CODE)
|
|
|
|
value ||= @chunk[0,1]
|
|
|
|
skip_following_newlines if EXP_START.include?(value)
|
|
|
|
remove_leading_newlines if EXP_END.include?(value)
|
|
|
|
token(value, value)
|
|
|
|
@i += value.length
|
2009-12-13 17:07:16 -05:00
|
|
|
end
|
|
|
|
|
2009-12-22 10:48:58 -05:00
|
|
|
# Add a token to the results, taking note of the line number, and
|
|
|
|
# immediately-preceding comment.
|
2009-12-17 22:13:29 -05:00
|
|
|
def token(tag, value)
|
2009-12-22 11:27:19 -05:00
|
|
|
@tokens << [tag, Value.new(value, @line)]
|
2009-12-17 22:13:29 -05:00
|
|
|
end
|
2009-12-17 09:29:49 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Peek at the previous token.
|
2009-12-17 22:13:29 -05:00
|
|
|
def last_value
|
|
|
|
@tokens.last && @tokens.last[1]
|
|
|
|
end
|
2009-12-17 09:29:49 -05:00
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# A source of ambiguity in our grammar was parameter lists in function
|
|
|
|
# definitions (as opposed to argument lists in function calls). Tag
|
|
|
|
# parameter identifiers in order to avoid this.
|
2009-12-17 22:13:29 -05:00
|
|
|
def tag_parameters
|
|
|
|
index = 0
|
|
|
|
loop do
|
|
|
|
tok = @tokens[index -= 1]
|
2009-12-18 07:11:01 -05:00
|
|
|
return if !tok
|
2009-12-17 22:13:29 -05:00
|
|
|
next if tok[0] == ','
|
2009-12-18 07:11:01 -05:00
|
|
|
return if tok[0] != :IDENTIFIER
|
2009-12-17 22:13:29 -05:00
|
|
|
tok[0] = :PARAM
|
|
|
|
end
|
2009-12-13 20:29:44 -05:00
|
|
|
end
|
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Consume and ignore newlines immediately after this point.
|
2009-12-17 22:13:29 -05:00
|
|
|
def skip_following_newlines
|
|
|
|
newlines = @code[(@i+1)..-1][NEWLINE, 1]
|
|
|
|
if newlines
|
|
|
|
@line += newlines.length
|
|
|
|
@i += newlines.length
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-12-17 22:54:24 -05:00
|
|
|
# Discard newlines immediately before this point.
|
2009-12-17 22:13:29 -05:00
|
|
|
def remove_leading_newlines
|
|
|
|
@tokens.pop if last_value == "\n"
|
2009-12-17 09:29:49 -05:00
|
|
|
end
|
2009-12-16 20:48:37 -05:00
|
|
|
|
2009-12-24 16:48:46 -05:00
|
|
|
# Close up all remaining open blocks.
|
|
|
|
def close_indentation
|
|
|
|
while indent = @indents.pop
|
|
|
|
token(:OUTDENT, @indents.first || 0)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-12-16 20:48:37 -05:00
|
|
|
end
|
|
|
|
|
2009-12-13 17:07:16 -05:00
|
|
|
end
|