///|
/// One decoded Source Map segment. Lines and columns are zero-based.
pub struct Mapping {
generated_line : Int
generated_column : Int
source_index : Int?
original_line : Int?
original_column : Int?
name_index : Int?
} derive(Eq, Debug)
///|
/// Construct one generated-only or original-source mapping segment.
pub fn Mapping::make(
generated_line : Int,
generated_column : Int,
source_index? : Int? = None,
original_line? : Int? = None,
original_column? : Int? = None,
name_index? : Int? = None,
) -> Mapping {
{
generated_line,
generated_column,
source_index,
original_line,
original_column,
name_index,
}
}
///|
/// A resolved mapping suitable for debugger and editor integrations.
pub struct ResolvedMapping {
generated_line : Int
generated_column : Int
source : String?
original_line : Int?
original_column : Int?
name : String?
} derive(Eq, Debug)
///|
/// Select which neighboring mapping a position query returns.
pub(all) enum MappingBias {
GreatestLowerBound
LeastUpperBound
} derive(Eq, Debug)
///|
/// A decoded Source Map v3 core model.
pub struct SourceMap {
file : String?
source_root : String?
sources : Array[String]
sources_content : Array[String?]
names : Array[String]
mappings : Array[Mapping]
}
///|
/// Build and validate a decoded Source Map.
pub fn SourceMap::make(
sources : Array[String],
names : Array[String],
mappings : Array[Mapping],
file? : String? = None,
source_root? : String? = None,
sources_content? : Array[String?] = [],
) -> SourceMap raise SourceMapError {
if sources_content.length() != 0 &&
sources_content.length() != sources.length() {
raise SourceMapError::At(
0, "sources_content length must be zero or match sources length",
)
}
let mut previous_line = 0
let mut previous_column = -1
for i in 0.. Unit raise SourceMapError {
match (mapping.source_index, mapping.original_line, mapping.original_column) {
(None, None, None) => ()
(Some(source), Some(line), Some(column)) => {
if source < 0 || source >= source_count {
raise SourceMapError::At(offset, "source index is out of bounds")
}
if line < 0 || column < 0 {
raise SourceMapError::At(
offset, "original positions must be non-negative",
)
}
}
_ =>
raise SourceMapError::At(
offset, "source, original line and original column must appear together",
)
}
match mapping.name_index {
Some(_) if mapping.source_index is None =>
raise SourceMapError::At(offset, "name requires an original position")
Some(name) if name < 0 || name >= name_count =>
raise SourceMapError::At(offset, "name index is out of bounds")
_ => ()
}
}
///|
/// Parse a Source Map v3 `mappings` string using the supplied tables.
pub fn parse_mappings(
encoded : String,
sources : Array[String],
names : Array[String],
) -> SourceMap raise SourceMapError {
let mappings : Array[Mapping] = Array::new()
let mut generated_line = 0
let mut previous_generated_column = 0
let mut previous_source = 0
let mut previous_original_line = 0
let mut previous_original_column = 0
let mut previous_name = 0
let mut start = 0
for offset in 0..<=encoded.length() {
let delimiter = if offset == encoded.length() {
';'
} else {
encoded.unsafe_get(offset).to_int().unsafe_to_char()
}
if delimiter == ',' || delimiter == ';' {
if offset > start {
let values = decode_vlq_values(encoded[start:offset].to_owned()) catch {
error => raise shift_error(error, start)
}
if values.length() != 1 && values.length() != 4 && values.length() != 5 {
raise SourceMapError::At(
start, "a segment must contain 1, 4, or 5 fields",
)
}
previous_generated_column += values[0]
if previous_generated_column < 0 {
raise SourceMapError::At(start, "generated column became negative")
}
let mut source : Int? = None
let mut original_line : Int? = None
let mut original_column : Int? = None
let mut name : Int? = None
if values.length() >= 4 {
previous_source += values[1]
previous_original_line += values[2]
previous_original_column += values[3]
source = Some(previous_source)
original_line = Some(previous_original_line)
original_column = Some(previous_original_column)
if values.length() == 5 {
previous_name += values[4]
name = Some(previous_name)
}
}
mappings.push({
generated_line,
generated_column: previous_generated_column,
source_index: source,
original_line,
original_column,
name_index: name,
})
}
start = offset + 1
if delimiter == ';' && offset < encoded.length() {
generated_line += 1
previous_generated_column = 0
}
}
}
SourceMap::make(sources, names, mappings)
}
///|
fn shift_error(error : SourceMapError, base : Int) -> SourceMapError {
match error {
At(offset, message) => At(base + offset, message)
}
}
///|
/// Encode decoded mappings into a canonical Source Map v3 mappings string.
pub fn SourceMap::encode_mappings(self : SourceMap) -> String {
let mut out = ""
let mut generated_line = 0
let mut previous_generated_column = 0
let mut previous_source = 0
let mut previous_original_line = 0
let mut previous_original_column = 0
let mut previous_name = 0
let mut first_in_line = true
for mapping in self.mappings {
while generated_line < mapping.generated_line {
out = out + ";"
generated_line += 1
previous_generated_column = 0
first_in_line = true
}
if !first_in_line {
out = out + ","
}
out = out + encode_vlq(mapping.generated_column - previous_generated_column)
previous_generated_column = mapping.generated_column
match
(mapping.source_index, mapping.original_line, mapping.original_column) {
(Some(source), Some(line), Some(column)) => {
out = out + encode_vlq(source - previous_source)
out = out + encode_vlq(line - previous_original_line)
out = out + encode_vlq(column - previous_original_column)
previous_source = source
previous_original_line = line
previous_original_column = column
match mapping.name_index {
Some(name) => {
out = out + encode_vlq(name - previous_name)
previous_name = name
}
None => ()
}
}
_ => ()
}
first_in_line = false
}
out
}
///|
fn SourceMap::resolve(self : SourceMap, mapping : Mapping) -> ResolvedMapping {
let source = match mapping.source_index {
Some(index) => Some(self.sources[index])
None => None
}
let name = match mapping.name_index {
Some(index) => Some(self.names[index])
None => None
}
{
generated_line: mapping.generated_line,
generated_column: mapping.generated_column,
source,
original_line: mapping.original_line,
original_column: mapping.original_column,
name,
}
}
///|
/// Find a mapping neighboring a generated position on the same line.
pub fn SourceMap::original_position_for_with_bias(
self : SourceMap,
line : Int,
column : Int,
bias : MappingBias,
) -> ResolvedMapping? {
let mut low = 0
let mut high = self.mappings.length()
while low < high {
let mid = low + (high - low) / 2
let mapping = self.mappings[mid]
let column_precedes = match bias {
GreatestLowerBound => mapping.generated_column <= column
LeastUpperBound => mapping.generated_column < column
}
if mapping.generated_line < line ||
(mapping.generated_line == line && column_precedes) {
low = mid + 1
} else {
high = mid
}
}
let index = match bias {
GreatestLowerBound => low - 1
LeastUpperBound => low
}
if index < 0 ||
index >= self.mappings.length() ||
self.mappings[index].generated_line != line {
return None
}
Some(self.resolve(self.mappings[index]))
}
///|
/// Find the greatest mapping at or before a generated position on the same line.
pub fn SourceMap::original_position_for(
self : SourceMap,
line : Int,
column : Int,
) -> ResolvedMapping? {
self.original_position_for_with_bias(line, column, GreatestLowerBound)
}
///|
/// Find a mapping neighboring an original position on the same source line.
pub fn SourceMap::generated_position_for_with_bias(
self : SourceMap,
source : String,
line : Int,
column : Int,
bias : MappingBias,
) -> ResolvedMapping? {
let mut best : Mapping? = None
for mapping in self.mappings {
match
(mapping.source_index, mapping.original_line, mapping.original_column) {
(Some(source_index), Some(original_line), Some(original_column)) => {
let column_matches = match bias {
GreatestLowerBound => original_column <= column
LeastUpperBound => original_column >= column
}
if self.sources[source_index] == source &&
original_line == line &&
column_matches {
match best {
Some(current) =>
match current.original_column {
Some(current_column) => {
let current_is_better = match bias {
GreatestLowerBound => current_column >= original_column
LeastUpperBound => current_column <= original_column
}
if !current_is_better {
best = Some(mapping)
}
}
None => best = Some(mapping)
}
None => best = Some(mapping)
}
}
}
_ => ()
}
}
match best {
Some(mapping) => Some(self.resolve(mapping))
None => None
}
}
///|
/// Find the closest mapping at or before an original column on the same source line.
pub fn SourceMap::generated_position_for(
self : SourceMap,
source : String,
line : Int,
column : Int,
) -> ResolvedMapping? {
self.generated_position_for_with_bias(
source,
line,
column,
GreatestLowerBound,
)
}
///|
/// Find every generated position that maps to an exact original position.
/// This preserves generated mapping order and supports one-to-many mappings.
pub fn SourceMap::generated_positions_for(
self : SourceMap,
source : String,
line : Int,
column : Int,
) -> Array[ResolvedMapping] {
let results : Array[ResolvedMapping] = Array::new()
if line < 0 || column < 0 {
return results
}
for mapping in self.mappings {
match
(mapping.source_index, mapping.original_line, mapping.original_column) {
(Some(source_index), Some(original_line), Some(original_column)) =>
if self.sources[source_index] == source &&
original_line == line &&
original_column == column {
results.push(self.resolve(mapping))
}
_ => ()
}
}
results
}