From 6786bab2ba0768f426373e818ac3c7fa20626d75 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 18 Mar 2013 19:23:05 +0800 Subject: [PATCH] Big refactor of SourceMap class. Literate CoffeeScript. Purdy. --- documentation/docs/browser.html | 2 +- documentation/docs/cake.html | 2 +- documentation/docs/coffee-script.html | 19 +- documentation/docs/command.html | 2 +- documentation/docs/docco.css | 10 +- documentation/docs/grammar.html | 2 +- documentation/docs/helpers.html | 2 +- documentation/docs/index.html | 2 +- documentation/docs/lexer.html | 2 +- documentation/docs/nodes.html | 4 +- documentation/docs/optparse.html | 2 +- documentation/docs/repl.html | 2 +- documentation/docs/rewriter.html | 2 +- documentation/docs/scope.html | 2 +- documentation/docs/sourcemap.html | 466 +++++++++----------------- lib/coffee-script/coffee-script.js | 20 +- lib/coffee-script/sourcemap.js | 330 ++++++++---------- src/coffee-script.coffee | 17 +- src/sourcemap.coffee | 256 -------------- src/sourcemap.litcoffee | 188 +++++++++++ test/sourcemap.coffee | 37 +- 21 files changed, 545 insertions(+), 824 deletions(-) delete mode 100644 src/sourcemap.coffee create mode 100644 src/sourcemap.litcoffee diff --git a/documentation/docs/browser.html b/documentation/docs/browser.html index dc451de2..563659c6 100644 --- a/documentation/docs/browser.html +++ b/documentation/docs/browser.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee diff --git a/documentation/docs/cake.html b/documentation/docs/cake.html index dd1a055c..83351657 100644 --- a/documentation/docs/cake.html +++ b/documentation/docs/cake.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee diff --git a/documentation/docs/coffee-script.html b/documentation/docs/coffee-script.html index e69af1df..9ed3f4c2 100644 --- a/documentation/docs/coffee-script.html +++ b/documentation/docs/coffee-script.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee @@ -124,7 +124,7 @@ child_process = require 'child_process' {Lexer} = require './lexer' {parser} = require './parser' helpers = require './helpers' -sourcemap = require './sourcemap' +SourceMap = require './sourcemap' @@ -171,7 +171,7 @@ sourcemap = require './sourcemap'

If options.sourceMap is specified, then options.filename must also be specified. All -options that can be passed to generateV3SourceMap() may also be passed here. +options that can be passed to SourceMap#generate may also be passed here.

This returns a javascript string, unless options.sourceMap is passed, @@ -186,7 +186,7 @@ lookups. {merge} = exports.helpers if options.sourceMap - sourceMap = new sourcemap.SourceMap() + map = new SourceMap fragments = (parser.parse lexer.tokenize(code, options)).compileToFragments options @@ -210,9 +210,9 @@ lookups. -

    if sourceMap
+            
    if options.sourceMap
       if fragment.locationData
