use super::line_endings::line_break_len;
use alloc::vec::Vec;
use vize_l0::{FxHashMap, String, ToCompactString};
#[derive(Debug, Clone, Copy)]
struct Segment {
generated_offset: u32,
source_offset: u32,
name: Option<u32>,
}
#[derive(Debug, Default)]
pub struct SourceMapBuilder {
segments: Vec<Segment>,
names: Vec<String>,
name_index: FxHashMap<String, u32>,
}
impl SourceMapBuilder {
pub fn new() -> Self {
Self {
segments: Vec::new(),
names: Vec::new(),
name_index: FxHashMap::default(),
}
}
pub fn add_raw(&mut self, generated_offset: usize, source_offset: u32) {
self.segments.push(Segment {
generated_offset: generated_offset as u32,
source_offset,
name: None,
});
}
pub fn add_named(&mut self, generated_offset: usize, source_offset: u32, name: &str) {
let index = self.intern_name(name);
self.segments.push(Segment {
generated_offset: generated_offset as u32,
source_offset,
name: Some(index),
});
}
fn intern_name(&mut self, name: &str) -> u32 {
if let Some(&index) = self.name_index.get(name) {
return index;
}
let index = self.names.len() as u32;
self.names.push(name.to_compact_string());
self.name_index.insert(name.to_compact_string(), index);
index
}
pub fn finish(mut self, generated_code: &str, filename: &str, source_content: &str) -> String {
self.segments.sort_by_key(|s| s.generated_offset);
let resolved = resolve_positions(generated_code, source_content, &self.segments);
let mappings = encode_mappings(&resolved);
let names: Vec<&str> = self.names.iter().map(String::as_str).collect();
let doc = serde_json::json!({
"version": 3,
"file": filename,
"sources": [filename],
"sourcesContent": [source_content],
"names": names,
"mappings": mappings.as_str(),
});
serde_json::to_string(&doc)
.unwrap_or_default()
.to_compact_string()
}
}
#[derive(Debug, Clone, Copy)]
struct ResolvedSegment {
generated_line: u32,
generated_column: u32,
source_line: u32,
source_column: u32,
name: Option<u32>,
}
fn resolve_positions(code: &str, source: &str, segments: &[Segment]) -> Vec<ResolvedSegment> {
let code_bytes = code.as_bytes();
let source_line_starts = line_start_table(source);
let mut resolved = Vec::with_capacity(segments.len());
let mut cursor = 0usize; let mut gen_line = 0u32;
let mut gen_line_start = 0usize;
for seg in segments {
let target =
floor_char_boundary(code, (seg.generated_offset as usize).min(code_bytes.len()));
while cursor < target {
let width = line_break_len(code, cursor);
if width > 0 && cursor + width <= target {
cursor += width;
gen_line += 1;
gen_line_start = cursor;
} else {
cursor += 1;
}
}
let generated_column = utf16_len(code.get(gen_line_start..target).unwrap_or_default());
let (source_line, source_column) =
resolve_in_table(source, &source_line_starts, seg.source_offset as usize);
resolved.push(ResolvedSegment {
generated_line: gen_line,
generated_column,
source_line,
source_column,
name: seg.name,
});
}
resolved
}
fn line_start_table(text: &str) -> Vec<usize> {
let mut starts = Vec::with_capacity(16);
starts.push(0);
let mut cursor = 0;
while cursor < text.len() {
let width = line_break_len(text, cursor);
cursor += width.max(1);
if width > 0 {
starts.push(cursor);
}
}
starts
}
fn resolve_in_table(text: &str, line_starts: &[usize], offset: usize) -> (u32, u32) {
let offset = floor_char_boundary(text, offset.min(text.len()));
let line = match line_starts.binary_search(&offset) {
Ok(i) => i,
Err(i) => i.saturating_sub(1), };
let line_start = line_starts.get(line).copied().unwrap_or(0);
let column = utf16_len(text.get(line_start..offset).unwrap_or_default());
(line as u32, column)
}
fn floor_char_boundary(text: &str, mut offset: usize) -> usize {
while offset > 0 && !text.is_char_boundary(offset) {
offset -= 1;
}
offset
}
#[inline]
fn utf16_len(s: &str) -> u32 {
let mut n = 0u32;
for ch in s.chars() {
n += ch.len_utf16() as u32;
}
n
}
fn encode_mappings(segments: &[ResolvedSegment]) -> String {
let mut out = String::with_capacity(segments.len() * 6);
let mut current_line = 0u32;
let mut prev_generated_column = 0i64;
let mut prev_source_index = 0i64;
let mut prev_source_line = 0i64;
let mut prev_source_column = 0i64;
let mut prev_name_index = 0i64;
let mut first_in_line = true;
for seg in segments {
while current_line < seg.generated_line {
out.push(';');
current_line += 1;
prev_generated_column = 0;
first_in_line = true;
}
if first_in_line {
first_in_line = false;
} else {
out.push(',');
}
let gen_col = seg.generated_column as i64;
let src_index = 0i64; let src_line = seg.source_line as i64;
let src_col = seg.source_column as i64;
encode_vlq(&mut out, gen_col - prev_generated_column);
encode_vlq(&mut out, src_index - prev_source_index);
encode_vlq(&mut out, src_line - prev_source_line);
encode_vlq(&mut out, src_col - prev_source_column);
if let Some(name) = seg.name {
let name_index = name as i64;
encode_vlq(&mut out, name_index - prev_name_index);
prev_name_index = name_index;
}
prev_generated_column = gen_col;
prev_source_index = src_index;
prev_source_line = src_line;
prev_source_column = src_col;
}
out
}
mod vlq;
use vlq::encode_vlq;
#[cfg(test)]
mod tests;