• ジャンプ先 … +
    browser.coffee cake.coffee coffeescript.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 Lexer。ソースコードの先頭に対して、一連のトークンマッチング正規表現を使用して一致を試みます。一致が見つかると、トークンが生成され、一致を消費し、再び開始します。トークンは次の形式です

    [tag, value, locationData]
    

    ここで、locationData は {first_line, first_column, last_line, last_column, last_line_exclusive, last_column_exclusive} で、Jison に直接フィードできる形式です。これらは、coffeescript.coffee で定義されている parser.lexer 関数で Jison によって読み取られます。

    {Rewriter, INVERSES, UNFINISHED} = require './rewriter'
  • §

    必要なヘルパーをインポートします。

    {count, starts, compact, repeat, invertLiterate, merge,
    attachCommentsToNode, locationDataToString, throwSyntaxError
    replaceUnicodeCodePointEscapes, flatten, parseNumber} = 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.
        @continuationLineAdditionalIndent = 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.
        @indentLiteral = ''          # The indentation.
        @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? ...`.
        @jsxDepth = 0                # Used to optimize JSX checks, how deep in JSX we are.
        @jsxObjAttribute = {}        # Used to detect if JSX attributes is wrapped in {} (<div {props...} />).
    
        @chunkLine =
          opts.line or 0             # The start line for the current @chunk.
        @chunkColumn =
          opts.column or 0           # The start column of the current @chunk.
        @chunkOffset =
          opts.offset or 0           # The start offset for the current @chunk.
        @locationDataCompensations =
          opts.locationDataCompensations or {} # The location data compensations for 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
               @jsxToken()        or
               @regexToken()      or
               @jsToken()         or
               @literalToken()
  • §

    位置を更新します。

          [@chunkLine, @chunkColumn, @chunkOffset] = @getLineAndColumnFromChunk consumed
    
          i += consumed
    
          return {@tokens, index: i} if opts.untilBalanced and @ends.length is 0
    
        @closeIndentation()
        @error "missing #{end.tag}", (end.origin ? end)[2] if end = @ends.pop()
        return @tokens if opts.rewrite is off
        (new Rewriter).rewrite @tokens
  • §

    先頭と末尾の空白、キャリッジリターンなどを削除するために、コードを前処理します。リテレート CoffeeScript を字句解析する場合は、少なくとも4つのスペースまたはタブでインデントされていないすべての行を削除することにより、外部 Markdown を削除します。

      clean: (code) ->
        thusFar = 0
        if code.charCodeAt(0) is BOM
          code = code.slice 1
          @locationDataCompensations[0] = 1
          thusFar += 1
        if WHITESPACE.test code
          code = "\n#{code}"
          @chunkLine--
          @locationDataCompensations[0] ?= 0
          @locationDataCompensations[0] -= 1
        code = code
          .replace /\r/g, (match, offset) =>
            @locationDataCompensations[thusFar + offset] = 1
            ''
          .replace TRAILING_SPACES, ''
        code = invertLiterate code if @literate
        code
  • §

    トークナイザー

  • §
  • §

    識別リテラル(変数、キーワード、メソッド名など)に一致します。JavaScript の予約語が識別子として使用されていないことを確認します。CoffeeScript は JavaScript で許可されている少数のキーワードを予約しているため、ここでプロパティ名として参照されている場合はキーワードとしてタグ付けしないように注意してください。そのため、is は === を意味しますが、jQuery.is() を実行できます。

      identifierToken: ->
        inJSXTag = @atJSXTag()
        regex = if inJSXTag then JSX_ATTRIBUTE else IDENTIFIER
        return 0 unless match = regex.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(yes) in COFFEE_KEYWORDS
            prev = @prev()
            [prev[0], prev[1]] = ['IDENTIFIER', @value(yes)]
          if @tag() in ['DEFAULT', 'IMPORT_ALL', 'IDENTIFIER']
            @token 'AS', id
            return id.length
        if id is 'as' and @seenExport
          if @tag() in ['IDENTIFIER', 'DEFAULT']
            @token 'AS', id
            return id.length
          if @value(yes) in COFFEE_KEYWORDS
            prev = @prev()
            [prev[0], prev[1]] = ['IDENTIFIER', @value(yes)]
            @token 'AS', id
            return id.length
        if id is 'default' and @seenExport and @tag() in ['EXPORT', 'AS']
          @token 'DEFAULT', id
          return id.length
        if id is 'assert' and (@seenImport or @seenExport) and @tag() is 'STRING'
          @token 'ASSERT', id
          return id.length
        if id is 'do' and regExSuper = /^(\s*super)(?!\(\))/.exec @chunk[3...]
          @token 'SUPER', 'super'
          @token 'CALL_START', '('
          @token 'CALL_END', ')'
          [input, sup] = regExSuper
          return sup.length + 3
    
        prev = @prev()
    
        tag =
          if colon or prev? and
             (prev[0] in ['.', '?.', '::', '?::'] or
             not prev.spaced and prev[0] is '@')
            'PROPERTY'
          else
            'IDENTIFIER'
    
        tokenData = {}
        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 = {endsLength: @ends.length}
          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()
                tokenData.invert = poppedToken.data?.original ? poppedToken[1]
        else if tag is 'IDENTIFIER' and @seenFor and id is 'from' and
           isForFrom(prev)
          tag = 'FORFROM'
          @seenFor = no
  • §

    get または set をキーワードとして使用しようとすると、または CoffeeScript が通常 get または set という名前の関数への呼び出しとして解釈するもの(つまり、get({foo: function () {}}))でエラーが発生します。

        else if tag is 'PROPERTY' and prev
          if prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) and
             @tokens.length > 1 and @tokens[@tokens.length - 2][0] not in ['.', '?.', '@']
            @error "'#{prev[1]}' cannot be used as a keyword, or as a function call
            without parentheses", prev[2]
          else if prev[0] is '.' and @tokens.length > 1 and (prevprev = @tokens[@tokens.length - 2])[0] is 'UNARY' and prevprev[1] is 'new'
            prevprev[0] = 'NEW_TARGET'
          else if prev[0] is '.' and @tokens.length > 1 and (prevprev = @tokens[@tokens.length - 2])[0] is 'IMPORT' and prevprev[1] is 'import'
            @seenImport = no
            prevprev[0] = 'IMPORT_META'
          else if @tokens.length > 2
            prevprev = @tokens[@tokens.length - 2]
            if prev[0] in ['@', 'THIS'] and prevprev and prevprev.spaced and
               /^[gs]et$/.test(prevprev[1]) and
               @tokens[@tokens.length - 3][0] not in ['.', '?.', '@']
              @error "'#{prevprev[1]}' cannot be used as a keyword, or as a
              function call without parentheses", prevprev[2]
    
        if tag is 'IDENTIFIER' and id in RESERVED and not inJSXTag
          @error "reserved word '#{id}'", length: id.length
    
        unless tag is 'PROPERTY' or @exportSpecifierList or @importSpecifierList
          if id in COFFEE_ALIASES
            alias = id
            id = COFFEE_ALIAS_MAP[id]
            tokenData.original = alias
          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, length: idLength, data: tokenData
        tagToken.origin = [tag, alias, tagToken[2]] if alias
        if poppedToken
          [tagToken[2].first_line, tagToken[2].first_column, tagToken[2].range[0]] =
            [poppedToken[2].first_line, poppedToken[2].first_column, poppedToken[2].range[0]]
        if colon
          colonOffset = input.lastIndexOf if inJSXTag then '=' else ':'
          colonToken = @token ':', ':', offset: colonOffset
          colonToken.jsxColon = yes if inJSXTag # used by rewriter
        if inJSXTag and tag is 'IDENTIFIER' and prev[0] isnt ':'
          @token ',', ',', length: 0, origin: tagToken, generated: yes
    
        input.length
  • §

    10 進数、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 /^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
    
        parsedValue = parseNumber number
        tokenData = {parsedValue}
    
        tag = if parsedValue is Infinity then 'INFINITY' else 'NUMBER'
        if tag is 'INFINITY'
          tokenData.original = number
        @token tag, number,
          length: lexedLength
          data: tokenData
        lexedLength
  • §

    複数行文字列を含む文字列、および補間ありまたはなしのヒアドキュメントに一致します。

      stringToken: ->
        [quote] = STRING_START.exec(@chunk) || []
        return 0 unless quote
  • §

    前のトークンが from で、これが import または export ステートメントの場合、from に適切にタグ付けします。

        prev = @prev()
        if prev and @value() is 'from' and (@seenImport or @seenExport)
          prev[0] = 'FROM'
    
        regex = switch quote
          when "'"   then STRING_SINGLE
          when '"'   then STRING_DOUBLE
          when "'''" then HEREDOC_SINGLE
          when '"""' then HEREDOC_DOUBLE
    
        {tokens, index: end} = @matchWithInterpolations regex, quote
    
        heredoc = quote.length is 3
        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
    
        delimiter = quote.charAt(0)
        @mergeInterpolationTokens tokens, {quote, indent, endOffset: end}, (value) =>
          @validateUnicodeCodePointEscapes value, delimiter: quote
    
        if @atJSXTag()
          @token ',', ',', length: 0, origin: @prev, generated: yes
    
        end
  • §

    コメントに一致し、消費します。コメントはトークンストリームから取り出され、後で保存され、すべてが解析され、JavaScript コードが生成された後に、出力に再挿入されます。

      commentToken: (chunk = @chunk, {heregex, returnCommentTokens = no, offsetInChunk = 0} = {}) ->
        return 0 unless match = chunk.match COMMENT
        [commentWithSurroundingWhitespace, hereLeadingWhitespace, hereComment, hereTrailingWhitespace, lineComment] = match
        contents = null
  • §

    このコメントは同じ行のコードの後に続きますか?

        leadingNewline = /^\s*\n+\s*#/.test commentWithSurroundingWhitespace
        if hereComment
          matchIllegal = HERECOMMENT_ILLEGAL.exec hereComment
          if matchIllegal
            @error "block comments cannot contain #{matchIllegal[0]}",
              offset: '###'.length + matchIllegal.index, length: matchIllegal[0].length
  • §

    このブロックコメントが存在しないかのように、インデントまたはアウトデントを解析します。

          chunk = chunk.replace "####{hereComment}###", ''
  • §

    不要な TERMINATOR トークンの作成を避けるため、Rewriter::removeLeadingNewlines のように、先頭の改行を削除します。

          chunk = chunk.replace /^\n+/, ''
          @lineToken {chunk}
  • §

    ### スタイルのコメントの内容を取り出して、フォーマットします。

          content = hereComment
          contents = [{
            content
            length: commentWithSurroundingWhitespace.length - hereLeadingWhitespace.length - hereTrailingWhitespace.length
            leadingWhitespace: hereLeadingWhitespace
          }]
        else
  • §

    COMMENT 正規表現は、連続する行コメントを1つのトークンとしてキャプチャします。最初のコメントの前にある改行は削除しますが、行コメント間の空白行は保持します。

          leadingNewlines = ''
          content = lineComment.replace /^(\n*)/, (leading) ->
            leadingNewlines = leading
            ''
          precedingNonCommentLines = ''
          hasSeenFirstCommentLine = no
          contents =
            content.split '\n'
            .map (line, index) ->
              unless line.indexOf('#') > -1
                precedingNonCommentLines += "\n#{line}"
                return
              leadingWhitespace = ''
              content = line.replace /^([ |\t]*)#/, (_, whitespace) ->
                leadingWhitespace = whitespace
                ''
              comment = {
                content
                length: '#'.length + content.length
                leadingWhitespace: "#{unless hasSeenFirstCommentLine then leadingNewlines else ''}#{precedingNonCommentLines}#{leadingWhitespace}"
                precededByBlankLine: !!precedingNonCommentLines
              }
              hasSeenFirstCommentLine = yes
              precedingNonCommentLines = ''
              comment
            .filter (comment) -> comment
    
        getIndentSize = ({leadingWhitespace, nonInitial}) ->
          lastNewlineIndex = leadingWhitespace.lastIndexOf '\n'
          if hereComment? or not nonInitial
            return null unless lastNewlineIndex > -1
          else
            lastNewlineIndex ?= -1
          leadingWhitespace.length - 1 - lastNewlineIndex
        commentAttachments = for {content, length, leadingWhitespace, precededByBlankLine}, i in contents
          nonInitial = i isnt 0
          leadingNewlineOffset = if nonInitial then 1 else 0
          offsetInChunk += leadingNewlineOffset + leadingWhitespace.length
          indentSize = getIndentSize {leadingWhitespace, nonInitial}
          noIndent = not indentSize? or indentSize is -1
          commentAttachment = {
            content
            here: hereComment?
            newLine: leadingNewline or nonInitial # Line comments after the first one start new lines, by definition.
            locationData: @makeLocationData {offsetInChunk, length}
            precededByBlankLine
            indentSize
            indented:  not noIndent and indentSize > @indent
            outdented: not noIndent and indentSize < @indent
          }
          commentAttachment.heregex = yes if heregex
          offsetInChunk += length
          commentAttachment
    
        prev = @prev()
        unless prev
  • §

    前のトークンがない場合は、このコメントを添付するためのプレースホルダートークンを作成します。改行が続きます。

          commentAttachments[0].newLine = yes
          @lineToken chunk: @chunk[commentWithSurroundingWhitespace.length..], offset: commentWithSurroundingWhitespace.length # Set the indent.
          placeholderToken = @makeToken 'JS', '', offset: commentWithSurroundingWhitespace.length, generated: yes
          placeholderToken.comments = commentAttachments
          @tokens.push placeholderToken
          @newlineToken commentWithSurroundingWhitespace.length
        else
          attachCommentsToNode commentAttachments, prev
    
        return commentAttachments if returnCommentTokens
        commentWithSurroundingWhitespace.length
  • §

    バックティックを介してソースに直接補間された JavaScript に一致します。

      jsToken: ->
        return 0 unless @chunk.charAt(0) is '`' and
          (match = (matchedHere = HERE_JSTOKEN.exec(@chunk)) or JSTOKEN.exec(@chunk))
  • §

    エスケープされたバックティックをバックティックに変換し、エスケープされたバックティックの直前にあるエスケープされたバックスラッシュをバックスラッシュに変換します

        script = match[1]
        {length} = match[0]
        @token 'JS', script, {length, data: {here: !!matchedHere}}
        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
            comments = []
            while matchedComment = HEREGEX_COMMENT.exec @chunk[0...index]
              {index: commentIndex} = matchedComment
              [fullMatch, leadingWhitespace, comment] = matchedComment
              comments.push {comment, offsetInChunk: commentIndex + leadingWhitespace.length}
            commentTokens = flatten(
              for commentOpts in comments
                @commentToken commentOpts.comment, Object.assign commentOpts, heregex: yes, returnCommentTokens: yes
            )
          when match = REGEX.exec @chunk
            [regex, body, closed] = match
            @validateEscapes body, isRegex: yes, offsetInChunk: 1
            index = regex.length
            prev = @prev()
            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, length: 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
            delimiter = if body then '/' else '///'
            body ?= tokens[0][1]
            @validateUnicodeCodePointEscapes body, {delimiter}
            @token 'REGEX', "/#{body}/#{flags}", {length: end, origin, data: {delimiter}}
          else
            @token 'REGEX_START', '(',    {length: 0, origin, generated: yes}
            @token 'IDENTIFIER', 'RegExp', length: 0, generated: yes
            @token 'CALL_START', '(',      length: 0, generated: yes
            @mergeInterpolationTokens tokens, {double: yes, heregex: {flags}, endOffset: end - flags.length, quote: '///'}, (str) =>
              @validateUnicodeCodePointEscapes str, {delimiter}
            if flags
              @token ',', ',',                    offset: index - 1, length: 0, generated: yes
              @token 'STRING', '"' + flags + '"', offset: index,     length: flags.length
            @token ')', ')',                      offset: end,       length: 0, generated: yes
            @token 'REGEX_END', ')',              offset: end,       length: 0, generated: yes
  • §

    heregex コメントを REGEX/REGEX_END トークンに明示的に添付します。

        if commentTokens?.length
          addTokenData @tokens[@tokens.length - 1],
            heregexCommentTokens: commentTokens
    
        end
  • §

    改行、インデント、およびアウトデントに一致し、どれがどれかを判断します。現在の行が次の行に続いていることを検出できる場合、改行は抑制されます

    elements
      .each( ... )
      .map( ... )
    

    単一のアウトデントトークンで複数のインデントを閉じることができるため、インデントのレベルを追跡します。そのため、どの程度インデントされているかを知る必要があります。

      lineToken: ({chunk = @chunk, offset = 0} = {}) ->
        return 0 unless match = MULTI_DENT.exec chunk
        indent = match[0]
    
        prev = @prev()
        backslash = prev?[0] is '\\'
        @seenFor = no unless (backslash or @seenFor?.endsLength < @ends.length) and @seenFor
        @seenImport = no unless (backslash and @seenImport) or @importSpecifierList
        @seenExport = no unless (backslash and @seenExport) or @exportSpecifierList
    
        size = indent.length - 1 - indent.lastIndexOf '\n'
        noNewlines = @unfinished()
    
        newIndentLiteral = if size > 0 then indent[-size..] else ''
        unless /^(.?)\1*$/.exec newIndentLiteral
          @error 'mixed indentation', offset: indent.length
          return indent.length
    
        minLiteralLength = Math.min newIndentLiteral.length, @indentLiteral.length
        if newIndentLiteral[...minLiteralLength] isnt @indentLiteral[...minLiteralLength]
          @error 'indentation mismatch', offset: indent.length
          return indent.length
    
        if size - @continuationLineAdditionalIndent is @indent
          if noNewlines then @suppressNewlines() else @newlineToken offset
          return indent.length
    
        if size > @indent
          if noNewlines
            @continuationLineAdditionalIndent = size - @indent unless backslash
            if @continuationLineAdditionalIndent
              prev.continuationLineIndent = @indent + @continuationLineAdditionalIndent
            @suppressNewlines()
            return indent.length
          unless @tokens.length
            @baseIndent = @indent = size
            @indentLiteral = newIndentLiteral
            return indent.length
          diff = size - @indent + @outdebt
          @token 'INDENT', diff, offset: offset + indent.length - size, length: size
          @indents.push diff
          @ends.push {tag: 'OUTDENT'}
          @outdebt = @continuationLineAdditionalIndent = 0
          @indent = size
          @indentLiteral = newIndentLiteral
        else if size < @baseIndent
          @error 'missing indentation', offset: offset + indent.length
        else
          endsContinuationLineIndentation = @continuationLineAdditionalIndent > 0
          @continuationLineAdditionalIndent = 0
          @outdentToken {moveOut: @indent - size, noNewlines, outdentLength: indent.length, offset, indentSize: size, endsContinuationLineIndentation}
        indent.length
  • §

    記録されたインデントをいくつか過ぎて内側に移動する場合、アウトデントトークンまたは複数のトークンを記録します。新しい @indent 値を設定します。

      outdentToken: ({moveOut, noNewlines, outdentLength = 0, offset = 0, indentSize, endsContinuationLineIndentation}) ->
        decreasedIndent = @indent - moveOut
        while moveOut > 0
          lastIndent = @indents[@indents.length - 1]
          if not lastIndent
            @outdebt = moveOut = 0
          else if @outdebt and moveOut <= @outdebt
            @outdebt -= moveOut
            moveOut   = 0
          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, length: outdentLength, indentSize: indentSize + moveOut - dent
            moveOut -= dent
        @outdebt -= moveOut if dent
        @suppressSemicolons()
    
        unless @tag() is 'TERMINATOR' or noNewlines
          terminatorToken = @token 'TERMINATOR', '\n', offset: offset + outdentLength, length: 0
          terminatorToken.endsContinuationLineIndentation = {preContinuationLineIndent: @indent} if endsContinuationLineIndentation
        @indent = decreasedIndent
        @indentLiteral = @indentLiteral[...decreasedIndent]
        this
  • §

    意味のない空白に一致し、消費します。前のトークンに「スペースあり」のタグを付けます。これは、違いが生じる場合があるためです。

      whitespaceToken: ->
        return 0 unless (match = WHITESPACE.exec @chunk) or
                        (nline = @chunk.charAt(0) is '\n')
        prev = @prev()
        prev[if match then 'spaced' else 'newLine'] = true if prev
        if match then match[0].length else 0
  • §

    改行トークンを生成します。連続する改行はマージされます。

      newlineToken: (offset) ->
        @suppressSemicolons()
        @token 'TERMINATOR', '\n', {offset, length: 0} unless @tag() is 'TERMINATOR'
        this
  • §

    行末に \ を使用して改行を抑制します。スラッシュはその仕事が完了したらここで削除されます。

      suppressNewlines: ->
        prev = @prev()
        if prev[1] is '\\'
          if prev.comments and @tokens.length > 1
  • §

    @tokens.length は少なくとも2つ(コード、次に \)である必要があります。何もない後に \ を配置すると、それに続くコメントが失われます。

            attachCommentsToNode prev.comments, @tokens[@tokens.length - 2]
          @tokens.pop()
        this
    
      jsxToken: ->
        firstChar = @chunk[0]
  • §

    前のトークンをチェックして、属性がスプレッドされているかどうかを検出します。

        prevChar = if @tokens.length > 0 then @tokens[@tokens.length - 1][0] else ''
        if firstChar is '<'
          match = JSX_IDENTIFIER.exec(@chunk[1...]) or JSX_FRAGMENT_IDENTIFIER.exec(@chunk[1...])
          return 0 unless match and (
            @jsxDepth > 0 or
  • §

    スペースのない比較の右側ではありません(つまり、a<b)。

            not (prev = @prev()) or
            prev.spaced or
            prev[0] not in COMPARABLE_LEFT_SIDE
          )
          [input, id] = match
          fullId = id
          if '.' in id
            [id, properties...] = id.split '.'
          else
            properties = []
          tagToken = @token 'JSX_TAG', id,
            length: id.length + 1
            data:
              openingBracketToken: @makeToken '<', '<'
              tagNameToken: @makeToken 'IDENTIFIER', id, offset: 1
          offset = id.length + 1
          for property in properties
            @token '.', '.', {offset}
            offset += 1
            @token 'PROPERTY', property, {offset}
            offset += property.length
          @token 'CALL_START', '(', generated: yes
          @token '[', '[', generated: yes
          @ends.push {tag: '/>', origin: tagToken, name: id, properties}
          @jsxDepth++
          return fullId.length + 1
        else if jsxTag = @atJSXTag()
          if @chunk[...2] is '/>' # Self-closing tag.
            @pair '/>'
            @token ']', ']',
              length: 2
              generated: yes
            @token 'CALL_END', ')',
              length: 2
              generated: yes
              data:
                selfClosingSlashToken: @makeToken '/', '/'
                closingBracketToken: @makeToken '>', '>', offset: 1
            @jsxDepth--
            return 2
          else if firstChar is '{'
            if prevChar is ':'
  • §

    このトークンは、式である JSX 属性値の開始を表します(例:<div a={b} /> の {b})。私たちの文法は式の始まりを ( トークンとして表すため、これを { として表示される ( トークンにします。

              token = @token '(', '{'
              @jsxObjAttribute[@jsxDepth] = no
  • §

    タグ属性名を JSX としてタグ付けします

              addTokenData @tokens[@tokens.length - 3],
                jsx: yes
            else
              token = @token '{', '{'
              @jsxObjAttribute[@jsxDepth] = yes
            @ends.push {tag: '}', origin: token}
            return 1
          else if firstChar is '>' # end of opening tag
  • §

    タグ内のターミネータを無視します。

            {origin: openingTagToken} = @pair '/>' # As if the current tag was self-closing.
            @token ']', ']',
              generated: yes
              data:
                closingBracketToken: @makeToken '>', '>'
            @token ',', 'JSX_COMMA', generated: yes
            {tokens, index: end} =
              @matchWithInterpolations INSIDE_JSX, '>', '</', JSX_INTERPOLATION
            @mergeInterpolationTokens tokens, {endOffset: end, jsx: yes}, (value) =>
              @validateUnicodeCodePointEscapes value, delimiter: '>'
            match = JSX_IDENTIFIER.exec(@chunk[end...]) or JSX_FRAGMENT_IDENTIFIER.exec(@chunk[end...])
            if not match or match[1] isnt "#{jsxTag.name}#{(".#{property}" for property in jsxTag.properties).join ''}"
              @error "expected corresponding JSX closing tag for #{jsxTag.name}",
                jsxTag.origin.data.tagNameToken[2]
            [, fullTagName] = match
            afterTag = end + fullTagName.length
            if @chunk[afterTag] isnt '>'
              @error "missing closing > after tag name", offset: afterTag, length: 1
  • §

    開始 </ に -2/+2、終了 > に +1。

            endToken = @token 'CALL_END', ')',
              offset: end - 2
              length: fullTagName.length + 3
              generated: yes
              data:
                closingTagOpeningBracketToken: @makeToken '<', '<', offset: end - 2
                closingTagSlashToken: @makeToken '/', '/', offset: end - 1
  • §

    TODO:複雑なタグ名に個別のトークンを使用しますか?例:< / A . B >

                closingTagNameToken: @makeToken 'IDENTIFIER', fullTagName, offset: end
                closingTagClosingBracketToken: @makeToken '>', '>', offset: end + fullTagName.length
  • §

    終了タグの位置データを文法でよりアクセスしやすくします

            addTokenData openingTagToken, endToken.data
            @jsxDepth--
            return afterTag + 1
          else
            return 0
        else if @atJSXTag 1
          if firstChar is '}'
            @pair firstChar
            if @jsxObjAttribute[@jsxDepth]
              @token '}', '}'
              @jsxObjAttribute[@jsxDepth] = no
            else
              @token ')', '}'
            @token ',', ',', generated: yes
            return 1
          else
            return 0
        else
          return 0
    
      atJSXTag: (depth = 0) ->
        return no if @jsxDepth is 0
        i = @ends.length - 1
        i-- while @ends[i]?.tag is 'OUTDENT' or depth-- > 0 # Ignore indents.
        last = @ends[i]
        last?.tag is '/>' and last
  • §

    他のすべての単一文字はトークンとして扱います。例:( ) , . ! 複数文字の演算子もリテラルトークンであるため、Jison は適切な演算の順序を割り当てることができます。ここで特別にタグ付けする記号がいくつかあります。; と改行はどちらも TERMINATOR として扱われ、メソッド呼び出しを示す括弧と通常の括弧を区別します。

      literalToken: ->
        if match = OPERATOR.exec @chunk
          [value] = match
          @tagParameters() if CODE.test value
        else
          value = @chunk.charAt 0
        tag  = value
        prev = @prev()
    
        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.data.original += '=' if prev.data?.original
            prev[2].range = [
              prev[2].range[0]
              prev[2].range[1] + 1
            ]
            prev[2].last_column += 1
            prev[2].last_column_exclusive += 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 prev?[0] is 'IMPORT'
          prev[0] = 'DYNAMIC_IMPORT'
    
        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 ';'
          @error 'unexpected ;' if prev?[0] in ['=', UNFINISHED...]
          @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
          if value is '(' and not prev.spaced 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 and not prev.spaced) or
             (prev[0] is '::')) # `.prototype` can’t be a method you can call.
            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 @makeToken tag, value
        value.length
  • §

    トークンマニピュレータ

  • §
  • §

    以前は、文法のあいまいさの原因は、関数定義のパラメータリストと関数呼び出しの引数リストでした。パーサーにとって物事を容易にするために、パラメータに特別にタグを付けて後方に歩きます。

      tagParameters: ->
        return @tagDoIife() if @tag() isnt ')'
        stack = []
        {tokens} = this
        i = tokens.length
        paramEndToken = tokens[--i]
        paramEndToken[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 @tagDoIife i - 1
              else
                paramEndToken[0] = 'CALL_END'
                return this
        this
  • §

    異なる文法の優先順位を許可するために、識別子などに続く do とは異なる方法で、関数に続く do にタグを付けます

      tagDoIife: (tokenIndex) ->
        tok = @tokens[tokenIndex ? @tokens.length - 1]
        return this unless tok?[0] is 'DO'
        tok[0] = 'DO_IIFE'
        this
  • §

    ファイルの終わりに、残りのすべての開いているブロックを閉じます。

      closeIndentation: ->
        @outdentToken moveOut: @indent, indentSize: 0
  • §

    区切り文字で囲まれたトークンの内容に一致し、任意の式の置換に Ruby ライクな表記法を使用して、内部の変数と式を展開します。

    "Hello #{name.capitalize()}."
    

    補間に遭遇した場合、このメソッドは再帰的に新しい Lexer を作成し、#{ の { が } とバランスが取れるまでトークン化します。

    • regex はトークンの内容に一致します(ただし、delimiter には一致せず、補間が望まれる場合は #{ には一致しません)。
    • delimiter はトークンの区切り文字です。例としては、'、"、'''、"""、/// があります。
    • closingDelimiter は JSX でのみ delimiter とは異なります
    • interpolators は補間の開始に一致します。JSX の場合は { と < の両方です(つまり、ネストされた JSX タグ)。

    このメソッドを使用すると、文字列内の補間内に文字列を無限に持つことができます。

      matchWithInterpolations: (regex, delimiter, closingDelimiter = delimiter, interpolators = /^#\{/) ->
        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, offset: offsetInChunk
    
          str = str[strPart.length..]
          offsetInChunk += strPart.length
    
          break unless match = interpolators.exec str
          [interpolator] = match
  • §

    #{ の # を削除するには。

          interpolationOffset = interpolator.length - 1
          [line, column, offset] = @getLineAndColumnFromChunk offsetInChunk + interpolationOffset
          rest = str[interpolationOffset..]
          {tokens: nested, index} =
            new Lexer().tokenize rest, {line, column, offset, untilBalanced: on, @locationDataCompensations}
  • §

    #{ の # を考慮に入れます。

          index += interpolationOffset
    
          braceInterpolator = str[index - 1] is '}'
          if braceInterpolator
  • §

    先頭と末尾の { と } を括弧に変えます。不要な括弧は後で削除されます。

            [open, ..., close] = nested
            open[0]  = 'INTERPOLATION_START'
            open[1]  = '('
            open[2].first_column -= interpolationOffset
            open[2].range = [
              open[2].range[0] - interpolationOffset
              open[2].range[1]
            ]
            close[0]  = 'INTERPOLATION_END'
            close[1] = ')'
            close.origin = ['', 'end of interpolation', close[2]]
  • §

    先頭の 'TERMINATOR'(存在する場合)を削除します。

          nested.splice 1, 1 if nested[1]?[0] is 'TERMINATOR'
  • §

    末尾の 'INDENT'/'OUTDENT' ペア(存在する場合)を削除します。

          nested.splice -3, 2 if nested[nested.length - 3]?[0] is 'INDENT' and nested[nested.length - 2][0] is 'OUTDENT'
    
          unless braceInterpolator
  • §

    { と } を使用していないため、代わりに補間されたトークンをラップします。

            open = @makeToken 'INTERPOLATION_START', '(', offset: offsetInChunk,         length: 0, generated: yes
            close = @makeToken 'INTERPOLATION_END', ')',  offset: offsetInChunk + index, length: 0, generated: yes
            nested = [open, nested..., close]
  • §

    後で実際のトークンに変換される偽の 'TOKENS' トークンをプッシュします。

          tokens.push ['TOKENS', nested]
    
          str = str[index..]
          offsetInChunk += index
    
        unless str[...closingDelimiter.length] is closingDelimiter
          @error "missing #{closingDelimiter}", length: delimiter.length
    
        {tokens, index: offsetInChunk + closingDelimiter.length}
  • §

    偽のトークンタイプ 'TOKENS' および 'NEOSTRING'(matchWithInterpolations によって返される)の配列 tokens をトークンストリームにマージします。'NEOSTRING' の値は最初に fn を使用して変換され、options を使用して文字列に変換されます。

      mergeInterpolationTokens: (tokens, options, fn) ->
        {quote, indent, double, heregex, endOffset, jsx} = options
    
        if tokens.length > 1
          lparen = @token 'STRING_START', '(', length: quote?.length ? 0, data: {quote}, generated: not quote?.length
    
        firstIndex = @tokens.length
        $ = tokens.length - 1
        for token, i in tokens
          [tag, value] = token
          switch tag
            when 'TOKENS'
  • §

    この補間にはコメント(およびそれ以外のもの)があります。

              if value.length is 2 and (value[0].comments or value[1].comments)
                placeholderToken = @makeToken 'JS', '', generated: yes
  • §

    最初の括弧と同じ位置データを使用します。

                placeholderToken[2] = value[0][2]
                for val in value when val.comments
                  placeholderToken.comments ?= []
                  placeholderToken.comments.push val.comments...
                value.splice 1, 0, placeholderToken
  • §

    偽の 'TOKENS' トークン内のすべてのトークンをプッシュします。これらはすでに適切な位置データを持っています。

              locationToken = value[0]
              tokensToPush = value
            when 'NEOSTRING'
  • §

    'NEOSTRING' を 'STRING' に変換します。

              converted = fn.call this, token[1], i
              addTokenData token, initialChunk: yes if i is 0
              addTokenData token, finalChunk: yes   if i is $
              addTokenData token, {indent, quote, double}
              addTokenData token, {heregex} if heregex
              addTokenData token, {jsx} if jsx
              token[0] = 'STRING'
              token[1] = '"' + converted + '"'
              if tokens.length is 1 and quote?
                token[2].first_column -= quote.length
                if token[1].substr(-2, 1) is '\n'
                  token[2].last_line += 1
                  token[2].last_column = quote.length - 1
                else
                  token[2].last_column += quote.length
                  token[2].last_column -= 1 if token[1].length is 2
                token[2].last_column_exclusive += quote.length
                token[2].range = [
                  token[2].range[0] - quote.length
                  token[2].range[1] + quote.length
                ]
              locationToken = token
              tokensToPush = [token]
          @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
            last_line_exclusive:   lastToken[2].last_line_exclusive
            last_column_exclusive: lastToken[2].last_column_exclusive
            range: [
              lparen[2].range[0]
              lastToken[2].range[1]
            ]
          ]
          lparen[2] = lparen.origin[2] unless quote?.length
          rparen = @token 'STRING_END', ')', offset: endOffset - (quote ? '').length, length: quote?.length ? 0, generated: not quote?.length
  • §

    終了トークンをペアリングし、リストされているすべてのトークンペアがトークンストリーム全体で正しくバランスされていることを確認します。

      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 moveOut: lastIndent, noNewlines: true
          return @pair tag
        @ends.pop()
  • §

    ヘルパー

  • §
  • §

    最初に削除したもの(例:キャリッジリターン)を補正して、位置データが元のソースファイルに対して正確なままになるようにします。

      getLocationDataCompensation: (start, end) ->
        totalCompensation = 0
        initialEnd = end
        current = start
        while current <= end
          break if current is end and start isnt initialEnd
          compensation = @locationDataCompensations[current]
          if compensation?
            totalCompensation += compensation
            end += compensation
          current++
        return totalCompensation
  • §

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

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

      getLineAndColumnFromChunk: (offset) ->
        compensation = @getLocationDataCompensation @chunkOffset, @chunkOffset + offset
    
        if offset is 0
          return [@chunkLine, @chunkColumn + compensation, @chunkOffset + compensation]
    
        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
          previousLinesCompensation = @getLocationDataCompensation @chunkOffset, @chunkOffset + offset - column
  • §

    最初に挿入された改行を再補正しないでください。

          previousLinesCompensation = 0 if previousLinesCompensation < 0
          columnCompensation = @getLocationDataCompensation(
            @chunkOffset + offset + previousLinesCompensation - column
            @chunkOffset + offset + previousLinesCompensation
          )
        else
          column += string.length
          columnCompensation = compensation
    
        [@chunkLine + lineCount, column + columnCompensation, @chunkOffset + offset + compensation]
    
      makeLocationData: ({ offsetInChunk, length }) ->
        locationData = range: []
        [locationData.first_line, locationData.first_column, locationData.range[0]] =
          @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, endOffset] =
          @getLineAndColumnFromChunk offsetInChunk + lastCharacter
        [locationData.last_line_exclusive, locationData.last_column_exclusive] =
          @getLineAndColumnFromChunk offsetInChunk + lastCharacter + (if length > 0 then 1 else 0)
        locationData.range[1] = if length > 0 then endOffset + 1 else endOffset
    
        locationData
  • §

    token と同じですが、これは結果に追加せずにトークンを返すだけです。

      makeToken: (tag, value, {offset: offsetInChunk = 0, length = value.length, origin, generated, indentSize} = {}) ->
        token = [tag, value, @makeLocationData {offsetInChunk, length}]
        token.origin = origin if origin
        token.generated = yes if generated
        token.indentSize = indentSize if indentSize?
        token
  • §

    結果にトークンを追加します。offset は、トークンが開始される現在の @chunk へのオフセットです。length は、オフセット後の @chunk 内のトークンの長さです。指定しない場合、value の長さが使用されます。

    新しいトークンを返します。

      token: (tag, value, {offset, length, origin, data, generated, indentSize} = {}) ->
        token = @makeToken tag, value, {offset, length, origin, generated, indentSize}
        addTokenData token, data if data
        @tokens.push token
        token
  • §

    トークンストリームの最後のタグを覗き見ます。

      tag: ->
        [..., token] = @tokens
        token?[0]
  • §

    トークンストリームの最後の値を覗き見します。

      value: (useOrigin = no) ->
        [..., token] = @tokens
        if useOrigin and token?.origin?
          token.origin[1]
        else
          token?[1]
  • §

    トークンストリームの前のトークンを取得します。

      prev: ->
        @tokens[@tokens.length - 1]
  • §

    未完了の式の中間にあるかどうか。

      unfinished: ->
        LINE_CONTINUER.test(@chunk) or
        @tag() in UNFINISHED
    
      validateUnicodeCodePointEscapes: (str, options) ->
        replaceUnicodeCodePointEscapes str, merge options, {@error}
  • §

    文字列と正規表現のエスケープを検証します。

      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
    
      suppressSemicolons: ->
        while @value() is ';'
          @tokens.pop()
          @error 'unexpected ;' if @prev()?[0] in ['=', UNFINISHED...]
  • §

    現在のチャンクから指定されたオフセット、またはトークン(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) ->
  • §

    for i from iterable

      if prev[0] is 'IDENTIFIER'
        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
    
    addTokenData = (token, data) ->
      Object.assign (token.data ?= {}), data
  • §

    定数

  • §
  • §

    CoffeeScriptがJavaScriptと共通して持つキーワード。

    JS_KEYWORDS = [
      'true', 'false', 'null', 'this'
      'new', 'delete', 'typeof', 'in', 'instanceof'
      'return', 'throw', 'break', 'continue', 'debugger', 'yield', 'await'
      '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?
    ///
  • §

    IDENTIFIERと同様ですが、-を含みます。

    JSX_IDENTIFIER_PART = /// (?: (?!\s)[\-$\w\x7f-\uffff] )+ ///.source
  • §

    https://facebook.github.io/jsx/ の仕様では、JSXElementNameはJSXIdentifier、JSXNamespacedName(JSXIdentifier : JSXIdentifier)、またはJSXMemberExpression(.で接続された2つ以上のJSXIdentifier)にすることができます。

    JSX_IDENTIFIER = /// ^
      (?![\d<]) # Must not start with `<`.
      ( #{JSX_IDENTIFIER_PART}
        (?: \s* : \s* #{JSX_IDENTIFIER_PART}       # JSXNamespacedName
        | (?: \s* \. \s* #{JSX_IDENTIFIER_PART} )+ # JSXMemberExpression
        )? )
    ///
  • §

    フラグメント: <></>

    JSX_FRAGMENT_IDENTIFIER = /// ^
      ()> # Ends immediately with `>`.
    ///
  • §

    https://facebook.github.io/jsx/ の仕様では、JSXAttributeNameはJSXIdentifierまたはJSXNamespacedName(JSXIdentifier : JSXIdentifier)のいずれかです。

    JSX_ATTRIBUTE = /// ^
      (?!\d)
      ( #{JSX_IDENTIFIER_PART}
        (?: \s* : \s* #{JSX_IDENTIFIER_PART}       # JSXNamespacedName
        )? )
      ( [^\S]* = (?!=) )?  # Is this an attribute with a value?
    ///
    
    NUMBER     = ///
      ^ 0b[01](?:_?[01])*n?                         | # binary
      ^ 0o[0-7](?:_?[0-7])*n?                       | # octal
      ^ 0x[\da-f](?:_?[\da-f])*n?                   | # hex
      ^ \d+(?:_\d+)*n                               | # decimal bigint
      ^ (?:\d+(?:_\d+)*)?      \.? \d+(?:_\d+)*       # decimal
                         (?:e[+-]? \d+(?:_\d+)* )?
    
  • §

    参考までに、数値リテラルセパレーターをサポートしない10進数:\d*.?\d+ (?:e[+-]?\d+)?

    ///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\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] | "(?!"") | \#(?!\{) )* ///
    
    INSIDE_JSX = /// ^(?:
        [^
          \{ # Start of CoffeeScript interpolation.
          <  # Maybe JSX tag (`<` not allowed even if bare).
        ]
      )* /// # Similar to `HEREDOC_DOUBLE` but there is no escaping.
    JSX_INTERPOLATION = /// ^(?:
          \{       # CoffeeScript interpolation.
        | <(?!/)   # JSX opening tag.
      )///
    
    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)[gimsuy]*$/
    
    HEREGEX      = /// ^
      (?:
    
  • §

    以下で特別な処理が必要な文字を除く、任意の文字に一致します。

          [^\\/#\s]
  • §

    \に続く任意の文字に一致します。

        | \\[\s\S]
  • §

    ///を除く任意の/に一致します。

        | /(?!//)
  • §

    補間の一部ではない#(例:#{})に一致します。

        | \#(?!\{)
  • §

    コメントは、///を含む、行末までのすべてを消費します。

        | \s+(?:#(?!\{).*)?
      )*
    ///
    
    HEREGEX_COMMENT = /(\s+)(#(?!{).*)/gm
    
    REGEX_ILLEGAL = /// ^ ( / | /{3}\s*) (\*) ///
    
    POSSIBLY_DIVISION   = /// ^ /=?\s ///
  • §

    その他の正規表現。

    HERECOMMENT_ILLEGAL = /\*\//
    
    LINE_CONTINUER      = /// ^ \s* (?: , | \??\.(?![.\d]) | \??:: ) ///
    
    STRING_INVALID_ESCAPE = ///
      ( (?:^|[^\\]) (?:\\\\)* )        # Make sure the escape isn’t escaped.
      \\ (
         ?: (0\d|[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\d)                      # 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
      )
    ///
    
    TRAILING_SPACES     = /\s+$/
  • §

    複合代入トークン。

    COMPOUND_ASSIGN = [
      '-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>='
      '&=', '^=', '|=', '**=', '//=', '%%='
    ]
  • §

    単項トークン。

    UNARY = ['NEW', 'TYPEOF', 'DELETE']
    
    UNARY_MATH = ['!', '~']
  • §

    ビットシフトトークン。

    SHIFT = ['<<', '>>', '>>>']
  • §

    比較トークン。

    COMPARE = ['==', '!=', '<', '>', '<=', '>=']
  • §

    数学トークン。

    MATH = ['*', '/', '%', '//', '%%']
  • §

    notプレフィックスで否定できる関係トークン。

    RELATION = ['IN', 'OF', 'INSTANCEOF']
  • §

    ブールトークン。

    BOOL = ['TRUE', 'FALSE']
  • §

    正当に呼び出しまたはインデックス化できるトークン。これらのトークンの後に続く開き括弧または角括弧は、関数呼び出しまたはインデックス操作の開始として記録されます。

    CALLABLE  = ['IDENTIFIER', 'PROPERTY', ')', ']', '?', '@', 'THIS', 'SUPER', 'DYNAMIC_IMPORT']
    INDEXABLE = CALLABLE.concat [
      'NUMBER', 'INFINITY', 'NAN', 'STRING', 'STRING_END', 'REGEX', 'REGEX_END'
      'BOOL', 'NULL', 'UNDEFINED', '}', '::'
    ]
  • §

    a<bのように、小なり比較の左辺になることができるトークン。

    COMPARABLE_LEFT_SIDE = ['IDENTIFIER', ')', ']', 'NUMBER']
  • §

    正規表現が直後に続くことは決してない(場合によってはスペースで区切られた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 = [')', '}', ']']