• ジャンプ先 … +
    browser.coffee cake.coffee coffee-script.coffee command.coffee grammar.coffee helpers.coffee index.coffee lexer.coffee nodes.coffee optparse.coffee register.coffee repl.coffee rewriter.coffee scope.litcoffee sourcemap.litcoffee
  • lexer.coffee

  • ¶

    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クラス

  • ¶
  • ¶

    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
  • ¶

    次の構文をサポートするためにINDENTを自動的に閉じます。

    el.click((event) ->
      el.hide())
    
          [..., lastIndent] = @indents
          @outdentToken lastIndent, true
          return @pair tag
        @ends.pop()
  • ¶

    ヘルパー

  • ¶
  • ¶

    現在のチャンクへのオフセットから行番号と列番号を返します。

    offsetは@chunkへの文字数です。

      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']