2025-10-15 10:12:44 +03:00

361 lines
11 KiB
Go

package sourcemap
import (
"errors"
"slices"
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
"github.com/go-json-experiment/json"
)
type (
SourceIndex int
NameIndex int
)
const (
sourceIndexNotSet SourceIndex = -1
nameIndexNotSet NameIndex = -1
notSet int = -1
)
type Generator struct {
pathOptions tspath.ComparePathsOptions
file string
sourceRoot string
sourcesDirectoryPath string
rawSources []string
sources []string
sourceToSourceIndexMap map[string]SourceIndex
sourcesContent []*string
names []string
nameToNameIndexMap map[string]NameIndex
mappings strings.Builder
lastGeneratedLine int
lastGeneratedCharacter int
lastSourceIndex SourceIndex
lastSourceLine int
lastSourceCharacter int
lastNameIndex NameIndex
hasLast bool
pendingGeneratedLine int
pendingGeneratedCharacter int
pendingSourceIndex SourceIndex
pendingSourceLine int
pendingSourceCharacter int
pendingNameIndex NameIndex
hasPending bool
hasPendingSource bool
hasPendingName bool
}
type RawSourceMap struct {
Version int `json:"version"`
File string `json:"file"`
SourceRoot string `json:"sourceRoot"`
Sources []string `json:"sources"`
Names []string `json:"names"`
Mappings string `json:"mappings"`
SourcesContent []*string `json:"sourcesContent,omitzero"`
}
func NewGenerator(file string, sourceRoot string, sourcesDirectoryPath string, options tspath.ComparePathsOptions) *Generator {
return &Generator{
file: file,
sourceRoot: sourceRoot,
sourcesDirectoryPath: sourcesDirectoryPath,
pathOptions: options,
}
}
func (gen *Generator) Sources() []string { return gen.rawSources }
// Adds a source to the source map
func (gen *Generator) AddSource(fileName string) SourceIndex {
source := tspath.GetRelativePathToDirectoryOrUrl(
gen.sourcesDirectoryPath,
fileName,
true, /*isAbsolutePathAnUrl*/
gen.pathOptions,
)
sourceIndex, found := gen.sourceToSourceIndexMap[source]
if !found {
sourceIndex = SourceIndex(len(gen.sources))
gen.sources = append(gen.sources, source)
gen.rawSources = append(gen.rawSources, fileName)
if gen.sourceToSourceIndexMap == nil {
gen.sourceToSourceIndexMap = make(map[string]SourceIndex)
}
gen.sourceToSourceIndexMap[source] = sourceIndex
}
return sourceIndex
}
// Sets the content for a source
func (gen *Generator) SetSourceContent(sourceIndex SourceIndex, content string) error {
if sourceIndex < 0 || int(sourceIndex) >= len(gen.sources) {
return errors.New("sourceIndex is out of range")
}
for len(gen.sourcesContent) <= int(sourceIndex) {
gen.sourcesContent = append(gen.sourcesContent, nil)
}
gen.sourcesContent[sourceIndex] = &content
return nil
}
// Declares a name in the source map, returning the index of the name
func (gen *Generator) AddName(name string) NameIndex {
nameIndex, found := gen.nameToNameIndexMap[name]
if !found {
nameIndex = NameIndex(len(gen.names))
gen.names = append(gen.names, name)
if gen.nameToNameIndexMap == nil {
gen.nameToNameIndexMap = make(map[string]NameIndex)
}
gen.nameToNameIndexMap[name] = nameIndex
}
return nameIndex
}
func (gen *Generator) isNewGeneratedPosition(generatedLine int, generatedCharacter int) bool {
return !gen.hasPending ||
gen.pendingGeneratedLine != generatedLine ||
gen.pendingGeneratedCharacter != generatedCharacter
}
func (gen *Generator) isBacktrackingSourcePosition(sourceIndex SourceIndex, sourceLine int, sourceCharacter int) bool {
return sourceIndex != sourceIndexNotSet &&
sourceLine != notSet &&
sourceCharacter != notSet &&
gen.pendingSourceIndex == sourceIndex &&
(gen.pendingSourceLine > sourceLine ||
gen.pendingSourceLine == sourceLine && gen.pendingSourceCharacter > sourceCharacter)
}
func (gen *Generator) shouldCommitMapping() bool {
return gen.hasPending && (!gen.hasLast ||
gen.lastGeneratedLine != gen.pendingGeneratedLine ||
gen.lastGeneratedCharacter != gen.pendingGeneratedCharacter ||
gen.lastSourceIndex != gen.pendingSourceIndex ||
gen.lastSourceLine != gen.pendingSourceLine ||
gen.lastSourceCharacter != gen.pendingSourceCharacter ||
gen.lastNameIndex != gen.pendingNameIndex)
}
func (gen *Generator) appendMappingCharCode(charCode rune) {
gen.mappings.WriteRune(charCode)
}
func (gen *Generator) appendBase64VLQ(inValue int) {
// Add a new least significant bit that has the sign of the value.
// if negative number the least significant bit that gets added to the number has value 1
// else least significant bit value that gets added is 0
// eg. -1 changes to binary : 01 [1] => 3
// +1 changes to binary : 01 [0] => 2
if inValue < 0 {
inValue = ((-inValue) << 1) + 1
} else {
inValue = inValue << 1
}
// Encode 5 bits at a time starting from least significant bits
for {
currentDigit := inValue & 31 // 11111
inValue = inValue >> 5
if inValue > 0 {
// There are still more digits to decode, set the msb (6th bit)
currentDigit = currentDigit | 32
}
gen.appendMappingCharCode(base64FormatEncode(currentDigit))
if inValue <= 0 {
break
}
}
}
func (gen *Generator) commitPendingMapping() {
if !gen.shouldCommitMapping() {
return
}
// Line/Comma delimiters
if gen.lastGeneratedLine < gen.pendingGeneratedLine {
// Emit line delimiters
for {
gen.appendMappingCharCode(';')
gen.lastGeneratedLine++
if gen.lastGeneratedLine >= gen.pendingGeneratedLine {
break
}
}
// Only need to set this once
gen.lastGeneratedCharacter = 0
} else {
if gen.lastGeneratedLine != gen.pendingGeneratedLine {
// panic rather than error as an invariant has been violated
panic("generatedLine cannot backtrack")
}
// Emit comma to separate the entry
if gen.hasLast {
gen.appendMappingCharCode(',')
}
}
// 1. Relative generated character
gen.appendBase64VLQ(gen.pendingGeneratedCharacter - gen.lastGeneratedCharacter)
gen.lastGeneratedCharacter = gen.pendingGeneratedCharacter
if gen.hasPendingSource {
// 2. Relative sourceIndex
gen.appendBase64VLQ(int(gen.pendingSourceIndex - gen.lastSourceIndex))
gen.lastSourceIndex = gen.pendingSourceIndex
// 3. Relative source line
gen.appendBase64VLQ(gen.pendingSourceLine - gen.lastSourceLine)
gen.lastSourceLine = gen.pendingSourceLine
// 4. Relative source character
gen.appendBase64VLQ(gen.pendingSourceCharacter - gen.lastSourceCharacter)
gen.lastSourceCharacter = gen.pendingSourceCharacter
if gen.hasPendingName {
// 5. Relative nameIndex
gen.appendBase64VLQ(int(gen.pendingNameIndex - gen.lastNameIndex))
gen.lastNameIndex = gen.pendingNameIndex
}
}
gen.hasLast = true
}
func (gen *Generator) addMapping(generatedLine int, generatedCharacter int, sourceIndex SourceIndex, sourceLine int, sourceCharacter int, nameIndex NameIndex) {
if gen.isNewGeneratedPosition(generatedLine, generatedCharacter) ||
gen.isBacktrackingSourcePosition(sourceIndex, sourceLine, sourceCharacter) {
gen.commitPendingMapping()
gen.pendingGeneratedLine = generatedLine
gen.pendingGeneratedCharacter = generatedCharacter
gen.hasPendingSource = false
gen.hasPendingName = false
gen.hasPending = true
}
if sourceIndex != sourceIndexNotSet && sourceLine != notSet && sourceCharacter != notSet {
gen.pendingSourceIndex = sourceIndex
gen.pendingSourceLine = sourceLine
gen.pendingSourceCharacter = sourceCharacter
gen.hasPendingSource = true
if nameIndex != nameIndexNotSet {
gen.pendingNameIndex = nameIndex
gen.hasPendingName = true
}
}
}
// Adds a mapping without source information
func (gen *Generator) AddGeneratedMapping(generatedLine int, generatedCharacter int) error {
if generatedLine < gen.pendingGeneratedLine {
return errors.New("generatedLine cannot backtrack")
}
if generatedCharacter < 0 {
return errors.New("generatedCharacter cannot be negative")
}
gen.addMapping(generatedLine, generatedCharacter, sourceIndexNotSet, notSet /*sourceLine*/, notSet /*sourceCharacter*/, nameIndexNotSet)
return nil
}
// Adds a mapping with source information
func (gen *Generator) AddSourceMapping(generatedLine int, generatedCharacter int, sourceIndex SourceIndex, sourceLine int, sourceCharacter int) error {
if generatedLine < gen.pendingGeneratedLine {
return errors.New("generatedLine cannot backtrack")
}
if generatedCharacter < 0 {
return errors.New("generatedCharacter cannot be negative")
}
if sourceIndex < 0 || int(sourceIndex) >= len(gen.sources) {
return errors.New("sourceIndex is out of range")
}
if sourceLine < 0 {
return errors.New("sourceLine cannot be negative")
}
if sourceCharacter < 0 {
return errors.New("sourceCharacter cannot be negative")
}
gen.addMapping(generatedLine, generatedCharacter, sourceIndex, sourceLine, sourceCharacter, nameIndexNotSet)
return nil
}
// Adds a mapping with source and name information
func (gen *Generator) AddNamedSourceMapping(generatedLine int, generatedCharacter int, sourceIndex SourceIndex, sourceLine int, sourceCharacter int, nameIndex NameIndex) error {
if generatedLine < gen.pendingGeneratedLine {
return errors.New("generatedLine cannot backtrack")
}
if generatedCharacter < 0 {
return errors.New("generatedCharacter cannot be negative")
}
if sourceIndex < 0 || int(sourceIndex) >= len(gen.sources) {
return errors.New("sourceIndex is out of range")
}
if sourceLine < 0 {
return errors.New("sourceLine cannot be negative")
}
if sourceCharacter < 0 {
return errors.New("sourceCharacter cannot be negative")
}
if nameIndex < 0 || int(nameIndex) >= len(gen.names) {
return errors.New("nameIndex is out of range")
}
gen.addMapping(generatedLine, generatedCharacter, sourceIndex, sourceLine, sourceCharacter, nameIndex)
return nil
}
// Gets the source map as a `RawSourceMap` object
func (gen *Generator) RawSourceMap() *RawSourceMap {
gen.commitPendingMapping()
sources := slices.Clone(gen.sources)
if sources == nil {
sources = []string{}
}
names := slices.Clone(gen.names)
if names == nil {
names = []string{}
}
return &RawSourceMap{
Version: 3,
File: gen.file,
SourceRoot: gen.sourceRoot,
Sources: sources,
Names: names,
Mappings: gen.mappings.String(),
SourcesContent: slices.Clone(gen.sourcesContent),
}
}
// Gets the string representation of the source map
func (gen *Generator) String() string {
buf, err := json.Marshal(gen.RawSourceMap())
if err != nil {
panic(err.Error())
}
return string(buf)
}
func base64FormatEncode(value int) rune {
switch {
case value >= 0 && value < 26:
return 'A' + rune(value)
case value >= 26 && value < 52:
return 'a' + rune(value) - 26
case value >= 52 && value < 62:
return '0' + rune(value) - 52
case value == 62:
return '+'
case value == 63:
return '/'
default:
panic("not a base64 value")
}
}