///|
/// Precomputed indexes for repeated source-map queries.
pub(all) struct SourceMapConsumer {
map : DecodedSourceMap
line_starts : Array[Int]
reverse : Map[Int, Array[Int]]
}
///|
/// Summary metrics useful for CI reports and producer diagnostics.
pub(all) struct SourceMapStats {
sources : Int
mappings : Int
mapped : Int
generated_only : Int
named : Int
ignored_sources : Int
generated_lines : Int
original_lines : Int
}
///|
fn max_generated_line(mappings : ArrayView[Mapping]) -> Int {
let mut result = -1
for mapping in mappings {
if mapping.generated.line > result {
result = mapping.generated.line
}
}
result
}
///|
fn build_line_starts(mappings : ArrayView[Mapping]) -> Array[Int] {
let maximum = max_generated_line(mappings)
if maximum < 0 {
return [0]
}
let starts = Array::make(maximum + 2, mappings.length())
let mut mapping_index = 0
for line in 0..<=maximum {
starts[line] = mapping_index
while mapping_index < mappings.length() &&
mappings[mapping_index].generated.line == line {
mapping_index = mapping_index + 1
}
}
starts[maximum + 1] = mappings.length()
starts
}
///|
fn build_reverse_index(mappings : ArrayView[Mapping]) -> Map[Int, Array[Int]] {
let reverse : Map[Int, Array[Int]] = Map([])
for index, mapping in mappings {
match mapping.original {
Some(original) =>
match reverse.get(original.source_index) {
Some(indices) => indices.push(index)
None => reverse[original.source_index] = [index]
}
None => ()
}
}
reverse
}
///|
/// Construct a consumer, sorting a private mapping copy when necessary.
pub fn SourceMapConsumer::new(map : DecodedSourceMap) -> SourceMapConsumer {
let normalized = if mappings_are_sorted(map.mappings) {
DecodedSourceMap::new(
file?=map.file,
sources=map.sources.copy(),
mappings=map.mappings.copy(),
)
} else {
DecodedSourceMap::new(
file?=map.file,
sources=map.sources.copy(),
mappings=sort_mappings(map.mappings),
)
}
{
map: normalized,
line_starts: build_line_starts(normalized.mappings),
reverse: build_reverse_index(normalized.mappings),
}
}
///|
/// Parse and flatten a document directly into a consumer.
pub fn SourceMapConsumer::from_document(
document : SourceMapDocument,
map_url? : String,
) -> SourceMapConsumer raise SourceMapError {
SourceMapConsumer::new(decode_document(document, map_url?))
}
///|
fn consumer_line_range(consumer : SourceMapConsumer, line : Int) -> (Int, Int)? {
if line < 0 || line + 1 >= consumer.line_starts.length() {
None
} else {
Some((consumer.line_starts[line], consumer.line_starts[line + 1]))
}
}
///|
fn lower_bound_range(
mappings : ArrayView[Mapping],
start : Int,
end : Int,
column : Int,
) -> Int {
let mut low = start
let mut high = end
while low < high {
let middle = low + (high - low) / 2
if mappings[middle].generated.column < column {
low = middle + 1
} else {
high = middle
}
}
low
}
///|
fn upper_bound_range(
mappings : ArrayView[Mapping],
start : Int,
end : Int,
column : Int,
) -> Int {
let mut low = start
let mut high = end
while low < high {
let middle = low + (high - low) / 2
if mappings[middle].generated.column <= column {
low = middle + 1
} else {
high = middle
}
}
low
}
///|
/// Query a generated position using the precomputed line index.
pub fn SourceMapConsumer::original_position_for(
self : SourceMapConsumer,
generated~ : Position,
bias? : LookupBias = GreatestLowerBound,
) -> Mapping? {
if !generated.is_valid() {
return None
}
let (start, end) = match consumer_line_range(self, generated.line) {
Some(range) => range
None => return None
}
if start == end {
return None
}
match bias {
GreatestLowerBound => {
let index = upper_bound_range(
self.map.mappings,
start,
end,
generated.column,
) -
1
if index < start {
None
} else {
Some(self.map.mappings[index])
}
}
LeastUpperBound => {
let index = lower_bound_range(
self.map.mappings,
start,
end,
generated.column,
)
if index >= end {
None
} else {
Some(self.map.mappings[index])
}
}
}
}
///|
/// Run ECMA-426 `GetOriginalPositions` against the consumer's sorted mapping
/// copy.
pub fn SourceMapConsumer::get_original_positions(
self : SourceMapConsumer,
generated~ : Position,
) -> Array[OriginalPosition?] {
get_original_positions(self.map, generated~)
}
///|
/// Query multiple positions without rebuilding indexes.
pub fn SourceMapConsumer::original_positions_for(
self : SourceMapConsumer,
positions : ArrayView[Position],
bias? : LookupBias = GreatestLowerBound,
) -> Array[Mapping?] {
positions.map(position => {
self.original_position_for(generated=position, bias~)
})
}
///|
/// Return mappings for one source, optionally filtered by original line.
pub fn SourceMapConsumer::generated_positions_for(
self : SourceMapConsumer,
source_index~ : Int,
line? : Int,
column? : Int,
) -> Array[Mapping] {
let result : Array[Mapping] = []
match self.reverse.get(source_index) {
Some(indices) =>
for index in indices {
let mapping = self.map.mappings[index]
match mapping.original {
Some(original) =>
if (line is None || line == Some(original.line)) &&
(column is None || column == Some(original.column)) {
result.push(mapping)
}
None => ()
}
}
None => ()
}
result
}
///|
/// Find the first source index with an exact URL.
pub fn SourceMapConsumer::source_index_for_url(
self : SourceMapConsumer,
url : String,
) -> Int? {
for index, source in self.map.sources {
if source.url == Some(url) {
return Some(index)
}
}
None
}
///|
/// Return every source index sharing a URL.
pub fn SourceMapConsumer::source_indices_for_url(
self : SourceMapConsumer,
url : String,
) -> Array[Int] {
let result : Array[Int] = []
for index, source in self.map.sources {
if source.url == Some(url) {
result.push(index)
}
}
result
}
///|
/// Return aggregate map statistics.
pub fn SourceMapConsumer::stats(self : SourceMapConsumer) -> SourceMapStats {
let mut mapped = 0
let mut generated_only = 0
let mut named = 0
let mut ignored_sources = 0
let mut original_lines = 0
for source in self.map.sources {
if source.ignored {
ignored_sources = ignored_sources + 1
}
}
for mapping in self.map.mappings {
match mapping.original {
Some(original) => {
mapped = mapped + 1
if original.line + 1 > original_lines {
original_lines = original.line + 1
}
}
None => generated_only = generated_only + 1
}
if mapping.name is Some(_) {
named = named + 1
}
}
{
sources: self.map.sources.length(),
mappings: self.map.mappings.length(),
mapped,
generated_only,
named,
ignored_sources,
generated_lines: max_generated_line(self.map.mappings) + 1,
original_lines,
}
}
///|
/// Convert statistics to stable JSON.
pub fn SourceMapStats::to_json(self : SourceMapStats) -> Json {
Json::object({
"sources": Json::number(self.sources.to_double()),
"mappings": Json::number(self.mappings.to_double()),
"mapped": Json::number(self.mapped.to_double()),
"generated_only": Json::number(self.generated_only.to_double()),
"named": Json::number(self.named.to_double()),
"ignored_sources": Json::number(self.ignored_sources.to_double()),
"generated_lines": Json::number(self.generated_lines.to_double()),
"original_lines": Json::number(self.original_lines.to_double()),
})
}