{Rewriter, INVERSES} = require './rewriter'
CoffeeScriptの字句解析器。ソースコードの先頭と一致を試みる、一連のトークン照合正規表現を使用します。一致が見つかると、トークンが生成され、一致した部分を消費し、最初からやり直します。トークンは次の形式になります。
[tag, value, locationData]
ここで、locationDataは{first_line, first_column, last_line, last_column}であり、Jisonに直接供給できる形式です。これらは、coffee-script.coffeeで定義されているparser.lexer
関数でJisonによって読み取られます。
{Rewriter, INVERSES} = require './rewriter'
必要なヘルパーをインポートします。
{count, starts, compact, repeat, invertLiterate,
locationDataToString, throwSyntaxError} = require './helpers'
LexerクラスはCoffeeScriptのストリームを読み取り、タグ付きトークンに分割します。文法における潜在的な曖昧性のいくつかは、Lexerにいくつかの追加のスマート機能を追加することで回避されています。
exports.Lexer = class Lexer
tokenizeはLexerの主要なメソッドです。残りのコードの先頭に固定された正規表現、またはカスタムの再帰的なトークン照合メソッド(補間の場合)を使用して、一度に1つずつトークンの一致を試みることでスキャンします。次のトークンが記録されると、トークンを越えてコード内で先に進み、再度開始します。
各トークン化メソッドは、消費した文字数を返す責任があります。
Rewriterを通してトークンストリームを実行してから返します。
tokenize: (code, opts = {}) ->
@literate = opts.literate # Are we lexing literate CoffeeScript?
@indent = 0 # The current indentation level.
@baseIndent = 0 # The overall minimum indentation level
@indebt = 0 # The over-indentation at the current level.
@outdebt = 0 # The under-outdentation at the current level.
@indents = [] # The stack of all current indentation levels.
@ends = [] # The stack for pairing up tokens.
@tokens = [] # Stream of parsed tokens in the form `['TYPE', value, location data]`.
@seenFor = no # Used to recognize FORIN, FOROF and FORFROM tokens.
@seenImport = no # Used to recognize IMPORT FROM? AS? tokens.
@seenExport = no # Used to recognize EXPORT FROM? AS? tokens.
@importSpecifierList = no # Used to identify when in an IMPORT {...} FROM? ...
@exportSpecifierList = no # Used to identify when in an EXPORT {...} FROM? ...
@chunkLine =
opts.line or 0 # The start line for the current @chunk.
@chunkColumn =
opts.column or 0 # The start column of the current @chunk.
code = @clean code # The stripped, cleaned original source code.
すべての位置で、この試行一致リストを実行し、いずれかが成功した場合はショートサーキットします。それらの順序は優先順位を決定します。@literalToken
はフォールバックの総称です。
i = 0
while @chunk = code[i..]
consumed = \
@identifierToken() or
@commentToken() or
@whitespaceToken() or
@lineToken() or
@stringToken() or
@numberToken() or
@regexToken() or
@jsToken() or
@literalToken()
位置の更新
[@chunkLine, @chunkColumn] = @getLineAndColumnFromChunk consumed
i += consumed
return {@tokens, index: i} if opts.untilBalanced and @ends.length is 0
@closeIndentation()
@error "missing #{end.tag}", end.origin[2] if end = @ends.pop()
return @tokens if opts.rewrite is off
(new Rewriter).rewrite @tokens
先頭と末尾の空白、キャリッジリターンなどを削除するためにコードを前処理します。リテラルCoffeeScriptを字句解析している場合、少なくとも4つのスペースまたはタブでインデントされていないすべての行を削除することで、外部のMarkdownを削除します。
clean: (code) ->
code = code.slice(1) if code.charCodeAt(0) is BOM
code = code.replace(/\r/g, '').replace TRAILING_SPACES, ''
if WHITESPACE.test code
code = "\n#{code}"
@chunkLine--
code = invertLiterate code if @literate
code
リテラル(変数、キーワード、メソッド名など)を識別する一致。JavaScriptの予約語が識別子として使用されていないことを確認します。CoffeeScriptはJavaScriptで許可されているいくつかのキーワードを予約しているので、ここではプロパティ名として参照されている場合にキーワードとしてタグ付けしないように注意しています。そのため、is
が===
を意味するにもかかわらず、jQuery.is()
を実行できます。
identifierToken: ->
return 0 unless match = IDENTIFIER.exec @chunk
[input, id, colon] = match
位置データのIDの長さを保持
idLength = id.length
poppedToken = undefined
if id is 'own' and @tag() is 'FOR'
@token 'OWN', id
return id.length
if id is 'from' and @tag() is 'YIELD'
@token 'FROM', id
return id.length
if id is 'as' and @seenImport
if @value() is '*'
@tokens[@tokens.length - 1][0] = 'IMPORT_ALL'
else if @value() in COFFEE_KEYWORDS
@tokens[@tokens.length - 1][0] = 'IDENTIFIER'
if @tag() in ['DEFAULT', 'IMPORT_ALL', 'IDENTIFIER']
@token 'AS', id
return id.length
if id is 'as' and @seenExport and @tag() in ['IDENTIFIER', 'DEFAULT']
@token 'AS', id
return id.length
if id is 'default' and @seenExport and @tag() in ['EXPORT', 'AS']
@token 'DEFAULT', id
return id.length
[..., prev] = @tokens
tag =
if colon or prev? and
(prev[0] in ['.', '?.', '::', '?::'] or
not prev.spaced and prev[0] is '@')
'PROPERTY'
else
'IDENTIFIER'
if tag is 'IDENTIFIER' and (id in JS_KEYWORDS or id in COFFEE_KEYWORDS) and
not (@exportSpecifierList and id in COFFEE_KEYWORDS)
tag = id.toUpperCase()
if tag is 'WHEN' and @tag() in LINE_BREAK
tag = 'LEADING_WHEN'
else if tag is 'FOR'
@seenFor = yes
else if tag is 'UNLESS'
tag = 'IF'
else if tag is 'IMPORT'
@seenImport = yes
else if tag is 'EXPORT'
@seenExport = yes
else if tag in UNARY
tag = 'UNARY'
else if tag in RELATION
if tag isnt 'INSTANCEOF' and @seenFor
tag = 'FOR' + tag
@seenFor = no
else
tag = 'RELATION'
if @value() is '!'
poppedToken = @tokens.pop()
id = '!' + id
else if tag is 'IDENTIFIER' and @seenFor and id is 'from' and
isForFrom(prev)
tag = 'FORFROM'
@seenFor = no
if tag is 'IDENTIFIER' and id in RESERVED
@error "reserved word '#{id}'", length: id.length
unless tag is 'PROPERTY'
if id in COFFEE_ALIASES
alias = id
id = COFFEE_ALIAS_MAP[id]
tag = switch id
when '!' then 'UNARY'
when '==', '!=' then 'COMPARE'
when 'true', 'false' then 'BOOL'
when 'break', 'continue', \
'debugger' then 'STATEMENT'
when '&&', '||' then id
else tag
tagToken = @token tag, id, 0, idLength
tagToken.origin = [tag, alias, tagToken[2]] if alias
if poppedToken
[tagToken[2].first_line, tagToken[2].first_column] =
[poppedToken[2].first_line, poppedToken[2].first_column]
if colon
colonOffset = input.lastIndexOf ':'
@token ':', ':', colonOffset, colon.length
input.length
小数、16進数、指数表記を含む数値に一致します。進行中の範囲に干渉しないように注意してください。
numberToken: ->
return 0 unless match = NUMBER.exec @chunk
number = match[0]
lexedLength = number.length
switch
when /^0[BOX]/.test number
@error "radix prefix in '#{number}' must be lowercase", offset: 1
when /^(?!0x).*E/.test number
@error "exponential notation in '#{number}' must be indicated with a lowercase 'e'",
offset: number.indexOf('E')
when /^0\d*[89]/.test number
@error "decimal literal '#{number}' must not be prefixed with '0'", length: lexedLength
when /^0\d+/.test number
@error "octal literal '#{number}' must be prefixed with '0o'", length: lexedLength
base = switch number.charAt 1
when 'b' then 2
when 'o' then 8
when 'x' then 16
else null
numberValue = if base? then parseInt(number[2..], base) else parseFloat(number)
if number.charAt(1) in ['b', 'o']
number = "0x#{numberValue.toString 16}"
tag = if numberValue is Infinity then 'INFINITY' else 'NUMBER'
@token tag, number, 0, lexedLength
lexedLength
補間付きまたはなしで、複数行文字列、heredocを含む文字列に一致します。
stringToken: ->
[quote] = STRING_START.exec(@chunk) || []
return 0 unless quote
前のトークンがfrom
であり、これがインポートまたはエクスポートステートメントの場合、from
に適切にタグを付けます。
if @tokens.length and @value() is 'from' and (@seenImport or @seenExport)
@tokens[@tokens.length - 1][0] = 'FROM'
regex = switch quote
when "'" then STRING_SINGLE
when '"' then STRING_DOUBLE
when "'''" then HEREDOC_SINGLE
when '"""' then HEREDOC_DOUBLE
heredoc = quote.length is 3
{tokens, index: end} = @matchWithInterpolations regex, quote
$ = tokens.length - 1
delimiter = quote.charAt(0)
if heredoc
最小のインデントを見つけます。後ですべての行から削除されます。
indent = null
doc = (token[1] for token, i in tokens when token[0] is 'NEOSTRING').join '#{}'
while match = HEREDOC_INDENT.exec doc
attempt = match[1]
indent = attempt if indent is null or 0 < attempt.length < indent.length
indentRegex = /// \n#{indent} ///g if indent
@mergeInterpolationTokens tokens, {delimiter}, (value, i) =>
value = @formatString value, delimiter: quote
value = value.replace indentRegex, '\n' if indentRegex
value = value.replace LEADING_BLANK_LINE, '' if i is 0
value = value.replace TRAILING_BLANK_LINE, '' if i is $
value
else
@mergeInterpolationTokens tokens, {delimiter}, (value, i) =>
value = @formatString value, delimiter: quote
value = value.replace SIMPLE_STRING_OMIT, (match, offset) ->
if (i is 0 and offset is 0) or
(i is $ and offset + match.length is value.length)
''
else
' '
value
end
コメントに一致し、消費します。
commentToken: ->
return 0 unless match = @chunk.match COMMENT
[comment, here] = match
if here
if match = HERECOMMENT_ILLEGAL.exec comment
@error "block comments cannot contain #{match[0]}",
offset: match.index, length: match[0].length
if here.indexOf('\n') >= 0
here = here.replace /// \n #{repeat ' ', @indent} ///g, '\n'
@token 'HERECOMMENT', here, 0, comment.length
comment.length
バックティックを使用してソースに直接補間されたJavaScriptに一致します。
jsToken: ->
return 0 unless @chunk.charAt(0) is '`' and
(match = HERE_JSTOKEN.exec(@chunk) or JSTOKEN.exec(@chunk))
エスケープされたバックティックをバックティックに、エスケープされたバックティックの直前のエスケープされたバックスラッシュをバックスラッシュに変換します。
script = match[1].replace /\\+(`|$)/g, (string) ->
string
は常に '```', '\`', '\\`' などの値です。後半分に減らすことで、'```' を '```' に、'\`' を '```' などに変換します。
string[-Math.ceil(string.length / 2)..]
@token 'JS', script, 0, match[0].length
match[0].length
正規表現リテラル、および複数行の拡張された正規表現に一致します。正規表現の字句解析は除算との区別が難しいので、JavaScriptとRubyの基本的なヒューリスティックを借用しています。
regexToken: ->
switch
when match = REGEX_ILLEGAL.exec @chunk
@error "regular expressions cannot begin with #{match[2]}",
offset: match.index + match[1].length
when match = @matchWithInterpolations HEREGEX, '///'
{tokens, index} = match
when match = REGEX.exec @chunk
[regex, body, closed] = match
@validateEscapes body, isRegex: yes, offsetInChunk: 1
body = @formatRegex body, delimiter: '/'
index = regex.length
[..., prev] = @tokens
if prev
if prev.spaced and prev[0] in CALLABLE
return 0 if not closed or POSSIBLY_DIVISION.test regex
else if prev[0] in NOT_REGEX
return 0
@error 'missing / (unclosed regex)' unless closed
else
return 0
[flags] = REGEX_FLAGS.exec @chunk[index..]
end = index + flags.length
origin = @makeToken 'REGEX', null, 0, end
switch
when not VALID_FLAGS.test flags
@error "invalid regular expression flags #{flags}", offset: index, length: flags.length
when regex or tokens.length is 1
body ?= @formatHeregex tokens[0][1]
@token 'REGEX', "#{@makeDelimitedLiteral body, delimiter: '/'}#{flags}", 0, end, origin
else
@token 'REGEX_START', '(', 0, 0, origin
@token 'IDENTIFIER', 'RegExp', 0, 0
@token 'CALL_START', '(', 0, 0
@mergeInterpolationTokens tokens, {delimiter: '"', double: yes}, @formatHeregex
if flags
@token ',', ',', index - 1, 0
@token 'STRING', '"' + flags + '"', index - 1, flags.length
@token ')', ')', end - 1, 0
@token 'REGEX_END', ')', end - 1, 0
end
改行、インデント、アウトデントに一致し、消費して、どちらであるかを判断します。現在の行が次の行に継続されていることが検出できる場合、改行は抑制されます。
elements
.each( ... )
.map( ... )
インデントレベルを追跡します。単一のアウトデントトークンで複数のインデントを閉じることができるため、現在どの程度インデントされているかを把握する必要があります。
lineToken: ->
return 0 unless match = MULTI_DENT.exec @chunk
indent = match[0]
@seenFor = no
@seenImport = no unless @importSpecifierList
@seenExport = no unless @exportSpecifierList
size = indent.length - 1 - indent.lastIndexOf '\n'
noNewlines = @unfinished()
if size - @indebt is @indent
if noNewlines then @suppressNewlines() else @newlineToken 0
return indent.length
if size > @indent
if noNewlines
@indebt = size - @indent
@suppressNewlines()
return indent.length
unless @tokens.length
@baseIndent = @indent = size
return indent.length
diff = size - @indent + @outdebt
@token 'INDENT', diff, indent.length - size, size
@indents.push diff
@ends.push {tag: 'OUTDENT'}
@outdebt = @indebt = 0
@indent = size
else if size < @baseIndent
@error 'missing indentation', offset: indent.length
else
@indebt = 0
@outdentToken @indent - size, noNewlines, indent.length
indent.length
記録された複数のインデントを内側に移動している場合、アウトデントトークンまたは複数のトークンを記録します。新しい@indent値を設定します。
outdentToken: (moveOut, noNewlines, outdentLength) ->
decreasedIndent = @indent - moveOut
while moveOut > 0
lastIndent = @indents[@indents.length - 1]
if not lastIndent
moveOut = 0
else if lastIndent is @outdebt
moveOut -= @outdebt
@outdebt = 0
else if lastIndent < @outdebt
@outdebt -= lastIndent
moveOut -= lastIndent
else
dent = @indents.pop() + @outdebt
if outdentLength and @chunk[outdentLength] in INDENTABLE_CLOSERS
decreasedIndent -= dent - moveOut
moveOut = dent
@outdebt = 0
pairはoutdentTokenを呼び出す可能性があるため、decreasedIndentを保持します。
@pair 'OUTDENT'
@token 'OUTDENT', moveOut, 0, outdentLength
moveOut -= dent
@outdebt -= moveOut if dent
@tokens.pop() while @value() is ';'
@token 'TERMINATOR', '\n', outdentLength, 0 unless @tag() is 'TERMINATOR' or noNewlines
@indent = decreasedIndent
this
意味のない空白に一致し、消費します。いくつかの場合に違いが生じるため、前のトークンに「スペース付き」というタグを付けます。
whitespaceToken: ->
return 0 unless (match = WHITESPACE.exec @chunk) or
(nline = @chunk.charAt(0) is '\n')
[..., prev] = @tokens
prev[if match then 'spaced' else 'newLine'] = true if prev
if match then match[0].length else 0
改行トークンを生成します。連続する改行はマージされます。
newlineToken: (offset) ->
@tokens.pop() while @value() is ';'
@token 'TERMINATOR', '\n', offset, 0 unless @tag() is 'TERMINATOR'
this
行末の\
を使用して改行を抑制します。スラッシュは、その役割を終えた時点でここで削除されます。
suppressNewlines: ->
@tokens.pop() if @value() is '\\'
this
他のすべての単一文字をトークンとして扱います。例:( ) , . !
多文字演算子もリテラルトークンなので、Jisonは適切な演算順序を割り当てることができます。ここで特別にタグ付けする記号がいくつかあります。;
と改行はどちらもTERMINATOR
として扱われ、メソッド呼び出しを示す括弧と通常の括弧を区別するなどします。
literalToken: ->
if match = OPERATOR.exec @chunk
[value] = match
@tagParameters() if CODE.test value
else
value = @chunk.charAt 0
tag = value
[..., prev] = @tokens
if prev and value in ['=', COMPOUND_ASSIGN...]
skipToken = false
if value is '=' and prev[1] in ['||', '&&'] and not prev.spaced
prev[0] = 'COMPOUND_ASSIGN'
prev[1] += '='
prev = @tokens[@tokens.length - 2]
skipToken = true
if prev and prev[0] isnt 'PROPERTY'
origin = prev.origin ? prev
message = isUnassignable prev[1], origin[1]
@error message, origin[2] if message
return value.length if skipToken
if value is '{' and @seenImport
@importSpecifierList = yes
else if @importSpecifierList and value is '}'
@importSpecifierList = no
else if value is '{' and prev?[0] is 'EXPORT'
@exportSpecifierList = yes
else if @exportSpecifierList and value is '}'
@exportSpecifierList = no
if value is ';'
@seenFor = @seenImport = @seenExport = no
tag = 'TERMINATOR'
else if value is '*' and prev[0] is 'EXPORT'
tag = 'EXPORT_ALL'
else if value in MATH then tag = 'MATH'
else if value in COMPARE then tag = 'COMPARE'
else if value in COMPOUND_ASSIGN then tag = 'COMPOUND_ASSIGN'
else if value in UNARY then tag = 'UNARY'
else if value in UNARY_MATH then tag = 'UNARY_MATH'
else if value in SHIFT then tag = 'SHIFT'
else if value is '?' and prev?.spaced then tag = 'BIN?'
else if prev and not prev.spaced
if value is '(' and prev[0] in CALLABLE
prev[0] = 'FUNC_EXIST' if prev[0] is '?'
tag = 'CALL_START'
else if value is '[' and prev[0] in INDEXABLE
tag = 'INDEX_START'
switch prev[0]
when '?' then prev[0] = 'INDEX_SOAK'
token = @makeToken tag, value
switch value
when '(', '{', '[' then @ends.push {tag: INVERSES[value], origin: token}
when ')', '}', ']' then @pair value
@tokens.push token
value.length
文法における曖昧性の原因の一つは、関数定義のパラメータリストと関数呼び出しの引数リストでした。後ろ向きに移動して、パーサーにとって容易にするためにパラメーターに特別にタグ付けします。
tagParameters: ->
return this if @tag() isnt ')'
stack = []
{tokens} = this
i = tokens.length
tokens[--i][0] = 'PARAM_END'
while tok = tokens[--i]
switch tok[0]
when ')'
stack.push tok
when '(', 'CALL_START'
if stack.length then stack.pop()
else if tok[0] is '('
tok[0] = 'PARAM_START'
return this
else return this
this
ファイルの最後に残っているすべての開いているブロックを閉じます。
closeIndentation: ->
@outdentToken @indent
区切り文字付きトークンの内容に一致し、Rubyのような表記を使用して、任意の式の置換を使用して内部の変数と式を展開します。
"Hello #{name.capitalize()}."
補間が見つかった場合、このメソッドは再帰的に新しいLexerを作成し、#{
の{
が}
でバランスが取れるまでトークン化します。
regex
はトークンの内容に一致します(ただし、delimiter
と、補間が必要な場合は#{
は除きます)。delimiter
はトークンの区切り文字です。例としては、'
、"
、'''
、"""
、///
などがあります。このメソッドにより、文字列内の補間内の文字列を無限に持つことができます。
matchWithInterpolations: (regex, delimiter) ->
tokens = []
offsetInChunk = delimiter.length
return null unless @chunk[...offsetInChunk] is delimiter
str = @chunk[offsetInChunk..]
loop
[strPart] = regex.exec str
@validateEscapes strPart, {isRegex: delimiter.charAt(0) is '/', offsetInChunk}
後で実際の文字列に変換される偽の 'NEOSTRING' トークンをプッシュします。
tokens.push @makeToken 'NEOSTRING', strPart, offsetInChunk
str = str[strPart.length..]
offsetInChunk += strPart.length
break unless str[...2] is '#{'
1
は#{
の#
を削除するためです。
[line, column] = @getLineAndColumnFromChunk offsetInChunk + 1
{tokens: nested, index} =
new Lexer().tokenize str[1..], line: line, column: column, untilBalanced: on
末尾の}
をスキップします。
index += 1
先頭と末尾の{
と}
を括弧に変換します。不要な括弧は後で削除されます。
[open, ..., close] = nested
open[0] = open[1] = '('
close[0] = close[1] = ')'
close.origin = ['', 'end of interpolation', close[2]]
先頭の 'TERMINATOR'(存在する場合)を削除します。
nested.splice 1, 1 if nested[1]?[0] is 'TERMINATOR'
後で実際のトークンに変換される偽の 'TOKENS' トークンをプッシュします。
tokens.push ['TOKENS', nested]
str = str[index..]
offsetInChunk += index
unless str[...delimiter.length] is delimiter
@error "missing #{delimiter}", length: delimiter.length
[firstToken, ..., lastToken] = tokens
firstToken[2].first_column -= delimiter.length
if lastToken[1].substr(-1) is '\n'
lastToken[2].last_line += 1
lastToken[2].last_column = delimiter.length - 1
else
lastToken[2].last_column += delimiter.length
lastToken[2].last_column -= 1 if lastToken[1].length is 0
{tokens, index: offsetInChunk + delimiter.length}
偽のトークンタイプ 'TOKENS' と 'NEOSTRING' の配列tokens
(matchWithInterpolations
によって返される)をトークンストリームにマージします。'NEOSTRING' の値はfn
を使用して変換され、最初にoptions
を使用して文字列に変換されます。
mergeInterpolationTokens: (tokens, options, fn) ->
if tokens.length > 1
lparen = @token 'STRING_START', '(', 0, 0
firstIndex = @tokens.length
for token, i in tokens
[tag, value] = token
switch tag
when 'TOKENS'
空の補間(空の括弧のペア)を最適化します。
continue if value.length is 2
偽の 'TOKENS' トークンのすべてのトークンをプッシュします。これらはすでに適切な位置データを持っています。
locationToken = value[0]
tokensToPush = value
when 'NEOSTRING'
'NEOSTRING' を 'STRING' に変換します。
converted = fn.call this, token[1], i
空の文字列を最適化します。ただし、結果が本当に文字列であることを確認するために、トークンストリームは常に文字列トークンで始まるようにします。
if converted.length is 0
if i is 0
firstEmptyStringIndex = @tokens.length
else
continue
ただし、開始する空の文字列を最適化できるケースが1つあります。
if i is 2 and firstEmptyStringIndex?
@tokens.splice firstEmptyStringIndex, 2 # Remove empty string and the plus.
token[0] = 'STRING'
token[1] = @makeDelimitedLiteral converted, options
locationToken = token
tokensToPush = [token]
if @tokens.length > firstIndex
0の長さの '+' トークンを作成します。
plusToken = @token '+', '+'
plusToken[2] =
first_line: locationToken[2].first_line
first_column: locationToken[2].first_column
last_line: locationToken[2].first_line
last_column: locationToken[2].first_column
@tokens.push tokensToPush...
if lparen
[..., lastToken] = tokens
lparen.origin = ['STRING', null,
first_line: lparen[2].first_line
first_column: lparen[2].first_column
last_line: lastToken[2].last_line
last_column: lastToken[2].last_column
]
rparen = @token 'STRING_END', ')'
rparen[2] =
first_line: lastToken[2].last_line
first_column: lastToken[2].last_column
last_line: lastToken[2].last_line
last_column: lastToken[2].last_column
終了トークンをペアにし、リストされたすべてのトークンのペアがトークンストリーム全体で正しくバランスが取れていることを確認します。
pair: (tag) ->
[..., prev] = @ends
unless tag is wanted = prev?.tag
@error "unmatched #{tag}" unless 'OUTDENT' is wanted
[..., lastIndent] = @indents
@outdentToken lastIndent, true
return @pair tag
@ends.pop()
getLineAndColumnFromChunk: (offset) ->
if offset is 0
return [@chunkLine, @chunkColumn]
if offset >= @chunk.length
string = @chunk
else
string = @chunk[..offset-1]
lineCount = count string, '\n'
column = @chunkColumn
if lineCount > 0
[..., lastLine] = string.split '\n'
column = lastLine.length
else
column += string.length
[@chunkLine + lineCount, column]
「token」と同じですが、結果に追加せずにトークンを返すだけです。
makeToken: (tag, value, offsetInChunk = 0, length = value.length) ->
locationData = {}
[locationData.first_line, locationData.first_column] =
@getLineAndColumnFromChunk offsetInChunk
最後のオフセットにはlength - 1を使用します。last_lineとlast_columnを提供しているので、last_column == first_columnの場合、長さ1の文字を見ています。
lastCharacter = if length > 0 then (length - 1) else 0
[locationData.last_line, locationData.last_column] =
@getLineAndColumnFromChunk offsetInChunk + lastCharacter
token = [tag, value, locationData]
token
結果にトークンを追加します。offset
は、トークンが始まる現在の@chunkへのオフセットです。length
は、オフセット後の@chunk内のトークンの長さです。指定されていない場合、value
の長さが使用されます。
新しいトークンを返します。
token: (tag, value, offsetInChunk, length, origin) ->
token = @makeToken tag, value, offsetInChunk, length
token.origin = origin if origin
@tokens.push token
token
トークンストリームの最後のタグを覗き見ます。
tag: ->
[..., token] = @tokens
token?[0]
トークンストリームの最後の値を覗き見ます。
value: ->
[..., token] = @tokens
token?[1]
未完成の式の真っ只中にいますか?
unfinished: ->
LINE_CONTINUER.test(@chunk) or
@tag() in UNFINISHED
formatString: (str, options) ->
@replaceUnicodeCodePointEscapes str.replace(STRING_OMIT, '$1'), options
formatHeregex: (str) ->
@formatRegex str.replace(HEREGEX_OMIT, '$1$2'), delimiter: '///'
formatRegex: (str, options) ->
@replaceUnicodeCodePointEscapes str, options
unicodeCodePointToUnicodeEscapes: (codePoint) ->
toUnicodeEscape = (val) ->
str = val.toString 16
"\\u#{repeat '0', 4 - str.length}#{str}"
return toUnicodeEscape(codePoint) if codePoint < 0x10000
サロゲートペア
high = Math.floor((codePoint - 0x10000) / 0x400) + 0xD800
low = (codePoint - 0x10000) % 0x400 + 0xDC00
"#{toUnicodeEscape(high)}#{toUnicodeEscape(low)}"
文字列と正規表現で\u{…}を\uxxxx[\uxxxx]に置き換えます。
replaceUnicodeCodePointEscapes: (str, options) ->
str.replace UNICODE_CODE_POINT_ESCAPE, (match, escapedBackslash, codePointHex, offset) =>
return escapedBackslash if escapedBackslash
codePointDecimal = parseInt codePointHex, 16
if codePointDecimal > 0x10ffff
@error "unicode code point escapes greater than \\u{10ffff} are not allowed",
offset: offset + options.delimiter.length
length: codePointHex.length + 4
@unicodeCodePointToUnicodeEscapes codePointDecimal
文字列と正規表現のエスケープを検証します。
validateEscapes: (str, options = {}) ->
invalidEscapeRegex =
if options.isRegex
REGEX_INVALID_ESCAPE
else
STRING_INVALID_ESCAPE
match = invalidEscapeRegex.exec str
return unless match
[[], before, octal, hex, unicodeCodePoint, unicode] = match
message =
if octal
"octal escape sequences are not allowed"
else
"invalid escape sequence"
invalidEscape = "\\#{octal or hex or unicodeCodePoint or unicode}"
@error "#{message} #{invalidEscape}",
offset: (options.offsetInChunk ? 0) + match.index + before.length
length: invalidEscape.length
特定の文字をエスケープして文字列または正規表現を構築します。
makeDelimitedLiteral: (body, options = {}) ->
body = '(?:)' if body is '' and options.delimiter is '/'
regex = ///
(\\\\) # escaped backslash
| (\\0(?=[1-7])) # nul character mistaken as octal escape
| \\?(#{options.delimiter}) # (possibly escaped) delimiter
| \\?(?: (\n)|(\r)|(\u2028)|(\u2029) ) # (possibly escaped) newlines
| (\\.) # other escapes
///g
body = body.replace regex, (match, backslash, nul, delimiter, lf, cr, ls, ps, other) -> switch
エスケープされたバックスラッシュを無視します。
when backslash then (if options.double then backslash + backslash else backslash)
when nul then '\\x00'
when delimiter then "\\#{delimiter}"
when lf then '\\n'
when cr then '\\r'
when ls then '\\u2028'
when ps then '\\u2029'
when other then (if options.double then "\\#{other}" else other)
"#{options.delimiter}#{body}#{options.delimiter}"
現在のチャンクからの特定のオフセット、またはトークンの場所(token[2]
)でエラーをスローします。
error: (message, options = {}) ->
location =
if 'first_line' of options
options
else
[first_line, first_column] = @getLineAndColumnFromChunk options.offset ? 0
{first_line, first_column, last_column: first_column + (options.length ? 1) - 1}
throwSyntaxError message, location
isUnassignable = (name, displayName = name) -> switch
when name in [JS_KEYWORDS..., COFFEE_KEYWORDS...]
"keyword '#{displayName}' can't be assigned"
when name in STRICT_PROSCRIBED
"'#{displayName}' can't be assigned"
when name in RESERVED
"reserved word '#{displayName}' can't be assigned"
else
false
exports.isUnassignable = isUnassignable
from
はCoffeeScriptのキーワードではありませんが、import
とexport
ステートメント(上記で処理済み)とfor
ループの宣言行のように動作します。from
が変数識別子である場合と、この「時々」キーワードである場合を検出してみてください。
isForFrom = (prev) ->
if prev[0] is 'IDENTIFIER'
for i from from
、for from from iterable
if prev[1] is 'from'
prev[1][0] = 'IDENTIFIER'
yes
for i from iterable
yes
for from…
else if prev[0] is 'FOR'
no
for {from}…
、for [from]…
、for {a, from}…
、for {a: from}…
else if prev[1] in ['{', '[', ',', ':']
no
else
yes
CoffeeScriptとJavaScriptで共通するキーワード。
JS_KEYWORDS = [
'true', 'false', 'null', 'this'
'new', 'delete', 'typeof', 'in', 'instanceof'
'return', 'throw', 'break', 'continue', 'debugger', 'yield'
'if', 'else', 'switch', 'for', 'while', 'do', 'try', 'catch', 'finally'
'class', 'extends', 'super'
'import', 'export', 'default'
]
CoffeeScript専用のキーワード。
COFFEE_KEYWORDS = [
'undefined', 'Infinity', 'NaN'
'then', 'unless', 'until', 'loop', 'of', 'by', 'when'
]
COFFEE_ALIAS_MAP =
and : '&&'
or : '||'
is : '=='
isnt : '!='
not : '!'
yes : 'true'
no : 'false'
on : 'true'
off : 'false'
COFFEE_ALIASES = (key for key of COFFEE_ALIAS_MAP)
COFFEE_KEYWORDS = COFFEE_KEYWORDS.concat COFFEE_ALIASES
JavaScriptによって予約されているが使用されていない、またはCoffeeScriptで内部的に使用されているキーワードのリスト。実行時にJavaScriptエラーが発生しないように、これらが検出されたときにエラーをスローします。
RESERVED = [
'case', 'function', 'var', 'void', 'with', 'const', 'let', 'enum'
'native', 'implements', 'interface', 'package', 'private'
'protected', 'public', 'static'
]
STRICT_PROSCRIBED = ['arguments', 'eval']
JavaScriptのキーワードと予約語の両方の上位集合であり、識別子またはプロパティとして使用することはできません。
exports.JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED).concat(STRICT_PROSCRIBED)
BOMとして知られる厄介なMicrosoftの狂気の文字コード。
BOM = 65279
トークン照合正規表現。
IDENTIFIER = /// ^
(?!\d)
( (?: (?!\s)[$\w\x7f-\uffff] )+ )
( [^\n\S]* : (?!:) )? # Is this a property name?
///
NUMBER = ///
^ 0b[01]+ | # binary
^ 0o[0-7]+ | # octal
^ 0x[\da-f]+ | # hex
^ \d*\.?\d+ (?:e[+-]?\d+)? # decimal
///i
OPERATOR = /// ^ (
?: [-=]> # function
| [-+*/%<>&|^!?=]= # compound assign / compare
| >>>=? # zero-fill right shift
| ([-+:])\1 # doubles
| ([&|<>*/%])\2=? # logic / shift / power / floor division / modulo
| \?(\.|::) # soak access
| \.{2,3} # range or splat
) ///
WHITESPACE = /^[^\n\S]+/
COMMENT = /^###([^#][\s\S]*?)(?:###[^\n\S]*|###$)|^(?:\s*#(?!##[^#]).*)+/
CODE = /^[-=]>/
MULTI_DENT = /^(?:\n[^\n\S]*)+/
JSTOKEN = ///^ `(?!``) ((?: [^`\\] | \\[\s\S] )*) ` ///
HERE_JSTOKEN = ///^ ``` ((?: [^`\\] | \\[\s\S] | `(?!``) )*) ``` ///
文字列照合正規表現。
STRING_START = /^(?:'''|"""|'|")/
STRING_SINGLE = /// ^(?: [^\\'] | \\[\s\S] )* ///
STRING_DOUBLE = /// ^(?: [^\\"#] | \\[\s\S] | \#(?!\{) )* ///
HEREDOC_SINGLE = /// ^(?: [^\\'] | \\[\s\S] | '(?!'') )* ///
HEREDOC_DOUBLE = /// ^(?: [^\\"#] | \\[\s\S] | "(?!"") | \#(?!\{) )* ///
STRING_OMIT = ///
((?:\\\\)+) # consume (and preserve) an even number of backslashes
| \\[^\S\n]*\n\s* # remove escaped newlines
///g
SIMPLE_STRING_OMIT = /\s*\n\s*/g
HEREDOC_INDENT = /\n+([^\n\S]*)(?=\S)/g
正規表現照合正規表現。
REGEX = /// ^
/ (?!/) ((
?: [^ [ / \n \\ ] # every other thing
| \\[^\n] # anything but newlines escaped
| \[ # character class
(?: \\[^\n] | [^ \] \n \\ ] )*
\]
)*) (/)?
///
REGEX_FLAGS = /^\w*/
VALID_FLAGS = /^(?!.*(.).*\1)[imguy]*$/
HEREGEX = /// ^(?: [^\\/#] | \\[\s\S] | /(?!//) | \#(?!\{) )* ///
HEREGEX_OMIT = ///
((?:\\\\)+) # consume (and preserve) an even number of backslashes
| \\(\s) # preserve escaped whitespace
| \s+(?:#.*)? # remove whitespace and comments
///g
REGEX_ILLEGAL = /// ^ ( / | /{3}\s*) (\*) ///
POSSIBLY_DIVISION = /// ^ /=?\s ///
その他の正規表現。
HERECOMMENT_ILLEGAL = /\*\//
LINE_CONTINUER = /// ^ \s* (?: , | \??\.(?![.\d]) | :: ) ///
STRING_INVALID_ESCAPE = ///
( (?:^|[^\\]) (?:\\\\)* ) # make sure the escape isn’t escaped
\\ (
?: (0[0-7]|[1-7]) # octal escape
| (x(?![\da-fA-F]{2}).{0,2}) # hex escape
| (u\{(?![\da-fA-F]{1,}\})[^}]*\}?) # unicode code point escape
| (u(?!\{|[\da-fA-F]{4}).{0,4}) # unicode escape
)
///
REGEX_INVALID_ESCAPE = ///
( (?:^|[^\\]) (?:\\\\)* ) # make sure the escape isn’t escaped
\\ (
?: (0[0-7]) # octal escape
| (x(?![\da-fA-F]{2}).{0,2}) # hex escape
| (u\{(?![\da-fA-F]{1,}\})[^}]*\}?) # unicode code point escape
| (u(?!\{|[\da-fA-F]{4}).{0,4}) # unicode escape
)
///
UNICODE_CODE_POINT_ESCAPE = ///
( \\\\ ) # make sure the escape isn’t escaped
|
\\u\{ ( [\da-fA-F]+ ) \}
///g
LEADING_BLANK_LINE = /^[^\n\S]*\n/
TRAILING_BLANK_LINE = /\n[^\n\S]*$/
TRAILING_SPACES = /\s+$/
複合代入トークン。
COMPOUND_ASSIGN = [
'-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>='
'&=', '^=', '|=', '**=', '//=', '%%='
]
単項トークン。
UNARY = ['NEW', 'TYPEOF', 'DELETE', 'DO']
UNARY_MATH = ['!', '~']
ビットシフトトークン。
SHIFT = ['<<', '>>', '>>>']
比較トークン。
COMPARE = ['==', '!=', '<', '>', '<=', '>=']
数学的トークン。
MATH = ['*', '/', '%', '//', '%%']
not
接頭辞で否定できる関係トークン。
RELATION = ['IN', 'OF', 'INSTANCEOF']
ブールトークン。
BOOL = ['TRUE', 'FALSE']
正当に呼び出したり、インデックス付けしたりできるトークン。これらのトークンに続く開き括弧または角括弧は、関数呼び出しまたはインデックス付け操作の開始として記録されます。
CALLABLE = ['IDENTIFIER', 'PROPERTY', ')', ']', '?', '@', 'THIS', 'SUPER']
INDEXABLE = CALLABLE.concat [
'NUMBER', 'INFINITY', 'NAN', 'STRING', 'STRING_END', 'REGEX', 'REGEX_END'
'BOOL', 'NULL', 'UNDEFINED', '}', '::'
]
正規表現がすぐに続くことはない(ただし、場合によってはスペース付きのCALLABLEを除く)トークンですが、除算演算子が続く可能性があります。
参照:http://www-archive.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions
NOT_REGEX = INDEXABLE.concat ['++', '--']
WHEN
の直前にあり、WHEN
が行の先頭にあることを示すトークン。これらを末尾のwhenと区別して、文法の曖昧さを回避します。
LINE_BREAK = ['INDENT', 'OUTDENT', 'TERMINATOR']
これらの前の追加のインデントは無視されます。
INDENTABLE_CLOSERS = [')', '}', ']']
行の最後に表示され、後続のTERMINATOR/INDENTトークンを抑制するトークン。
UNFINISHED = ['\\', '.', '?.', '?::', 'UNARY', 'MATH', 'UNARY_MATH', '+', '-',
'**', 'SHIFT', 'RELATION', 'COMPARE', '&', '^', '|', '&&', '||',
'BIN?', 'THROW', 'EXTENDS', 'DEFAULT']