758 lines
21 KiB
Go
758 lines
21 KiB
Go
package sourcemap
|
|
|
|
import (
|
|
"bytes"
|
|
"unicode/utf8"
|
|
|
|
"github.com/evanw/esbuild/internal/helpers"
|
|
"github.com/evanw/esbuild/internal/logger"
|
|
)
|
|
|
|
type Mapping struct {
|
|
GeneratedLine int32 // 0-based
|
|
GeneratedColumn int32 // 0-based count of UTF-16 code units
|
|
|
|
SourceIndex int32 // 0-based
|
|
OriginalLine int32 // 0-based
|
|
OriginalColumn int32 // 0-based count of UTF-16 code units
|
|
}
|
|
|
|
type SourceMap struct {
|
|
Sources []string
|
|
SourcesContent []SourceContent
|
|
Mappings []Mapping
|
|
}
|
|
|
|
type SourceContent struct {
|
|
// This stores both the unquoted and the quoted values. We try to use the
|
|
// already-quoted value if possible so we don't need to re-quote it
|
|
// unnecessarily for maximum performance.
|
|
Quoted string
|
|
|
|
// But sometimes we need to re-quote the value, such as when it contains
|
|
// non-ASCII characters and we are in ASCII-only mode. In that case we quote
|
|
// this parsed UTF-16 value.
|
|
Value []uint16
|
|
}
|
|
|
|
func (sm *SourceMap) Find(line int32, column int32) *Mapping {
|
|
mappings := sm.Mappings
|
|
|
|
// Binary search
|
|
count := len(mappings)
|
|
index := 0
|
|
for count > 0 {
|
|
step := count / 2
|
|
i := index + step
|
|
mapping := mappings[i]
|
|
if mapping.GeneratedLine < line || (mapping.GeneratedLine == line && mapping.GeneratedColumn <= column) {
|
|
index = i + 1
|
|
count -= step + 1
|
|
} else {
|
|
count = step
|
|
}
|
|
}
|
|
|
|
// Handle search failure
|
|
if index > 0 {
|
|
mapping := &mappings[index-1]
|
|
|
|
// Match the behavior of the popular "source-map" library from Mozilla
|
|
if mapping.GeneratedLine == line {
|
|
return mapping
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var base64 = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
|
|
|
|
// A single base 64 digit can contain 6 bits of data. For the base 64 variable
|
|
// length quantities we use in the source map spec, the first bit is the sign,
|
|
// the next four bits are the actual value, and the 6th bit is the continuation
|
|
// bit. The continuation bit tells us whether there are more digits in this
|
|
// value following this digit.
|
|
//
|
|
// Continuation
|
|
// | Sign
|
|
// | |
|
|
// V V
|
|
// 101011
|
|
//
|
|
func EncodeVLQ(value int) []byte {
|
|
var vlq int
|
|
if value < 0 {
|
|
vlq = ((-value) << 1) | 1
|
|
} else {
|
|
vlq = value << 1
|
|
}
|
|
|
|
// Handle the common case up front without allocations
|
|
if (vlq >> 5) == 0 {
|
|
digit := vlq & 31
|
|
return base64[digit : digit+1]
|
|
}
|
|
|
|
encoded := []byte{}
|
|
for {
|
|
digit := vlq & 31
|
|
vlq >>= 5
|
|
|
|
// If there are still more digits in this value, we must make sure the
|
|
// continuation bit is marked
|
|
if vlq != 0 {
|
|
digit |= 32
|
|
}
|
|
|
|
encoded = append(encoded, base64[digit])
|
|
|
|
if vlq == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
return encoded
|
|
}
|
|
|
|
func DecodeVLQ(encoded []byte, start int) (int, int) {
|
|
shift := 0
|
|
vlq := 0
|
|
|
|
// Scan over the input
|
|
for {
|
|
index := bytes.IndexByte(base64, encoded[start])
|
|
if index < 0 {
|
|
break
|
|
}
|
|
|
|
// Decode a single byte
|
|
vlq |= (index & 31) << shift
|
|
start++
|
|
shift += 5
|
|
|
|
// Stop if there's no continuation bit
|
|
if (index & 32) == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Recover the value
|
|
value := vlq >> 1
|
|
if (vlq & 1) != 0 {
|
|
value = -value
|
|
}
|
|
return value, start
|
|
}
|
|
|
|
func DecodeVLQUTF16(encoded []uint16) (int, int, bool) {
|
|
n := len(encoded)
|
|
if n == 0 {
|
|
return 0, 0, false
|
|
}
|
|
|
|
// Scan over the input
|
|
current := 0
|
|
shift := 0
|
|
vlq := 0
|
|
for {
|
|
if current >= n {
|
|
return 0, 0, false
|
|
}
|
|
index := bytes.IndexByte(base64, byte(encoded[current]))
|
|
if index < 0 {
|
|
return 0, 0, false
|
|
}
|
|
|
|
// Decode a single byte
|
|
vlq |= (index & 31) << shift
|
|
current++
|
|
shift += 5
|
|
|
|
// Stop if there's no continuation bit
|
|
if (index & 32) == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Recover the value
|
|
var value = vlq >> 1
|
|
if (vlq & 1) != 0 {
|
|
value = -value
|
|
}
|
|
return value, current, true
|
|
}
|
|
|
|
type LineColumnOffset struct {
|
|
Lines int
|
|
Columns int
|
|
}
|
|
|
|
func (a LineColumnOffset) ComesBefore(b LineColumnOffset) bool {
|
|
return a.Lines < b.Lines || (a.Lines == b.Lines && a.Columns < b.Columns)
|
|
}
|
|
|
|
func (a *LineColumnOffset) Add(b LineColumnOffset) {
|
|
if b.Lines == 0 {
|
|
a.Columns += b.Columns
|
|
} else {
|
|
a.Lines += b.Lines
|
|
a.Columns = b.Columns
|
|
}
|
|
}
|
|
|
|
func (offset *LineColumnOffset) AdvanceBytes(bytes []byte) {
|
|
columns := offset.Columns
|
|
for len(bytes) > 0 {
|
|
c, width := utf8.DecodeRune(bytes)
|
|
bytes = bytes[width:]
|
|
switch c {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
// Handle Windows-specific "\r\n" newlines
|
|
if c == '\r' && len(bytes) > 0 && bytes[0] == '\n' {
|
|
columns++
|
|
continue
|
|
}
|
|
|
|
offset.Lines++
|
|
columns = 0
|
|
|
|
default:
|
|
// Mozilla's "source-map" library counts columns using UTF-16 code units
|
|
if c <= 0xFFFF {
|
|
columns++
|
|
} else {
|
|
columns += 2
|
|
}
|
|
}
|
|
}
|
|
offset.Columns = columns
|
|
}
|
|
|
|
func (offset *LineColumnOffset) AdvanceString(text string) {
|
|
columns := offset.Columns
|
|
for i, c := range text {
|
|
switch c {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
// Handle Windows-specific "\r\n" newlines
|
|
if c == '\r' && i+1 < len(text) && text[i+1] == '\n' {
|
|
columns++
|
|
continue
|
|
}
|
|
|
|
offset.Lines++
|
|
columns = 0
|
|
|
|
default:
|
|
// Mozilla's "source-map" library counts columns using UTF-16 code units
|
|
if c <= 0xFFFF {
|
|
columns++
|
|
} else {
|
|
columns += 2
|
|
}
|
|
}
|
|
}
|
|
offset.Columns = columns
|
|
}
|
|
|
|
type SourceMapPieces struct {
|
|
Prefix []byte
|
|
Mappings []byte
|
|
Suffix []byte
|
|
}
|
|
|
|
func (pieces SourceMapPieces) HasContent() bool {
|
|
return len(pieces.Prefix)+len(pieces.Mappings)+len(pieces.Suffix) > 0
|
|
}
|
|
|
|
type SourceMapShift struct {
|
|
Before LineColumnOffset
|
|
After LineColumnOffset
|
|
}
|
|
|
|
func (pieces SourceMapPieces) Finalize(shifts []SourceMapShift) []byte {
|
|
// An optimized path for when there are no shifts
|
|
if len(shifts) == 1 {
|
|
bytes := pieces.Prefix
|
|
minCap := len(bytes) + len(pieces.Mappings) + len(pieces.Suffix)
|
|
if cap(bytes) < minCap {
|
|
bytes = append(make([]byte, 0, minCap), bytes...)
|
|
}
|
|
bytes = append(bytes, pieces.Mappings...)
|
|
bytes = append(bytes, pieces.Suffix...)
|
|
return bytes
|
|
}
|
|
|
|
startOfRun := 0
|
|
current := 0
|
|
generated := LineColumnOffset{}
|
|
prevShiftColumnDelta := 0
|
|
j := helpers.Joiner{}
|
|
|
|
// Start the source map
|
|
j.AddBytes(pieces.Prefix)
|
|
|
|
// This assumes that a) all mappings are valid and b) all mappings are ordered
|
|
// by increasing generated position. This should be the case for all mappings
|
|
// generated by esbuild, which should be the only mappings we process here.
|
|
for current < len(pieces.Mappings) {
|
|
// Handle a line break
|
|
if pieces.Mappings[current] == ';' {
|
|
generated.Lines++
|
|
generated.Columns = 0
|
|
prevShiftColumnDelta = 0
|
|
current++
|
|
continue
|
|
}
|
|
|
|
potentialEndOfRun := current
|
|
|
|
// Read the generated column
|
|
generatedColumnDelta, next := DecodeVLQ(pieces.Mappings, current)
|
|
generated.Columns += generatedColumnDelta
|
|
current = next
|
|
|
|
potentialStartOfRun := current
|
|
|
|
// Skip over the original position information
|
|
_, current = DecodeVLQ(pieces.Mappings, current) // The original source
|
|
_, current = DecodeVLQ(pieces.Mappings, current) // The original line
|
|
_, current = DecodeVLQ(pieces.Mappings, current) // The original column
|
|
|
|
// Skip a trailing comma
|
|
if current < len(pieces.Mappings) && pieces.Mappings[current] == ',' {
|
|
current++
|
|
}
|
|
|
|
// Detect crossing shift boundaries
|
|
didCrossBoundary := false
|
|
for len(shifts) > 1 && shifts[1].Before.ComesBefore(generated) {
|
|
shifts = shifts[1:]
|
|
didCrossBoundary = true
|
|
}
|
|
if !didCrossBoundary {
|
|
continue
|
|
}
|
|
|
|
// This shift isn't relevant if the next mapping after this shift is on a
|
|
// following line. In that case, don't split and keep scanning instead.
|
|
shift := shifts[0]
|
|
if shift.After.Lines != generated.Lines {
|
|
continue
|
|
}
|
|
|
|
// Add all previous mappings in a single run for efficiency. Since source
|
|
// mappings are relative, no data needs to be modified inside this run.
|
|
j.AddBytes(pieces.Mappings[startOfRun:potentialEndOfRun])
|
|
|
|
// Then modify the first mapping across the shift boundary with the updated
|
|
// generated column value. It's simplest to only support column shifts. This
|
|
// is reasonable because import paths should not contain newlines.
|
|
if shift.Before.Lines != shift.After.Lines {
|
|
panic("Unexpected line change when shifting source maps")
|
|
}
|
|
shiftColumnDelta := shift.After.Columns - shift.Before.Columns
|
|
j.AddBytes(EncodeVLQ(generatedColumnDelta + shiftColumnDelta - prevShiftColumnDelta))
|
|
prevShiftColumnDelta = shiftColumnDelta
|
|
|
|
// Finally, start the next run after the end of this generated column offset
|
|
startOfRun = potentialStartOfRun
|
|
}
|
|
|
|
// Finish the source map
|
|
j.AddBytes(pieces.Mappings[startOfRun:])
|
|
j.AddBytes(pieces.Suffix)
|
|
return j.Done()
|
|
}
|
|
|
|
// Coordinates in source maps are stored using relative offsets for size
|
|
// reasons. When joining together chunks of a source map that were emitted
|
|
// in parallel for different parts of a file, we need to fix up the first
|
|
// segment of each chunk to be relative to the end of the previous chunk.
|
|
type SourceMapState struct {
|
|
// This isn't stored in the source map. It's only used by the bundler to join
|
|
// source map chunks together correctly.
|
|
GeneratedLine int
|
|
|
|
// These are stored in the source map in VLQ format.
|
|
GeneratedColumn int
|
|
SourceIndex int
|
|
OriginalLine int
|
|
OriginalColumn int
|
|
}
|
|
|
|
// Source map chunks are computed in parallel for speed. Each chunk is relative
|
|
// to the zero state instead of being relative to the end state of the previous
|
|
// chunk, since it's impossible to know the end state of the previous chunk in
|
|
// a parallel computation.
|
|
//
|
|
// After all chunks are computed, they are joined together in a second pass.
|
|
// This rewrites the first mapping in each chunk to be relative to the end
|
|
// state of the previous chunk.
|
|
func AppendSourceMapChunk(j *helpers.Joiner, prevEndState SourceMapState, startState SourceMapState, sourceMap []byte) {
|
|
// Handle line breaks in between this mapping and the previous one
|
|
if startState.GeneratedLine != 0 {
|
|
j.AddBytes(bytes.Repeat([]byte{';'}, startState.GeneratedLine))
|
|
prevEndState.GeneratedColumn = 0
|
|
}
|
|
|
|
// Skip past any leading semicolons, which indicate line breaks
|
|
semicolons := 0
|
|
for sourceMap[semicolons] == ';' {
|
|
semicolons++
|
|
}
|
|
if semicolons > 0 {
|
|
j.AddBytes(sourceMap[:semicolons])
|
|
sourceMap = sourceMap[semicolons:]
|
|
prevEndState.GeneratedColumn = 0
|
|
startState.GeneratedColumn = 0
|
|
}
|
|
|
|
// Strip off the first mapping from the buffer. The first mapping should be
|
|
// for the start of the original file (the printer always generates one for
|
|
// the start of the file).
|
|
generatedColumn, i := DecodeVLQ(sourceMap, 0)
|
|
sourceIndex, i := DecodeVLQ(sourceMap, i)
|
|
originalLine, i := DecodeVLQ(sourceMap, i)
|
|
originalColumn, i := DecodeVLQ(sourceMap, i)
|
|
sourceMap = sourceMap[i:]
|
|
|
|
// Rewrite the first mapping to be relative to the end state of the previous
|
|
// chunk. We now know what the end state is because we're in the second pass
|
|
// where all chunks have already been generated.
|
|
startState.SourceIndex += sourceIndex
|
|
startState.GeneratedColumn += generatedColumn
|
|
startState.OriginalLine += originalLine
|
|
startState.OriginalColumn += originalColumn
|
|
j.AddBytes(appendMappingToBuffer(nil, j.LastByte(), prevEndState, startState))
|
|
|
|
// Then append everything after that without modification.
|
|
j.AddBytes(sourceMap)
|
|
}
|
|
|
|
func appendMappingToBuffer(buffer []byte, lastByte byte, prevState SourceMapState, currentState SourceMapState) []byte {
|
|
// Put commas in between mappings
|
|
if lastByte != 0 && lastByte != ';' && lastByte != '"' {
|
|
buffer = append(buffer, ',')
|
|
}
|
|
|
|
// Record the generated column (the line is recorded using ';' elsewhere)
|
|
buffer = append(buffer, EncodeVLQ(currentState.GeneratedColumn-prevState.GeneratedColumn)...)
|
|
prevState.GeneratedColumn = currentState.GeneratedColumn
|
|
|
|
// Record the generated source
|
|
buffer = append(buffer, EncodeVLQ(currentState.SourceIndex-prevState.SourceIndex)...)
|
|
prevState.SourceIndex = currentState.SourceIndex
|
|
|
|
// Record the original line
|
|
buffer = append(buffer, EncodeVLQ(currentState.OriginalLine-prevState.OriginalLine)...)
|
|
prevState.OriginalLine = currentState.OriginalLine
|
|
|
|
// Record the original column
|
|
buffer = append(buffer, EncodeVLQ(currentState.OriginalColumn-prevState.OriginalColumn)...)
|
|
prevState.OriginalColumn = currentState.OriginalColumn
|
|
|
|
return buffer
|
|
}
|
|
|
|
type LineOffsetTable struct {
|
|
byteOffsetToStartOfLine int32
|
|
|
|
// The source map specification is very loose and does not specify what
|
|
// column numbers actually mean. The popular "source-map" library from Mozilla
|
|
// appears to interpret them as counts of UTF-16 code units, so we generate
|
|
// those too for compatibility.
|
|
//
|
|
// We keep mapping tables around to accelerate conversion from byte offsets
|
|
// to UTF-16 code unit counts. However, this mapping takes up a lot of memory
|
|
// and generates a lot of garbage. Since most JavaScript is ASCII and the
|
|
// mapping for ASCII is 1:1, we avoid creating a table for ASCII-only lines
|
|
// as an optimization.
|
|
byteOffsetToFirstNonASCII int32
|
|
columnsForNonASCII []int32
|
|
}
|
|
|
|
func GenerateLineOffsetTables(contents string, approximateLineCount int32) []LineOffsetTable {
|
|
var ColumnsForNonASCII []int32
|
|
ByteOffsetToFirstNonASCII := int32(0)
|
|
lineByteOffset := 0
|
|
columnByteOffset := 0
|
|
column := int32(0)
|
|
|
|
// Preallocate the top-level table using the approximate line count from the lexer
|
|
lineOffsetTables := make([]LineOffsetTable, 0, approximateLineCount)
|
|
|
|
for i, c := range contents {
|
|
// Mark the start of the next line
|
|
if column == 0 {
|
|
lineByteOffset = i
|
|
}
|
|
|
|
// Start the mapping if this character is non-ASCII
|
|
if c > 0x7F && ColumnsForNonASCII == nil {
|
|
columnByteOffset = i - lineByteOffset
|
|
ByteOffsetToFirstNonASCII = int32(columnByteOffset)
|
|
ColumnsForNonASCII = []int32{}
|
|
}
|
|
|
|
// Update the per-byte column offsets
|
|
if ColumnsForNonASCII != nil {
|
|
for lineBytesSoFar := i - lineByteOffset; columnByteOffset <= lineBytesSoFar; columnByteOffset++ {
|
|
ColumnsForNonASCII = append(ColumnsForNonASCII, column)
|
|
}
|
|
}
|
|
|
|
switch c {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
// Handle Windows-specific "\r\n" newlines
|
|
if c == '\r' && i+1 < len(contents) && contents[i+1] == '\n' {
|
|
column++
|
|
continue
|
|
}
|
|
|
|
lineOffsetTables = append(lineOffsetTables, LineOffsetTable{
|
|
byteOffsetToStartOfLine: int32(lineByteOffset),
|
|
byteOffsetToFirstNonASCII: ByteOffsetToFirstNonASCII,
|
|
columnsForNonASCII: ColumnsForNonASCII,
|
|
})
|
|
columnByteOffset = 0
|
|
ByteOffsetToFirstNonASCII = 0
|
|
ColumnsForNonASCII = nil
|
|
column = 0
|
|
|
|
default:
|
|
// Mozilla's "source-map" library counts columns using UTF-16 code units
|
|
if c <= 0xFFFF {
|
|
column++
|
|
} else {
|
|
column += 2
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark the start of the next line
|
|
if column == 0 {
|
|
lineByteOffset = len(contents)
|
|
}
|
|
|
|
// Do one last update for the column at the end of the file
|
|
if ColumnsForNonASCII != nil {
|
|
for lineBytesSoFar := len(contents) - lineByteOffset; columnByteOffset <= lineBytesSoFar; columnByteOffset++ {
|
|
ColumnsForNonASCII = append(ColumnsForNonASCII, column)
|
|
}
|
|
}
|
|
|
|
lineOffsetTables = append(lineOffsetTables, LineOffsetTable{
|
|
byteOffsetToStartOfLine: int32(lineByteOffset),
|
|
byteOffsetToFirstNonASCII: ByteOffsetToFirstNonASCII,
|
|
columnsForNonASCII: ColumnsForNonASCII,
|
|
})
|
|
return lineOffsetTables
|
|
}
|
|
|
|
type Chunk struct {
|
|
Buffer []byte
|
|
|
|
// This end state will be used to rewrite the start of the following source
|
|
// map chunk so that the delta-encoded VLQ numbers are preserved.
|
|
EndState SourceMapState
|
|
|
|
// There probably isn't a source mapping at the end of the file (nor should
|
|
// there be) but if we're appending another source map chunk after this one,
|
|
// we'll need to know how many characters were in the last line we generated.
|
|
FinalGeneratedColumn int
|
|
|
|
ShouldIgnore bool
|
|
}
|
|
|
|
type ChunkBuilder struct {
|
|
inputSourceMap *SourceMap
|
|
sourceMap []byte
|
|
prevLoc logger.Loc
|
|
prevState SourceMapState
|
|
lastGeneratedUpdate int
|
|
generatedColumn int
|
|
hasPrevState bool
|
|
lineOffsetTables []LineOffsetTable
|
|
|
|
// This is a workaround for a bug in the popular "source-map" library:
|
|
// https://github.com/mozilla/source-map/issues/261. The library will
|
|
// sometimes return null when querying a source map unless every line
|
|
// starts with a mapping at column zero.
|
|
//
|
|
// The workaround is to replicate the previous mapping if a line ends
|
|
// up not starting with a mapping. This is done lazily because we want
|
|
// to avoid replicating the previous mapping if we don't need to.
|
|
lineStartsWithMapping bool
|
|
coverLinesWithoutMappings bool
|
|
}
|
|
|
|
func MakeChunkBuilder(inputSourceMap *SourceMap, lineOffsetTables []LineOffsetTable) ChunkBuilder {
|
|
return ChunkBuilder{
|
|
inputSourceMap: inputSourceMap,
|
|
prevLoc: logger.Loc{Start: -1},
|
|
lineOffsetTables: lineOffsetTables,
|
|
|
|
// We automatically repeat the previous source mapping if we ever generate
|
|
// a line that doesn't start with a mapping. This helps give files more
|
|
// complete mapping coverage without gaps.
|
|
//
|
|
// However, we probably shouldn't do this if the input file has a nested
|
|
// source map that we will be remapping through. We have no idea what state
|
|
// that source map is in and it could be pretty scrambled.
|
|
//
|
|
// I've seen cases where blindly repeating the last mapping for subsequent
|
|
// lines gives very strange and unhelpful results with source maps from
|
|
// other tools.
|
|
coverLinesWithoutMappings: inputSourceMap == nil,
|
|
}
|
|
}
|
|
|
|
func (b *ChunkBuilder) AddSourceMapping(loc logger.Loc, output []byte) {
|
|
if loc == b.prevLoc {
|
|
return
|
|
}
|
|
b.prevLoc = loc
|
|
|
|
// Binary search to find the line
|
|
lineOffsetTables := b.lineOffsetTables
|
|
count := len(lineOffsetTables)
|
|
originalLine := 0
|
|
for count > 0 {
|
|
step := count / 2
|
|
i := originalLine + step
|
|
if lineOffsetTables[i].byteOffsetToStartOfLine <= loc.Start {
|
|
originalLine = i + 1
|
|
count = count - step - 1
|
|
} else {
|
|
count = step
|
|
}
|
|
}
|
|
originalLine--
|
|
|
|
// Use the line to compute the column
|
|
line := &lineOffsetTables[originalLine]
|
|
originalColumn := int(loc.Start - line.byteOffsetToStartOfLine)
|
|
if line.columnsForNonASCII != nil && originalColumn >= int(line.byteOffsetToFirstNonASCII) {
|
|
originalColumn = int(line.columnsForNonASCII[originalColumn-int(line.byteOffsetToFirstNonASCII)])
|
|
}
|
|
|
|
b.updateGeneratedLineAndColumn(output)
|
|
|
|
// If this line doesn't start with a mapping and we're about to add a mapping
|
|
// that's not at the start, insert a mapping first so the line starts with one.
|
|
if b.coverLinesWithoutMappings && !b.lineStartsWithMapping && b.generatedColumn > 0 && b.hasPrevState {
|
|
b.appendMappingWithoutRemapping(SourceMapState{
|
|
GeneratedLine: b.prevState.GeneratedLine,
|
|
GeneratedColumn: 0,
|
|
SourceIndex: b.prevState.SourceIndex,
|
|
OriginalLine: b.prevState.OriginalLine,
|
|
OriginalColumn: b.prevState.OriginalColumn,
|
|
})
|
|
}
|
|
|
|
b.appendMapping(SourceMapState{
|
|
GeneratedLine: b.prevState.GeneratedLine,
|
|
GeneratedColumn: b.generatedColumn,
|
|
OriginalLine: originalLine,
|
|
OriginalColumn: originalColumn,
|
|
})
|
|
|
|
// This line now has a mapping on it, so don't insert another one
|
|
b.lineStartsWithMapping = true
|
|
}
|
|
|
|
func (b *ChunkBuilder) GenerateChunk(output []byte) Chunk {
|
|
b.updateGeneratedLineAndColumn(output)
|
|
shouldIgnore := true
|
|
for _, c := range b.sourceMap {
|
|
if c != ';' {
|
|
shouldIgnore = false
|
|
break
|
|
}
|
|
}
|
|
return Chunk{
|
|
Buffer: b.sourceMap,
|
|
EndState: b.prevState,
|
|
FinalGeneratedColumn: b.generatedColumn,
|
|
ShouldIgnore: shouldIgnore,
|
|
}
|
|
}
|
|
|
|
// Scan over the printed text since the last source mapping and update the
|
|
// generated line and column numbers
|
|
func (b *ChunkBuilder) updateGeneratedLineAndColumn(output []byte) {
|
|
for i, c := range string(output[b.lastGeneratedUpdate:]) {
|
|
switch c {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
// Handle Windows-specific "\r\n" newlines
|
|
if c == '\r' {
|
|
newlineCheck := b.lastGeneratedUpdate + i + 1
|
|
if newlineCheck < len(output) && output[newlineCheck] == '\n' {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// If we're about to move to the next line and the previous line didn't have
|
|
// any mappings, add a mapping at the start of the previous line.
|
|
if b.coverLinesWithoutMappings && !b.lineStartsWithMapping && b.hasPrevState {
|
|
b.appendMappingWithoutRemapping(SourceMapState{
|
|
GeneratedLine: b.prevState.GeneratedLine,
|
|
GeneratedColumn: 0,
|
|
SourceIndex: b.prevState.SourceIndex,
|
|
OriginalLine: b.prevState.OriginalLine,
|
|
OriginalColumn: b.prevState.OriginalColumn,
|
|
})
|
|
}
|
|
|
|
b.prevState.GeneratedLine++
|
|
b.prevState.GeneratedColumn = 0
|
|
b.generatedColumn = 0
|
|
b.sourceMap = append(b.sourceMap, ';')
|
|
|
|
// This new line doesn't have a mapping yet
|
|
b.lineStartsWithMapping = false
|
|
|
|
default:
|
|
// Mozilla's "source-map" library counts columns using UTF-16 code units
|
|
if c <= 0xFFFF {
|
|
b.generatedColumn++
|
|
} else {
|
|
b.generatedColumn += 2
|
|
}
|
|
}
|
|
}
|
|
|
|
b.lastGeneratedUpdate = len(output)
|
|
}
|
|
|
|
func (b *ChunkBuilder) appendMapping(currentState SourceMapState) {
|
|
// If the input file had a source map, map all the way back to the original
|
|
if b.inputSourceMap != nil {
|
|
mapping := b.inputSourceMap.Find(
|
|
int32(currentState.OriginalLine),
|
|
int32(currentState.OriginalColumn))
|
|
|
|
// Some locations won't have a mapping
|
|
if mapping == nil {
|
|
return
|
|
}
|
|
|
|
currentState.SourceIndex = int(mapping.SourceIndex)
|
|
currentState.OriginalLine = int(mapping.OriginalLine)
|
|
currentState.OriginalColumn = int(mapping.OriginalColumn)
|
|
}
|
|
|
|
b.appendMappingWithoutRemapping(currentState)
|
|
}
|
|
|
|
func (b *ChunkBuilder) appendMappingWithoutRemapping(currentState SourceMapState) {
|
|
var lastByte byte
|
|
if len(b.sourceMap) != 0 {
|
|
lastByte = b.sourceMap[len(b.sourceMap)-1]
|
|
}
|
|
|
|
b.sourceMap = appendMappingToBuffer(b.sourceMap, lastByte, b.prevState, currentState)
|
|
b.prevState = currentState
|
|
b.hasPrevState = true
|
|
}
|