-        sourceMap.addMapping(
+        map.add(
           [fragment.locationData.first_line, fragment.locationData.first_column],
           [currentLine, currentColumn],
           {noReplace: true})
@@ -242,9 +242,8 @@ lookups.
 
   if options.sourceMap
     answer = {js}
-    if sourceMap
-      answer.sourceMap = sourceMap
-      answer.v3SourceMap = sourcemap.generateV3SourceMap(sourceMap, options, code)
+    answer.sourceMap = map
+    answer.v3SourceMap = map.generate(options, code)
     answer
   else
     js
@@ -710,7 +709,7 @@ exception is thrown in the module body.) getSourceMapping = (filename, line, column) -> sourceMap = mainModule._sourceMaps[filename] - answer = sourceMap.getSourcePosition [line - 1, column - 1] if sourceMap + answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap if answer then [answer[0] + 1, answer[1] + 1] else null frames = for frame in stack diff --git a/documentation/docs/command.html b/documentation/docs/command.html index 3addd25d..e1a7efc9 100644 --- a/documentation/docs/command.html +++ b/documentation/docs/command.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee
diff --git a/documentation/docs/docco.css b/documentation/docs/docco.css index f690a079..6eedee3e 100644 --- a/documentation/docs/docco.css +++ b/documentation/docs/docco.css @@ -51,9 +51,17 @@ b, strong { font-family: "aller-bold"; } -p, ul, ol { +p { margin: 15px 0 0px; } + .annotation ul, .annotation ol { + margin: 25px 0; + } + .annotation ul li, .annotation ol li { + font-size: 14px; + line-height: 18px; + margin: 10px 0; + } h1, h2, h3, h4, h5, h6 { color: #112233; diff --git a/documentation/docs/grammar.html b/documentation/docs/grammar.html index d624f534..e8cc1e64 100644 --- a/documentation/docs/grammar.html +++ b/documentation/docs/grammar.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee
diff --git a/documentation/docs/helpers.html b/documentation/docs/helpers.html index a582288f..b362692b 100644 --- a/documentation/docs/helpers.html +++ b/documentation/docs/helpers.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee diff --git a/documentation/docs/index.html b/documentation/docs/index.html index 40f8ad68..9ee66be1 100644 --- a/documentation/docs/index.html +++ b/documentation/docs/index.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee diff --git a/documentation/docs/lexer.html b/documentation/docs/lexer.html index 5e44d4e1..c6bd6529 100644 --- a/documentation/docs/lexer.html +++ b/documentation/docs/lexer.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee diff --git a/documentation/docs/nodes.html b/documentation/docs/nodes.html index ce4779ed..99ba7e52 100644 --- a/documentation/docs/nodes.html +++ b/documentation/docs/nodes.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee @@ -881,7 +881,7 @@ declarations of all inner variables pushed up to the top. if assigns fragments.push @makeCode ",\n#{@tab + TAB}" if declars fragments.push @makeCode (scope.assignedVariables().join ",\n#{@tab + TAB}") - fragments.push @makeCode ';\n\n' + fragments.push @makeCode ";\n#{if @spaced then '\n' else ''}" fragments.concat post diff --git a/documentation/docs/optparse.html b/documentation/docs/optparse.html index 3b45ce4d..843fd133 100644 --- a/documentation/docs/optparse.html +++ b/documentation/docs/optparse.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee diff --git a/documentation/docs/repl.html b/documentation/docs/repl.html index 5f3ec91c..73afcf29 100644 --- a/documentation/docs/repl.html +++ b/documentation/docs/repl.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee diff --git a/documentation/docs/rewriter.html b/documentation/docs/rewriter.html index ca2a985c..36929eb0 100644 --- a/documentation/docs/rewriter.html +++ b/documentation/docs/rewriter.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee diff --git a/documentation/docs/scope.html b/documentation/docs/scope.html index 09bc3fc1..634afdbc 100644 --- a/documentation/docs/scope.html +++ b/documentation/docs/scope.html @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee diff --git a/documentation/docs/sourcemap.html b/documentation/docs/sourcemap.html index 4a6f03af..98f826fe 100644 --- a/documentation/docs/sourcemap.html +++ b/documentation/docs/sourcemap.html @@ -2,7 +2,7 @@ - sourcemap.coffee + sourcemap.litcoffee @@ -85,7 +85,7 @@ - sourcemap.coffee + sourcemap.litcoffee @@ -96,7 +96,7 @@
  • -

    sourcemap.coffee

    +

    sourcemap.litcoffee

  • @@ -105,18 +105,21 @@
  • -
    +
    -

    LineMapping

    -

    Hold data about mappings for one line of generated source code. +

    Source maps allow JavaScript runtimes to match running JavaScript back to +the original CoffeeScript source code that corresponds to it. In order to +produce maps, we must keep track of positions (line number, column number) +for every construct in the syntax tree, and be able to generate a map file +-- which is a compact, VLQ-encoded representation of the JSON serialization +of this information -- to write out alongside the generated JavaScript.

    -
    class LineMapping
    -  constructor: (@generatedLine) ->
    +
    {merge} = require './helpers'
  • @@ -124,16 +127,13 @@
  • -
    +
    -

    columnMap keeps track of which columns we've already mapped. -

    +

    LineMap

    -
        @columnMap = {}
    -
  • @@ -143,15 +143,24 @@
    -

    columnMappings is an array of all column mappings, sorted by generated-column. +

    Keeps track of information about column positions within a single line of +output JavaScript code. SourceMaps are implemented in terms of LineMaps. +

    -
        @columnMappings = []
    +            
    class LineMap
    +  constructor: (@line) ->
    +    @columns = []
     
    -  addMapping: (generatedColumn, [sourceLine, sourceColumn], options={}) ->
    -    if @columnMap[generatedColumn] and options.noReplace
    + add: (column, [sourceLine, sourceColumn], options={}) -> + return if @columns[column] and options.noReplace + @columns[column] = {line: @line, column, sourceLine, sourceColumn} + + sourceLocation: (column) -> + column-- until (mapping = @columns[column]) or (column <= 0) + mapping and [mapping.sourceLine, mapping.sourceColumn]
    @@ -159,60 +168,37 @@
  • -
    +
    -

    We already have a mapping for this column. -

    +

    SourceMap

    -
          return
    -
    -    @columnMap[generatedColumn] = {
    -      generatedLine: @generatedLine
    -      generatedColumn
    -      sourceLine
    -      sourceColumn
    -    }
    -
    -    @columnMappings.push @columnMap[generatedColumn]
    -    @columnMappings.sort (a,b) -> a.generatedColumn - b.generatedColumn
    -
    -  getSourcePosition: (generatedColumn) ->
    -    answer = null
    -    lastColumnMapping = null
    -    for columnMapping in @columnMappings
    -      if columnMapping.generatedColumn > generatedColumn
    -        break
    -      else
    -        lastColumnMapping = columnMapping
    -    if lastColumnMapping
    -      answer = [lastColumnMapping.sourceLine, lastColumnMapping.sourceColumn]
    -
  • -
    +
    -

    SourceMap

    -

    Maps locations in a generated source file back to locations in the original source file. +

    Maps locations in for a single generated JavaScript file back to locations in +the original CoffeeScript source file.

    -

    This is intentionally agnostic towards how a source map might be represented on disk. A -SourceMap can be converted to a "v3" style sourcemap with #generateV3SourceMap(), for example -but the SourceMap class itself knows nothing about v3 source maps. +

    This is intentionally agnostic towards how a source map might be represented on +disk. Once the compiler is ready to produce a "v3"-style source map, we can walk +through the arrays of line and column buffer to produce it.

    -
    class exports.SourceMap
    -  constructor: () ->
    +
    class SourceMap
    +  constructor: ->
    +    @lines = []
  • @@ -223,12 +209,19 @@ but the SourceMap class itself knows nothing about v3 source maps.
    -

    generatedLines is an array of LineMappings, one per generated line. +

    Adds a mapping to this SourceMap. sourceLocation and generatedLocation +are both [line, column] arrays. If options.noReplace is true, then if there +is already a mapping for the specified line and column, this will have no +effect. +

    -
        @generatedLines = []
    +
      add: (sourceLocation, generatedLocation, options = {}) ->
    +    [line, column] = generatedLocation
    +    lineMap = (@lines[line] or= new LineMap(line))
    +    lineMap.add column, sourceLocation, options
    @@ -239,26 +232,16 @@ but the SourceMap class itself knows nothing about v3 source maps.
    -

    Adds a mapping to this SourceMap. +

    Look up the original position of a given line and column in the generated +code. -

    -

    sourceLocation and generatedLocation are both [line, column] arrays. - -

    -

    If options.noReplace is true, then if there is already a mapping for -the specified generatedLine and generatedColumn, this will have no effect.

    -
      addMapping: (sourceLocation, generatedLocation, options={}) ->
    -    [generatedLine, generatedColumn] = generatedLocation
    -
    -    lineMapping = @generatedLines[generatedLine]
    -    if not lineMapping
    -      lineMapping = @generatedLines[generatedLine] = new LineMapping(generatedLine)
    -
    -    lineMapping.addMapping generatedColumn, sourceLocation, options
    +
      sourceLocation: ([line, column]) ->
    +    line-- until (lineMap = @lines[line]) or (line <= 0)
    +    lineMap and lineMap.sourceLocation column
    @@ -269,15 +252,19 @@ the specified generatedLine and generatedColumn, this
    -

    Returns [sourceLine, sourceColumn], or null if no mapping could be found. +

    func will be called once for every recorded mapping, in the order in +which they occur in the generated source. fn will be passed an object +with four properties: sourceLine, sourceColumn, line, and +column. +

    -
      getSourcePosition: ([generatedLine, generatedColumn]) ->
    -    answer = null
    -    lineMapping = @generatedLines[generatedLine]
    -    if not lineMapping
    +
      each: (iterator) ->
    +    for lineMap, lineNumber in @lines when lineMap
    +      for mapping in lineMap.columns when mapping
    +        iterator mapping
    @@ -285,19 +272,13 @@ the specified generatedLine and generatedColumn, this
  • -
    +
    -

    TODO: Search backwards for the line? -

    +

    V3 SourceMap Generation

    -
        else
    -      answer = lineMapping.getSourcePosition generatedColumn
    -
    -    answer
    -
  • @@ -307,19 +288,29 @@ the specified generatedLine and generatedColumn, this
    -

    fn will be called once for every recorded mapping, in the order in -which they occur in the generated source. fn will be passed an object -with four properties: sourceLine, sourceColumn, generatedLine, and -generatedColumn. +

    Builds up a V3 source map, returning the generated JSON as a string. +options.sourceRoot may be used to specify the sourceRoot written to the source +map. Also, options.sourceFiles and options.generatedFile may be passed to +set "sources" and "file", respectively. +

    -
      forEachMapping: (fn) ->
    -    for lineMapping, generatedLineNumber in @generatedLines
    -      if lineMapping
    -        for columnMapping in lineMapping.columnMappings
    -          fn(columnMapping)
    +
      generate: (options = {}, code = null) ->
    +    writingline       = 0
    +    lastColumn        = 0
    +    lastSourceLine    = 0
    +    lastSourceColumn  = 0
    +    needComma         = no
    +    buffer            = ""
    +
    +    @each (mapping) =>
    +      while writingline < mapping.line
    +        lastColumn = 0
    +        needComma = no
    +        buffer += ";"
    +        writingline++
    @@ -327,41 +318,18 @@ generatedColumn.
  • -
    +
    -

    generateV3SourceMap

    -

    Builds a V3 source map from a SourceMap object. -Returns the generated JSON as a string. - -

    -

    options.sourceRoot may be used to specify the sourceRoot written to the source map. Also, -options.sourceFiles and options.generatedFile may be passed to set "sources" and "file", -respectively. Note that sourceFiles must be an array. +

    Write a comma if we've already written a segment on this line.

    -
    exports.generateV3SourceMap = (sourceMap, options={}, code) ->
    -  sourceRoot = options.sourceRoot or ""
    -  sourceFiles = options.sourceFiles or [""]
    -  generatedFile = options.generatedFile or ""
    -
    -  writingGeneratedLine = 0
    -  lastGeneratedColumnWritten = 0
    -  lastSourceLineWritten = 0
    -  lastSourceColumnWritten = 0
    -  needComma = no
    -
    -  mappings = ""
    -
    -  sourceMap.forEachMapping (mapping) ->
    -    while writingGeneratedLine < mapping.generatedLine
    -      lastGeneratedColumnWritten = 0
    -      needComma = no
    -      mappings += ";"
    -      writingGeneratedLine++
    +
          if needComma
    +        buffer += ","
    +        needComma = no
  • @@ -372,14 +340,19 @@ respectively. Note that sourceFiles must be an array.
    -

    Write a comma if we've already written a segment on this line. +

    Write the next segment. Segments can be 1, 4, or 5 values. If just one, then it +is a generated column which doesn't match anything in the source code. + +

    +

    The starting column in the generated source, relative to any previous recorded +column for the current line: +

    -
        if needComma
    -      mappings += ","
    -      needComma = no
    +
          buffer += @encodeVlq mapping.column - lastColumn
    +      lastColumn = mapping.column
    @@ -390,26 +363,13 @@ respectively. Note that sourceFiles must be an array.
    -

    Write the next segment. -Segments can be 1, 4, or 5 values. If just one, then it is a generated column which -doesn't match anything in the source code. +

    The index into the list of sources: -

    -

    Fields are all zero-based, and relative to the previous occurence unless otherwise noted: - starting-column in generated source, relative to previous occurence for the current line. - index into the "sources" list - starting line in the original source - starting column in the original source - * index into the "names" list associated with this segment. - -

    -

    Add the generated start-column

    -
        mappings += exports.vlqEncodeValue(mapping.generatedColumn - lastGeneratedColumnWritten)
    -    lastGeneratedColumnWritten = mapping.generatedColumn
    +
          buffer += @encodeVlq 0
    @@ -420,12 +380,16 @@ doesn't match anything in the source code.
    -

    Add the index into the sources list +

    The starting line in the original source, relative to the previous source line. +

    -
        mappings += exports.vlqEncodeValue(0)
    +
          buffer += @encodeVlq mapping.sourceLine - lastSourceLine
    +      if lastSourceLine isnt mapping.sourceLine
    +        lastSourceLine = mapping.sourceLine
    +        lastSourceColumn = 0
    @@ -436,13 +400,15 @@ doesn't match anything in the source code.
    -

    Add the source start-line +

    The starting column in the original source, relative to the previous column. +

    -
        mappings += exports.vlqEncodeValue(mapping.sourceLine - lastSourceLineWritten)
    -    lastSourceLineWritten = mapping.sourceLine
    +
          buffer += @encodeVlq mapping.sourceColumn - lastSourceColumn
    +      lastSourceColumn = mapping.sourceColumn
    +      needComma = yes
    @@ -453,13 +419,23 @@ doesn't match anything in the source code.
    -

    Add the source start-column +

    Produce the canonical JSON object format for a "v3" source map. +

    -
        mappings += exports.vlqEncodeValue(mapping.sourceColumn - lastSourceColumnWritten)
    -    lastSourceColumnWritten = mapping.sourceColumn
    +
        v3 =
    +      version:    3
    +      file:       options.generatedFile or ''
    +      sourceRoot: options.sourceRoot or ''
    +      sources:    options.sourceFiles or ['']
    +      names:      []
    +      mappings:   buffer
    +
    +    v3.sourcesContent = [code] if options.inline
    +
    +    return JSON.stringify v3, null, 2
    @@ -467,29 +443,13 @@ doesn't match anything in the source code.
  • -
    +
    -

    TODO: Do we care about symbol names for CoffeeScript? Probably not. - -

    +

    Base64 VLQ Encoding

    -
        needComma = yes
    -
    -  answer = {
    -    version: 3
    -    file: generatedFile
    -    sourceRoot
    -    sources: sourceFiles
    -    names: []
    -    mappings
    -  }
    -  answer.sourcesContent = [code] if options.inline
    -
    -  return JSON.stringify answer, null, 2
    -
  • @@ -499,13 +459,22 @@ doesn't match anything in the source code.
    -

    Load a SourceMap from a JSON string. Returns the SourceMap object. +

    Note that SourceMap VLQ encoding is "backwards". MIDI-style VLQ encoding puts +the most-significant-bit (MSB) from the original value into the MSB of the VLQ +encoded value (see Wikipedia). +SourceMap VLQ does things the other way around, with the least significat four +bits of the original value encoded into the first byte of the VLQ encoded value. +

    -
    exports.loadV3SourceMap = (sourceMap) ->
    -  todo()
    +
      VLQ_SHIFT            = 5
    +  VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT             # 0010 0000
    +  VLQ_VALUE_MASK       = VLQ_CONTINUATION_BIT - 1   # 0001 1111
    +
    +  encodeVlq: (value) ->
    +    answer = ''
    @@ -513,28 +482,15 @@ doesn't match anything in the source code.
  • -
    +
    -

    Base64 encoding helpers

    +

    Least significant bit represents the sign. +

    -
    BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    -MAX_BASE64_VALUE = BASE64_CHARS.length - 1
    -
    -encodeBase64Char = (value) ->
    -  if value > MAX_BASE64_VALUE
    -    throw new Error "Cannot encode value #{value} > #{MAX_BASE64_VALUE}"
    -  else if value < 0
    -    throw new Error "Cannot encode value #{value} < 0"
    -  BASE64_CHARS[value]
    -
    -decodeBase64Char = (char) ->
    -  value = BASE64_CHARS.indexOf char
    -  if value == -1
    -    throw new Error "Invalid Base 64 character: #{char}"
    -  value
    +
        signBit = if value < 0 then 1 else 0
  • @@ -542,23 +498,15 @@ MAX_BASE64_VALUE = BASE64_CHARS.length - 1
  • -
    +
    -

    Base 64 VLQ encoding/decoding helpers

    -

    Note that SourceMap VLQ encoding is "backwards". MIDI style VLQ encoding puts the -most-significant-bit (MSB) from the original value into the MSB of the VLQ encoded value -(see http://en.wikipedia.org/wiki/File:Uintvar_coding.svg). SourceMap VLQ does things -the other way around, with the least significat four bits of the original value encoded -into the first byte of the VLQ encoded value. - +

    The next bits are the actual value.

    -
    VLQ_SHIFT      = 5
    -VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT # 0010 0000
    -VLQ_VALUE_MASK       = VLQ_CONTINUATION_BIT - 1 # 0001 1111
    +
        valueToEncode = (Math.abs(value) << 1) + signBit
  • @@ -569,12 +517,18 @@ VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1 -

    Encode a value as Base 64 VLQ. +

    Make sure we encode at least one character, even if valueToEncode is 0.

    -
    exports.vlqEncodeValue = (value) ->
    +
        while valueToEncode or not answer
    +      nextChunk = valueToEncode & VLQ_VALUE_MASK
    +      valueToEncode = valueToEncode >> VLQ_SHIFT
    +      nextChunk |= VLQ_CONTINUATION_BIT if valueToEncode
    +      answer += @encodeBase64 nextChunk
    +
    +    return answer
    @@ -582,16 +536,13 @@ VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1
    -
    +
    -

    Least significant bit represents the sign. -

    +

    Regular Base64 Encoding

    -
      signBit = if value < 0 then 1 else 0
    - @@ -601,14 +552,13 @@ VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1
    -

    Next bits are the actual value -

    - + -
      valueToEncode = (Math.abs(value) << 1) + signBit
    +            
      BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
     
    -  answer = ""
    + encodeBase64: (value) -> + BASE64_CHARS[value] or throw new Error "Cannot Base64 encode value: #{value}"
    @@ -619,111 +569,13 @@ VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1 -

    Make sure we encode at least one character, even if valueToEncode is 0. +

    Our API for source maps is just the SourceMap class. +

    -
      while valueToEncode || !answer
    -    nextVlqChunk = valueToEncode & VLQ_VALUE_MASK
    -    valueToEncode = valueToEncode >> VLQ_SHIFT
    -
    -    if valueToEncode
    -      nextVlqChunk |= VLQ_CONTINUATION_BIT
    -
    -    answer += encodeBase64Char(nextVlqChunk)
    -
    -  return answer
    - - - - -
  • -
    - -
    - -
    -

    Decode a Base 64 VLQ value. - -

    -

    Returns [value, consumed] where value is the decoded value, and consumed is the number -of characters consumed from str. -

    - -
    - -
    exports.vlqDecodeValue = (str, offset=0) ->
    -  position = offset
    -  done = false
    -
    -  value = 0
    -  continuationShift = 0
    -
    -  while !done
    -    nextVlqChunk = decodeBase64Char(str[position])
    -    position += 1
    -
    -    nextChunkValue = nextVlqChunk & VLQ_VALUE_MASK
    -    value += (nextChunkValue << continuationShift)
    -
    -    if !(nextVlqChunk & VLQ_CONTINUATION_BIT)
    - -
  • - - -
  • -
    - -
    - -
    -

    We'll be done after this character. -

    - -
    - -
          done = true
    - -
  • - - -
  • -
    - -
    - -
    -

    Bits are encoded least-significant first (opposite of MIDI VLQ). Increase the -continuationShift, so the next byte will end up where it should in the value. -

    - -
    - -
        continuationShift += VLQ_SHIFT
    -
    -  consumed = position - offset
    - -
  • - - -
  • -
    - -
    - -
    -

    Least significant bit represents the sign. -

    - -
    - -
      signBit = value & 1
    -  value = value >> 1
    -
    -  if signBit then value = -value
    -
    -  return [value, consumed]
    +
    module.exports = SourceMap
  • diff --git a/lib/coffee-script/coffee-script.js b/lib/coffee-script/coffee-script.js index f57a0243..7ea99c33 100644 --- a/lib/coffee-script/coffee-script.js +++ b/lib/coffee-script/coffee-script.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 1.6.2 (function() { - var Lexer, child_process, compile, ext, fork, formatSourcePosition, fs, helpers, lexer, loadFile, parser, patchStackTrace, patched, path, sourcemap, vm, _i, _len, _ref, + var Lexer, SourceMap, child_process, compile, ext, fork, formatSourcePosition, fs, helpers, lexer, loadFile, parser, patchStackTrace, patched, path, vm, _i, _len, _ref, __hasProp = {}.hasOwnProperty; fs = require('fs'); @@ -17,20 +17,20 @@ helpers = require('./helpers'); - sourcemap = require('./sourcemap'); + SourceMap = require('./sourcemap'); exports.VERSION = '1.6.2'; exports.helpers = helpers; exports.compile = compile = function(code, options) { - var answer, currentColumn, currentLine, fragment, fragments, header, js, merge, newLines, sourceMap, _i, _len; + var answer, currentColumn, currentLine, fragment, fragments, header, js, map, merge, newLines, _i, _len; if (options == null) { options = {}; } merge = exports.helpers.merge; if (options.sourceMap) { - sourceMap = new sourcemap.SourceMap(); + map = new SourceMap; } fragments = (parser.parse(lexer.tokenize(code, options))).compileToFragments(options); currentLine = 0; @@ -41,9 +41,9 @@ js = ""; for (_i = 0, _len = fragments.length; _i < _len; _i++) { fragment = fragments[_i]; - if (sourceMap) { + if (options.sourceMap) { if (fragment.locationData) { - sourceMap.addMapping([fragment.locationData.first_line, fragment.locationData.first_column], [currentLine, currentColumn], { + map.add([fragment.locationData.first_line, fragment.locationData.first_column], [currentLine, currentColumn], { noReplace: true }); } @@ -61,10 +61,8 @@ answer = { js: js }; - if (sourceMap) { - answer.sourceMap = sourceMap; - answer.v3SourceMap = sourcemap.generateV3SourceMap(sourceMap, options, code); - } + answer.sourceMap = map; + answer.v3SourceMap = map.generate(options, code); return answer; } else { return js; @@ -255,7 +253,7 @@ var answer, sourceMap; sourceMap = mainModule._sourceMaps[filename]; if (sourceMap) { - answer = sourceMap.getSourcePosition([line - 1, column - 1]); + answer = sourceMap.sourceLocation([line - 1, column - 1]); } if (answer) { return [answer[0] + 1, answer[1] + 1]; diff --git a/lib/coffee-script/sourcemap.js b/lib/coffee-script/sourcemap.js index 7fbde858..c2a0c72e 100644 --- a/lib/coffee-script/sourcemap.js +++ b/lib/coffee-script/sourcemap.js @@ -1,238 +1,180 @@ // Generated by CoffeeScript 1.6.2 (function() { - var BASE64_CHARS, LineMapping, MAX_BASE64_VALUE, VLQ_CONTINUATION_BIT, VLQ_SHIFT, VLQ_VALUE_MASK, decodeBase64Char, encodeBase64Char; + var LineMap, SourceMap, merge; - LineMapping = (function() { - function LineMapping(generatedLine) { - this.generatedLine = generatedLine; - this.columnMap = {}; - this.columnMappings = []; + merge = require('./helpers').merge; + + LineMap = (function() { + function LineMap(line) { + this.line = line; + this.columns = []; } - LineMapping.prototype.addMapping = function(generatedColumn, _arg, options) { + LineMap.prototype.add = function(column, _arg, options) { var sourceColumn, sourceLine; sourceLine = _arg[0], sourceColumn = _arg[1]; if (options == null) { options = {}; } - if (this.columnMap[generatedColumn] && options.noReplace) { + if (this.columns[column] && options.noReplace) { return; } - this.columnMap[generatedColumn] = { - generatedLine: this.generatedLine, - generatedColumn: generatedColumn, + return this.columns[column] = { + line: this.line, + column: column, sourceLine: sourceLine, sourceColumn: sourceColumn }; - this.columnMappings.push(this.columnMap[generatedColumn]); - return this.columnMappings.sort(function(a, b) { - return a.generatedColumn - b.generatedColumn; - }); }; - LineMapping.prototype.getSourcePosition = function(generatedColumn) { - var answer, columnMapping, lastColumnMapping, _i, _len, _ref; - answer = null; - lastColumnMapping = null; - _ref = this.columnMappings; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - columnMapping = _ref[_i]; - if (columnMapping.generatedColumn > generatedColumn) { - break; - } else { - lastColumnMapping = columnMapping; - } - } - if (lastColumnMapping) { - return answer = [lastColumnMapping.sourceLine, lastColumnMapping.sourceColumn]; + LineMap.prototype.sourceLocation = function(column) { + var mapping; + while (!((mapping = this.columns[column]) || (column <= 0))) { + column--; } + return mapping && [mapping.sourceLine, mapping.sourceColumn]; }; - return LineMapping; + return LineMap; })(); - exports.SourceMap = (function() { + SourceMap = (function() { + var BASE64_CHARS, VLQ_CONTINUATION_BIT, VLQ_SHIFT, VLQ_VALUE_MASK; + function SourceMap() { - this.generatedLines = []; + this.lines = []; } - SourceMap.prototype.addMapping = function(sourceLocation, generatedLocation, options) { - var generatedColumn, generatedLine, lineMapping; + SourceMap.prototype.add = function(sourceLocation, generatedLocation, options) { + var column, line, lineMap, _base; if (options == null) { options = {}; } - generatedLine = generatedLocation[0], generatedColumn = generatedLocation[1]; - lineMapping = this.generatedLines[generatedLine]; - if (!lineMapping) { - lineMapping = this.generatedLines[generatedLine] = new LineMapping(generatedLine); - } - return lineMapping.addMapping(generatedColumn, sourceLocation, options); + line = generatedLocation[0], column = generatedLocation[1]; + lineMap = ((_base = this.lines)[line] || (_base[line] = new LineMap(line))); + return lineMap.add(column, sourceLocation, options); }; - SourceMap.prototype.getSourcePosition = function(_arg) { - var answer, generatedColumn, generatedLine, lineMapping; - generatedLine = _arg[0], generatedColumn = _arg[1]; - answer = null; - lineMapping = this.generatedLines[generatedLine]; - if (!lineMapping) { + SourceMap.prototype.sourceLocation = function(_arg) { + var column, line, lineMap; + line = _arg[0], column = _arg[1]; + while (!((lineMap = this.lines[line]) || (line <= 0))) { + line--; + } + return lineMap && lineMap.sourceLocation(column); + }; - } else { - answer = lineMapping.getSourcePosition(generatedColumn); + SourceMap.prototype.each = function(iterator) { + var lineMap, lineNumber, mapping, _i, _len, _ref, _results; + _ref = this.lines; + _results = []; + for (lineNumber = _i = 0, _len = _ref.length; _i < _len; lineNumber = ++_i) { + lineMap = _ref[lineNumber]; + if (lineMap) { + _results.push((function() { + var _j, _len1, _ref1, _results1; + _ref1 = lineMap.columns; + _results1 = []; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + mapping = _ref1[_j]; + if (mapping) { + _results1.push(iterator(mapping)); + } + } + return _results1; + })()); + } + } + return _results; + }; + + SourceMap.prototype.generate = function(options, code) { + var buffer, lastColumn, lastSourceColumn, lastSourceLine, needComma, v3, writingline, + _this = this; + if (options == null) { + options = {}; + } + if (code == null) { + code = null; + } + writingline = 0; + lastColumn = 0; + lastSourceLine = 0; + lastSourceColumn = 0; + needComma = false; + buffer = ""; + this.each(function(mapping) { + while (writingline < mapping.line) { + lastColumn = 0; + needComma = false; + buffer += ";"; + writingline++; + } + if (needComma) { + buffer += ","; + needComma = false; + } + buffer += _this.encodeVlq(mapping.column - lastColumn); + lastColumn = mapping.column; + buffer += _this.encodeVlq(0); + buffer += _this.encodeVlq(mapping.sourceLine - lastSourceLine); + if (lastSourceLine !== mapping.sourceLine) { + lastSourceLine = mapping.sourceLine; + lastSourceColumn = 0; + } + buffer += _this.encodeVlq(mapping.sourceColumn - lastSourceColumn); + lastSourceColumn = mapping.sourceColumn; + return needComma = true; + }); + v3 = { + version: 3, + file: options.generatedFile || '', + sourceRoot: options.sourceRoot || '', + sources: options.sourceFiles || [''], + names: [], + mappings: buffer + }; + if (options.inline) { + v3.sourcesContent = [code]; + } + return JSON.stringify(v3, null, 2); + }; + + VLQ_SHIFT = 5; + + VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT; + + VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1; + + SourceMap.prototype.encodeVlq = function(value) { + var answer, nextChunk, signBit, valueToEncode; + answer = ''; + signBit = value < 0 ? 1 : 0; + valueToEncode = (Math.abs(value) << 1) + signBit; + while (valueToEncode || !answer) { + nextChunk = valueToEncode & VLQ_VALUE_MASK; + valueToEncode = valueToEncode >> VLQ_SHIFT; + if (valueToEncode) { + nextChunk |= VLQ_CONTINUATION_BIT; + } + answer += this.encodeBase64(nextChunk); } return answer; }; - SourceMap.prototype.forEachMapping = function(fn) { - var columnMapping, generatedLineNumber, lineMapping, _i, _len, _ref, _results; - _ref = this.generatedLines; - _results = []; - for (generatedLineNumber = _i = 0, _len = _ref.length; _i < _len; generatedLineNumber = ++_i) { - lineMapping = _ref[generatedLineNumber]; - if (lineMapping) { - _results.push((function() { - var _j, _len1, _ref1, _results1; - _ref1 = lineMapping.columnMappings; - _results1 = []; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - columnMapping = _ref1[_j]; - _results1.push(fn(columnMapping)); - } - return _results1; - })()); - } else { - _results.push(void 0); - } - } - return _results; + BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + SourceMap.prototype.encodeBase64 = function(value) { + return BASE64_CHARS[value] || (function() { + throw new Error("Cannot Base64 encode value: " + value); + })(); }; return SourceMap; })(); - exports.generateV3SourceMap = function(sourceMap, options, code) { - var answer, generatedFile, lastGeneratedColumnWritten, lastSourceColumnWritten, lastSourceLineWritten, mappings, needComma, sourceFiles, sourceRoot, writingGeneratedLine; - if (options == null) { - options = {}; - } - sourceRoot = options.sourceRoot || ""; - sourceFiles = options.sourceFiles || [""]; - generatedFile = options.generatedFile || ""; - writingGeneratedLine = 0; - lastGeneratedColumnWritten = 0; - lastSourceLineWritten = 0; - lastSourceColumnWritten = 0; - needComma = false; - mappings = ""; - sourceMap.forEachMapping(function(mapping) { - while (writingGeneratedLine < mapping.generatedLine) { - lastGeneratedColumnWritten = 0; - needComma = false; - mappings += ";"; - writingGeneratedLine++; - } - if (needComma) { - mappings += ","; - needComma = false; - } - mappings += exports.vlqEncodeValue(mapping.generatedColumn - lastGeneratedColumnWritten); - lastGeneratedColumnWritten = mapping.generatedColumn; - mappings += exports.vlqEncodeValue(0); - mappings += exports.vlqEncodeValue(mapping.sourceLine - lastSourceLineWritten); - lastSourceLineWritten = mapping.sourceLine; - mappings += exports.vlqEncodeValue(mapping.sourceColumn - lastSourceColumnWritten); - lastSourceColumnWritten = mapping.sourceColumn; - return needComma = true; - }); - answer = { - version: 3, - file: generatedFile, - sourceRoot: sourceRoot, - sources: sourceFiles, - names: [], - mappings: mappings - }; - if (options.inline) { - answer.sourcesContent = [code]; - } - return JSON.stringify(answer, null, 2); - }; - - exports.loadV3SourceMap = function(sourceMap) { - return todo(); - }; - - BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - - MAX_BASE64_VALUE = BASE64_CHARS.length - 1; - - encodeBase64Char = function(value) { - if (value > MAX_BASE64_VALUE) { - throw new Error("Cannot encode value " + value + " > " + MAX_BASE64_VALUE); - } else if (value < 0) { - throw new Error("Cannot encode value " + value + " < 0"); - } - return BASE64_CHARS[value]; - }; - - decodeBase64Char = function(char) { - var value; - value = BASE64_CHARS.indexOf(char); - if (value === -1) { - throw new Error("Invalid Base 64 character: " + char); - } - return value; - }; - - VLQ_SHIFT = 5; - - VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT; - - VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1; - - exports.vlqEncodeValue = function(value) { - var answer, nextVlqChunk, signBit, valueToEncode; - signBit = value < 0 ? 1 : 0; - valueToEncode = (Math.abs(value) << 1) + signBit; - answer = ""; - while (valueToEncode || !answer) { - nextVlqChunk = valueToEncode & VLQ_VALUE_MASK; - valueToEncode = valueToEncode >> VLQ_SHIFT; - if (valueToEncode) { - nextVlqChunk |= VLQ_CONTINUATION_BIT; - } - answer += encodeBase64Char(nextVlqChunk); - } - return answer; - }; - - exports.vlqDecodeValue = function(str, offset) { - var consumed, continuationShift, done, nextChunkValue, nextVlqChunk, position, signBit, value; - if (offset == null) { - offset = 0; - } - position = offset; - done = false; - value = 0; - continuationShift = 0; - while (!done) { - nextVlqChunk = decodeBase64Char(str[position]); - position += 1; - nextChunkValue = nextVlqChunk & VLQ_VALUE_MASK; - value += nextChunkValue << continuationShift; - if (!(nextVlqChunk & VLQ_CONTINUATION_BIT)) { - done = true; - } - continuationShift += VLQ_SHIFT; - } - consumed = position - offset; - signBit = value & 1; - value = value >> 1; - if (signBit) { - value = -value; - } - return [value, consumed]; - }; + module.exports = SourceMap; }).call(this); diff --git a/src/coffee-script.coffee b/src/coffee-script.coffee index 818dfe30..2445b7d5 100644 --- a/src/coffee-script.coffee +++ b/src/coffee-script.coffee @@ -10,7 +10,7 @@ child_process = require 'child_process' {Lexer} = require './lexer' {parser} = require './parser' helpers = require './helpers' -sourcemap = require './sourcemap' +SourceMap = require './sourcemap' # The current CoffeeScript version number. exports.VERSION = '1.6.2' @@ -21,7 +21,7 @@ exports.helpers = helpers # Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler. # # If `options.sourceMap` is specified, then `options.filename` must also be specified. All -# options that can be passed to `generateV3SourceMap()` may also be passed here. +# options that can be passed to `SourceMap#generate` may also be passed here. # # This returns a javascript string, unless `options.sourceMap` is passed, # in which case this returns a `{js, v3SourceMap, sourceMap} @@ -31,7 +31,7 @@ exports.compile = compile = (code, options = {}) -> {merge} = exports.helpers if options.sourceMap - sourceMap = new sourcemap.SourceMap() + map = new SourceMap fragments = (parser.parse lexer.tokenize(code, options)).compileToFragments options @@ -41,9 +41,9 @@ exports.compile = compile = (code, options = {}) -> js = "" for fragment in fragments # Update the sourcemap with data from each fragment - if sourceMap + if options.sourceMap if fragment.locationData - sourceMap.addMapping( + map.add( [fragment.locationData.first_line, fragment.locationData.first_column], [currentLine, currentColumn], {noReplace: true}) @@ -60,9 +60,8 @@ exports.compile = compile = (code, options = {}) -> if options.sourceMap answer = {js} - if sourceMap - answer.sourceMap = sourceMap - answer.v3SourceMap = sourcemap.generateV3SourceMap(sourceMap, options, code) + answer.sourceMap = map + answer.v3SourceMap = map.generate(options, code) answer else js @@ -222,7 +221,7 @@ patchStackTrace = -> getSourceMapping = (filename, line, column) -> sourceMap = mainModule._sourceMaps[filename] - answer = sourceMap.getSourcePosition [line - 1, column - 1] if sourceMap + answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap if answer then [answer[0] + 1, answer[1] + 1] else null frames = for frame in stack diff --git a/src/sourcemap.coffee b/src/sourcemap.coffee deleted file mode 100644 index 0d16dcfb..00000000 --- a/src/sourcemap.coffee +++ /dev/null @@ -1,256 +0,0 @@ -#### LineMapping - -# Hold data about mappings for one line of generated source code. - -class LineMapping - constructor: (@generatedLine) -> - # columnMap keeps track of which columns we've already mapped. - @columnMap = {} - - # columnMappings is an array of all column mappings, sorted by generated-column. - @columnMappings = [] - - addMapping: (generatedColumn, [sourceLine, sourceColumn], options={}) -> - if @columnMap[generatedColumn] and options.noReplace - # We already have a mapping for this column. - return - - @columnMap[generatedColumn] = { - generatedLine: @generatedLine - generatedColumn - sourceLine - sourceColumn - } - - @columnMappings.push @columnMap[generatedColumn] - @columnMappings.sort (a,b) -> a.generatedColumn - b.generatedColumn - - getSourcePosition: (generatedColumn) -> - answer = null - lastColumnMapping = null - for columnMapping in @columnMappings - if columnMapping.generatedColumn > generatedColumn - break - else - lastColumnMapping = columnMapping - if lastColumnMapping - answer = [lastColumnMapping.sourceLine, lastColumnMapping.sourceColumn] - -#### SourceMap - -# Maps locations in a generated source file back to locations in the original source file. -# -# This is intentionally agnostic towards how a source map might be represented on disk. A -# SourceMap can be converted to a "v3" style sourcemap with `#generateV3SourceMap()`, for example -# but the SourceMap class itself knows nothing about v3 source maps. - -class exports.SourceMap - constructor: () -> - # `generatedLines` is an array of LineMappings, one per generated line. - @generatedLines = [] - - # Adds a mapping to this SourceMap. - # - # `sourceLocation` and `generatedLocation` are both [line, column] arrays. - # - # If `options.noReplace` is true, then if there is already a mapping for - # the specified `generatedLine` and `generatedColumn`, this will have no effect. - addMapping: (sourceLocation, generatedLocation, options={}) -> - [generatedLine, generatedColumn] = generatedLocation - - lineMapping = @generatedLines[generatedLine] - if not lineMapping - lineMapping = @generatedLines[generatedLine] = new LineMapping(generatedLine) - - lineMapping.addMapping generatedColumn, sourceLocation, options - - # Returns [sourceLine, sourceColumn], or null if no mapping could be found. - getSourcePosition: ([generatedLine, generatedColumn]) -> - answer = null - lineMapping = @generatedLines[generatedLine] - if not lineMapping - # TODO: Search backwards for the line? - else - answer = lineMapping.getSourcePosition generatedColumn - - answer - - - # `fn` will be called once for every recorded mapping, in the order in - # which they occur in the generated source. `fn` will be passed an object - # with four properties: sourceLine, sourceColumn, generatedLine, and - # generatedColumn. - forEachMapping: (fn) -> - for lineMapping, generatedLineNumber in @generatedLines - if lineMapping - for columnMapping in lineMapping.columnMappings - fn(columnMapping) - - -#### generateV3SourceMap - -# Builds a V3 source map from a SourceMap object. -# Returns the generated JSON as a string. -# -# `options.sourceRoot` may be used to specify the sourceRoot written to the source map. Also, -# `options.sourceFiles` and `options.generatedFile` may be passed to set "sources" and "file", -# respectively. Note that `sourceFiles` must be an array. - -exports.generateV3SourceMap = (sourceMap, options={}, code) -> - sourceRoot = options.sourceRoot or "" - sourceFiles = options.sourceFiles or [""] - generatedFile = options.generatedFile or "" - - writingGeneratedLine = 0 - lastGeneratedColumnWritten = 0 - lastSourceLineWritten = 0 - lastSourceColumnWritten = 0 - needComma = no - - mappings = "" - - sourceMap.forEachMapping (mapping) -> - while writingGeneratedLine < mapping.generatedLine - lastGeneratedColumnWritten = 0 - needComma = no - mappings += ";" - writingGeneratedLine++ - - # Write a comma if we've already written a segment on this line. - if needComma - mappings += "," - needComma = no - - # Write the next segment. - # Segments can be 1, 4, or 5 values. If just one, then it is a generated column which - # doesn't match anything in the source code. - # - # Fields are all zero-based, and relative to the previous occurence unless otherwise noted: - # * starting-column in generated source, relative to previous occurence for the current line. - # * index into the "sources" list - # * starting line in the original source - # * starting column in the original source - # * index into the "names" list associated with this segment. - - # Add the generated start-column - mappings += exports.vlqEncodeValue(mapping.generatedColumn - lastGeneratedColumnWritten) - lastGeneratedColumnWritten = mapping.generatedColumn - - # Add the index into the sources list - mappings += exports.vlqEncodeValue(0) - - # Add the source start-line - mappings += exports.vlqEncodeValue(mapping.sourceLine - lastSourceLineWritten) - lastSourceLineWritten = mapping.sourceLine - - # Add the source start-column - mappings += exports.vlqEncodeValue(mapping.sourceColumn - lastSourceColumnWritten) - lastSourceColumnWritten = mapping.sourceColumn - - # TODO: Do we care about symbol names for CoffeeScript? Probably not. - - needComma = yes - - answer = { - version: 3 - file: generatedFile - sourceRoot - sources: sourceFiles - names: [] - mappings - } - answer.sourcesContent = [code] if options.inline - - return JSON.stringify answer, null, 2 - -# Load a SourceMap from a JSON string. Returns the SourceMap object. -exports.loadV3SourceMap = (sourceMap) -> - todo() - -#### Base64 encoding helpers - -BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' -MAX_BASE64_VALUE = BASE64_CHARS.length - 1 - -encodeBase64Char = (value) -> - if value > MAX_BASE64_VALUE - throw new Error "Cannot encode value #{value} > #{MAX_BASE64_VALUE}" - else if value < 0 - throw new Error "Cannot encode value #{value} < 0" - BASE64_CHARS[value] - -decodeBase64Char = (char) -> - value = BASE64_CHARS.indexOf char - if value == -1 - throw new Error "Invalid Base 64 character: #{char}" - value - -#### Base 64 VLQ encoding/decoding helpers - -# Note that SourceMap VLQ encoding is "backwards". MIDI style VLQ encoding puts the -# most-significant-bit (MSB) from the original value into the MSB of the VLQ encoded value -# (see http://en.wikipedia.org/wiki/File:Uintvar_coding.svg). SourceMap VLQ does things -# the other way around, with the least significat four bits of the original value encoded -# into the first byte of the VLQ encoded value. - -VLQ_SHIFT = 5 -VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT # 0010 0000 -VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1 # 0001 1111 - -# Encode a value as Base 64 VLQ. -exports.vlqEncodeValue = (value) -> - # Least significant bit represents the sign. - signBit = if value < 0 then 1 else 0 - - # Next bits are the actual value - valueToEncode = (Math.abs(value) << 1) + signBit - - answer = "" - # Make sure we encode at least one character, even if valueToEncode is 0. - while valueToEncode || !answer - nextVlqChunk = valueToEncode & VLQ_VALUE_MASK - valueToEncode = valueToEncode >> VLQ_SHIFT - - if valueToEncode - nextVlqChunk |= VLQ_CONTINUATION_BIT - - answer += encodeBase64Char(nextVlqChunk) - - return answer - -# Decode a Base 64 VLQ value. -# -# Returns `[value, consumed]` where `value` is the decoded value, and `consumed` is the number -# of characters consumed from `str`. -exports.vlqDecodeValue = (str, offset=0) -> - position = offset - done = false - - value = 0 - continuationShift = 0 - - while !done - nextVlqChunk = decodeBase64Char(str[position]) - position += 1 - - nextChunkValue = nextVlqChunk & VLQ_VALUE_MASK - value += (nextChunkValue << continuationShift) - - if !(nextVlqChunk & VLQ_CONTINUATION_BIT) - # We'll be done after this character. - done = true - - # Bits are encoded least-significant first (opposite of MIDI VLQ). Increase the - # continuationShift, so the next byte will end up where it should in the value. - continuationShift += VLQ_SHIFT - - consumed = position - offset - - # Least significant bit represents the sign. - signBit = value & 1 - value = value >> 1 - - if signBit then value = -value - - return [value, consumed] - diff --git a/src/sourcemap.litcoffee b/src/sourcemap.litcoffee new file mode 100644 index 00000000..26cb584f --- /dev/null +++ b/src/sourcemap.litcoffee @@ -0,0 +1,188 @@ +Source maps allow JavaScript runtimes to match running JavaScript back to +the original CoffeeScript source code that corresponds to it. In order to +produce maps, we must keep track of positions (line number, column number) +for every construct in the syntax tree, and be able to generate a map file +-- which is a compact, VLQ-encoded representation of the JSON serialization +of this information -- to write out alongside the generated JavaScript. + + {merge} = require './helpers' + + +LineMap +------- + +Keeps track of information about column positions within a single line of +output JavaScript code. **SourceMap**s are implemented in terms of **LineMap**s. + + class LineMap + constructor: (@line) -> + @columns = [] + + add: (column, [sourceLine, sourceColumn], options={}) -> + return if @columns[column] and options.noReplace + @columns[column] = {line: @line, column, sourceLine, sourceColumn} + + sourceLocation: (column) -> + column-- until (mapping = @columns[column]) or (column <= 0) + mapping and [mapping.sourceLine, mapping.sourceColumn] + + +SourceMap +--------- + +Maps locations in for a single generated JavaScript file back to locations in +the original CoffeeScript source file. + +This is intentionally agnostic towards how a source map might be represented on +disk. Once the compiler is ready to produce a "v3"-style source map, we can walk +through the arrays of line and column buffer to produce it. + + class SourceMap + constructor: -> + @lines = [] + +Adds a mapping to this SourceMap. `sourceLocation` and `generatedLocation` +are both `[line, column]` arrays. If `options.noReplace` is true, then if there +is already a mapping for the specified `line` and `column`, this will have no +effect. + + add: (sourceLocation, generatedLocation, options = {}) -> + [line, column] = generatedLocation + lineMap = (@lines[line] or= new LineMap(line)) + lineMap.add column, sourceLocation, options + +Look up the original position of a given `line` and `column` in the generated +code. + + sourceLocation: ([line, column]) -> + line-- until (lineMap = @lines[line]) or (line <= 0) + lineMap and lineMap.sourceLocation column + +`func` will be called once for every recorded mapping, in the order in +which they occur in the generated source. `fn` will be passed an object +with four properties: sourceLine, sourceColumn, line, and +column. + + each: (iterator) -> + for lineMap, lineNumber in @lines when lineMap + for mapping in lineMap.columns when mapping + iterator mapping + + +V3 SourceMap Generation +----------------------- + +Builds up a V3 source map, returning the generated JSON as a string. +`options.sourceRoot` may be used to specify the sourceRoot written to the source +map. Also, `options.sourceFiles` and `options.generatedFile` may be passed to +set "sources" and "file", respectively. + + generate: (options = {}, code = null) -> + writingline = 0 + lastColumn = 0 + lastSourceLine = 0 + lastSourceColumn = 0 + needComma = no + buffer = "" + + @each (mapping) => + while writingline < mapping.line + lastColumn = 0 + needComma = no + buffer += ";" + writingline++ + +Write a comma if we've already written a segment on this line. + + if needComma + buffer += "," + needComma = no + +Write the next segment. Segments can be 1, 4, or 5 values. If just one, then it +is a generated column which doesn't match anything in the source code. + +The starting column in the generated source, relative to any previous recorded +column for the current line: + + buffer += @encodeVlq mapping.column - lastColumn + lastColumn = mapping.column + +The index into the list of sources: + + buffer += @encodeVlq 0 + +The starting line in the original source, relative to the previous source line. + + buffer += @encodeVlq mapping.sourceLine - lastSourceLine + if lastSourceLine isnt mapping.sourceLine + lastSourceLine = mapping.sourceLine + lastSourceColumn = 0 + +The starting column in the original source, relative to the previous column. + + buffer += @encodeVlq mapping.sourceColumn - lastSourceColumn + lastSourceColumn = mapping.sourceColumn + needComma = yes + +Produce the canonical JSON object format for a "v3" source map. + + v3 = + version: 3 + file: options.generatedFile or '' + sourceRoot: options.sourceRoot or '' + sources: options.sourceFiles or [''] + names: [] + mappings: buffer + + v3.sourcesContent = [code] if options.inline + + return JSON.stringify v3, null, 2 + + +Base64 VLQ Encoding +------------------- + +Note that SourceMap VLQ encoding is "backwards". MIDI-style VLQ encoding puts +the most-significant-bit (MSB) from the original value into the MSB of the VLQ +encoded value (see [Wikipedia](http://en.wikipedia.org/wiki/File:Uintvar_coding.svg)). +SourceMap VLQ does things the other way around, with the least significat four +bits of the original value encoded into the first byte of the VLQ encoded value. + + VLQ_SHIFT = 5 + VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT # 0010 0000 + VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1 # 0001 1111 + + encodeVlq: (value) -> + answer = '' + + # Least significant bit represents the sign. + signBit = if value < 0 then 1 else 0 + + # The next bits are the actual value. + valueToEncode = (Math.abs(value) << 1) + signBit + + # Make sure we encode at least one character, even if valueToEncode is 0. + while valueToEncode or not answer + nextChunk = valueToEncode & VLQ_VALUE_MASK + valueToEncode = valueToEncode >> VLQ_SHIFT + nextChunk |= VLQ_CONTINUATION_BIT if valueToEncode + answer += @encodeBase64 nextChunk + + return answer + + +Regular Base64 Encoding +----------------------- + + BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + + encodeBase64: (value) -> + BASE64_CHARS[value] or throw new Error "Cannot Base64 encode value: #{value}" + + +Our API for source maps is just the `SourceMap` class. + + module.exports = SourceMap + + + diff --git a/test/sourcemap.coffee b/test/sourcemap.coffee index 0005adc8..82b82d7f 100644 --- a/test/sourcemap.coffee +++ b/test/sourcemap.coffee @@ -1,6 +1,6 @@ return if global.testingBrowser -sourcemap = require '../src/sourcemap' +SourceMap = require '../src/sourcemap' vlqEncodedValues = [ [1, "C"], @@ -12,39 +12,30 @@ vlqEncodedValues = [ [948, "o7B"] ] -test "vlqEncodeValue tests", -> +test "encodeVlq tests", -> for pair in vlqEncodedValues - eq (sourcemap.vlqEncodeValue pair[0]), pair[1] - -test "vlqDecodeValue tests", -> - for pair in vlqEncodedValues - arrayEq (sourcemap.vlqDecodeValue pair[1]), [pair[0], pair[1].length] - -test "vlqDecodeValue with offset", -> - for pair in vlqEncodedValues - # Try with an offset, and some cruft at the end. - arrayEq (sourcemap.vlqDecodeValue ("abc" + pair[1] + "efg"), 3), [pair[0], pair[1].length] + eq ((new SourceMap).encodeVlq pair[0]), pair[1] eqJson = (a, b) -> eq (JSON.stringify JSON.parse a), (JSON.stringify JSON.parse b) test "SourceMap tests", -> - map = new sourcemap.SourceMap() - map.addMapping [0, 0], [0, 0] - map.addMapping [1, 5], [2, 4] - map.addMapping [1, 6], [2, 7] - map.addMapping [1, 9], [2, 8] - map.addMapping [3, 0], [3, 4] + map = new SourceMap + map.add [0, 0], [0, 0] + map.add [1, 5], [2, 4] + map.add [1, 6], [2, 7] + map.add [1, 9], [2, 8] + map.add [3, 0], [3, 4] - testWithFilenames = sourcemap.generateV3SourceMap map, { + testWithFilenames = map.generate { sourceRoot: "", sourceFiles: ["source.coffee"], generatedFile: "source.js"} - eqJson testWithFilenames, '{"version":3,"file":"source.js","sourceRoot":"","sources":["source.coffee"],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAET"}' - eqJson (sourcemap.generateV3SourceMap map), '{"version":3,"file":"","sourceRoot":"","sources":[""],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAET"}' + eqJson testWithFilenames, '{"version":3,"file":"source.js","sourceRoot":"","sources":["source.coffee"],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAEA"}' + eqJson map.generate(), '{"version":3,"file":"","sourceRoot":"","sources":[""],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAEA"}' # Look up a generated column - should get back the original source position. - arrayEq map.getSourcePosition([2,8]), [1,9] + arrayEq map.sourceLocation([2,8]), [1,9] # Look up a point futher along on the same line - should get back the same source position. - arrayEq map.getSourcePosition([2,10]), [1,9] + arrayEq map.sourceLocation([2,10]), [1,9]