sourcemap.coffee | |
---|---|
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 | class exports.SourceMap
constructor: () -> |
| @generatedLines = [] |
Adds a mapping to this SourceMap.
If | 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 |
| 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. | exports.generateV3SourceMap = (sourceMap, sourceFile=null, generatedFile=null) ->
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: if sourceFile then [sourceFile] else []
names: []
mappings
}
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 | 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]
|