use vize_carton::FxHashMap;
use vize_carton::String;
use vize_carton::ToCompactString;
#[derive(Debug, Clone, Copy)]
struct Segment {
generated_offset: u32,
source_offset: u32,
name: Option<u32>,
}
#[derive(Debug, Default)]
pub(crate) struct SourceMapBuilder {
segments: Vec<Segment>,
names: Vec<String>,
name_index: FxHashMap<String, u32>,
}
impl SourceMapBuilder {
pub(crate) fn new() -> Self {
Self {
segments: Vec::new(),
names: Vec::new(),
name_index: FxHashMap::default(),
}
}
pub(crate) 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(crate) 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(crate) 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: std::vec::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 = (seg.generated_offset as usize).min(code_bytes.len());
while cursor < target {
if code_bytes[cursor] == b'\n' {
gen_line += 1;
gen_line_start = cursor + 1;
}
cursor += 1;
}
let generated_column = utf16_len(&code[gen_line_start..target]);
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);
for (i, &b) in text.as_bytes().iter().enumerate() {
if b == b'\n' {
starts.push(i + 1);
}
}
starts
}
fn resolve_in_table(text: &str, line_starts: &[usize], offset: usize) -> (u32, u32) {
let offset = offset.min(text.len());
let line = match line_starts.binary_search(&offset) {
Ok(i) => i,
Err(i) => i - 1, };
let line_start = line_starts[line];
let column = utf16_len(&text[line_start..offset]);
(line as u32, column)
}
#[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
}
const BASE64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn encode_vlq(out: &mut String, value: i64) {
let mut vlq: u64 = if value < 0 {
((-value as u64) << 1) | 1
} else {
(value as u64) << 1
};
loop {
let mut digit = (vlq & 0b1_1111) as usize;
vlq >>= 5;
if vlq != 0 {
digit |= 0b10_0000;
}
out.push(BASE64_CHARS[digit] as char);
if vlq == 0 {
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn decode_vlq(chars: &[u8]) -> (i64, usize) {
let mut result: u64 = 0;
let mut shift = 0u32;
let mut consumed = 0usize;
for &c in chars {
let digit = BASE64_CHARS.iter().position(|&b| b == c).unwrap() as u64;
consumed += 1;
let has_continuation = (digit & 0b10_0000) != 0;
result |= (digit & 0b1_1111) << shift;
shift += 5;
if !has_continuation {
break;
}
}
let negative = (result & 1) != 0;
let magnitude = (result >> 1) as i64;
(if negative { -magnitude } else { magnitude }, consumed)
}
fn roundtrip(value: i64) {
let mut s = String::default();
encode_vlq(&mut s, value);
let (decoded, consumed) = decode_vlq(s.as_bytes());
assert_eq!(decoded, value, "VLQ roundtrip failed for {value}");
assert_eq!(consumed, s.len(), "consumed != encoded len for {value}");
}
#[test]
fn vlq_roundtrip_basic() {
for v in [
0, 1, -1, 15, 16, -16, 17, -17, 1000, -1000, 123_456, -123_456,
] {
roundtrip(v);
}
}
#[test]
fn vlq_known_encodings() {
let mut s = String::default();
encode_vlq(&mut s, 0);
assert_eq!(s.as_str(), "A");
s = String::default();
encode_vlq(&mut s, 1);
assert_eq!(s.as_str(), "C");
s = String::default();
encode_vlq(&mut s, -1);
assert_eq!(s.as_str(), "D");
s = String::default();
encode_vlq(&mut s, 16);
assert_eq!(s.as_str(), "gB");
}
#[test]
fn utf16_len_counts_surrogates() {
assert_eq!(utf16_len("abc"), 3);
assert_eq!(utf16_len("é"), 1); assert_eq!(utf16_len("𝟘"), 2); }
#[test]
fn resolve_generated_side_multiline() {
let code = "line0\nlinX1\nli";
let segs = [
Segment {
generated_offset: 9,
source_offset: 0,
name: None,
},
Segment {
generated_offset: 12,
source_offset: 0,
name: None,
},
];
let resolved = resolve_positions(code, "", &segs);
assert_eq!(
(resolved[0].generated_line, resolved[0].generated_column),
(1, 3)
);
assert_eq!(
(resolved[1].generated_line, resolved[1].generated_column),
(2, 0)
);
}
#[test]
fn resolve_source_offset_to_line_column() {
let source = "<div>\n {{ msg }}\n</div>";
let starts = line_start_table(source);
assert_eq!(resolve_in_table(source, &starts, 11), (1, 5));
assert_eq!(resolve_in_table(source, &starts, 0), (0, 0));
assert_eq!(resolve_in_table(source, &starts, 18), (2, 0));
}
#[test]
fn finish_produces_valid_v3_doc() {
let mut b = SourceMapBuilder::new();
b.add_raw(0, 8);
let code = "_ctx.msg";
let json = b.finish(code, "template.vue", "<div>{{ msg }}</div>");
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["version"], 3);
assert_eq!(parsed["sources"][0], "template.vue");
assert_eq!(parsed["sourcesContent"][0], "<div>{{ msg }}</div>");
let mappings = parsed["mappings"].as_str().unwrap();
let (gen_col, c1) = decode_vlq(mappings.as_bytes());
let (src_idx, c2) = decode_vlq(&mappings.as_bytes()[c1..]);
let (src_line, c3) = decode_vlq(&mappings.as_bytes()[c1 + c2..]);
let (src_col, _) = decode_vlq(&mappings.as_bytes()[c1 + c2 + c3..]);
assert_eq!((gen_col, src_idx, src_line, src_col), (0, 0, 0, 8));
}
#[test]
fn named_segment_populates_names_and_fifth_field() {
let mut b = SourceMapBuilder::new();
b.add_named(0, 5, "id");
let code = "id: \"app\"";
let json = b.finish(code, "Foo.vue", r#"<div id="app">"#);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["names"][0], "id");
let mappings = parsed["mappings"].as_str().unwrap();
let bytes = mappings.as_bytes();
let (gen_col, c1) = decode_vlq(bytes);
let (src_idx, c2) = decode_vlq(&bytes[c1..]);
let (src_line, c3) = decode_vlq(&bytes[c1 + c2..]);
let (src_col, c4) = decode_vlq(&bytes[c1 + c2 + c3..]);
let consumed = c1 + c2 + c3 + c4;
assert!(consumed < bytes.len(), "a 5th VLQ field must be present");
let (name_idx, c5) = decode_vlq(&bytes[consumed..]);
assert_eq!(
(gen_col, src_idx, src_line, src_col, name_idx),
(0, 0, 0, 5, 0)
);
assert_eq!(consumed + c5, bytes.len(), "no trailing bytes after name");
}
#[test]
fn intern_name_deduplicates() {
let mut b = SourceMapBuilder::new();
b.add_named(0, 0, "id");
b.add_named(10, 4, "id");
b.add_named(20, 8, "class");
let json = b.finish("0123456789012345678901234", "Foo.vue", "");
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let names = parsed["names"].as_array().unwrap();
assert_eq!(names.len(), 2, "`id` should be deduplicated");
assert_eq!(names[0], "id");
assert_eq!(names[1], "class");
}
}