use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SourceMapMode {
#[default]
External,
Inline,
Both,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceMap {
pub version: u32,
pub file: String,
pub sources: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sources_content: Option<Vec<String>>,
pub names: Vec<String>,
pub mappings: String,
}
impl Default for SourceMap {
fn default() -> Self {
Self {
version: 3,
file: String::new(),
sources: Vec::new(),
sources_content: None,
names: Vec::new(),
mappings: String::new(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Mapping {
pub generated_line: usize,
pub generated_column: usize,
pub source_index: usize,
pub original_line: usize,
pub original_column: usize,
pub name_index: Option<usize>,
}
pub struct SourceMapBuilder {
file: String,
sources: Vec<String>,
sources_content: Option<Vec<String>>,
names: Vec<String>,
name_indices: HashMap<String, usize>,
mappings: Vec<Mapping>,
}
impl SourceMapBuilder {
pub fn new(output_file: String) -> Self {
Self {
file: output_file,
sources: Vec::new(),
sources_content: None,
names: Vec::new(),
name_indices: HashMap::new(),
mappings: Vec::new(),
}
}
pub fn add_source(&mut self, source_path: String) -> usize {
let index = self.sources.len();
self.sources.push(source_path);
index
}
pub fn add_source_content(&mut self, content: String) {
if self.sources_content.is_none() {
self.sources_content = Some(Vec::new());
}
if let Some(ref mut contents) = self.sources_content {
contents.push(content);
}
}
pub fn add_name(&mut self, name: String) -> usize {
if let Some(&index) = self.name_indices.get(&name) {
return index;
}
let index = self.names.len();
self.name_indices.insert(name.clone(), index);
self.names.push(name);
index
}
pub fn add_mapping(&mut self, mapping: Mapping) {
self.mappings.push(mapping);
}
pub fn build(mut self) -> SourceMap {
self.mappings.sort_by(|a, b| {
a.generated_line
.cmp(&b.generated_line)
.then(a.generated_column.cmp(&b.generated_column))
});
let mappings_string = self.encode_mappings();
SourceMap {
version: 3,
file: self.file,
sources: self.sources,
sources_content: self.sources_content,
names: self.names,
mappings: mappings_string,
}
}
fn encode_mappings(&self) -> String {
let mut result = String::new();
let mut prev_generated_line = 0;
let mut prev_generated_column = 0;
let mut prev_source_index = 0;
let mut prev_original_line = 0;
let mut prev_original_column = 0;
let mut prev_name_index = 0;
for mapping in &self.mappings {
while prev_generated_line < mapping.generated_line {
result.push(';');
prev_generated_line += 1;
prev_generated_column = 0;
}
if !result.is_empty() && !result.ends_with(';') {
result.push(',');
}
let generated_column_delta =
mapping.generated_column as i32 - prev_generated_column as i32;
result.push_str(&encode_vlq(generated_column_delta));
let source_index_delta = mapping.source_index as i32 - prev_source_index as i32;
result.push_str(&encode_vlq(source_index_delta));
let original_line_delta = mapping.original_line as i32 - prev_original_line as i32;
result.push_str(&encode_vlq(original_line_delta));
let original_column_delta =
mapping.original_column as i32 - prev_original_column as i32;
result.push_str(&encode_vlq(original_column_delta));
if let Some(name_index) = mapping.name_index {
let name_index_delta = name_index as i32 - prev_name_index as i32;
result.push_str(&encode_vlq(name_index_delta));
prev_name_index = name_index;
}
prev_generated_column = mapping.generated_column;
prev_source_index = mapping.source_index;
prev_original_line = mapping.original_line;
prev_original_column = mapping.original_column;
}
result
}
}
fn encode_vlq(value: i32) -> String {
const BASE64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
let mut vlq = if value < 0 {
((-value) << 1) | 1
} else {
value << 1
};
loop {
let mut digit = vlq & 0b11111;
vlq >>= 5;
if vlq > 0 {
digit |= 0b100000; }
result.push(BASE64_CHARS[digit as usize] as char);
if vlq == 0 {
break;
}
}
result
}
pub fn generate_source_map(
output_file: &str,
source_file: &str,
_source_content: &str,
_generated_code: &str,
) -> SourceMap {
let mut builder = SourceMapBuilder::new(output_file.to_string());
let source_index = builder.add_source(source_file.to_string());
for line in 0..100 {
builder.add_mapping(Mapping {
generated_line: line,
generated_column: 0,
source_index,
original_line: line,
original_column: 0,
name_index: None,
});
}
builder.build()
}
pub fn source_map_to_inline(source_map: &SourceMap) -> String {
let json = serde_json::to_string(source_map).unwrap_or_default();
let base64 = base64_encode(json.as_bytes());
format!(
"//# sourceMappingURL=data:application/json;charset=utf-8;base64,{}",
base64
)
}
pub fn source_map_external_reference(map_filename: &str) -> String {
format!("//# sourceMappingURL={}", map_filename)
}
pub fn apply_source_map(
code: &str,
source_map: &SourceMap,
mode: SourceMapMode,
map_filename: &str,
) -> String {
let mut result = code.to_string();
match mode {
SourceMapMode::External => {
result.push('\n');
result.push_str(&source_map_external_reference(map_filename));
result.push('\n');
}
SourceMapMode::Inline => {
result.push('\n');
result.push_str(&source_map_to_inline(source_map));
result.push('\n');
}
SourceMapMode::Both => {
result.push('\n');
result.push_str(&source_map_external_reference(map_filename));
result.push('\n');
result.push_str(&source_map_to_inline(source_map));
result.push('\n');
}
}
result
}
fn base64_encode(data: &[u8]) -> String {
const BASE64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
for chunk in data.chunks(3) {
let mut buf = [0u8; 3];
for (i, &byte) in chunk.iter().enumerate() {
buf[i] = byte;
}
result.push(BASE64_CHARS[(buf[0] >> 2) as usize] as char);
result.push(BASE64_CHARS[(((buf[0] & 0x03) << 4) | (buf[1] >> 4)) as usize] as char);
if chunk.len() > 1 {
result.push(BASE64_CHARS[(((buf[1] & 0x0f) << 2) | (buf[2] >> 6)) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(BASE64_CHARS[(buf[2] & 0x3f) as usize] as char);
} else {
result.push('=');
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_vlq() {
assert_eq!(encode_vlq(0), "A");
assert_eq!(encode_vlq(1), "C");
assert_eq!(encode_vlq(-1), "D");
assert_eq!(encode_vlq(15), "e");
assert_eq!(encode_vlq(16), "gB");
}
#[test]
fn test_source_map_builder() {
let mut builder = SourceMapBuilder::new("output.js".to_string());
let source_idx = builder.add_source("input.wj".to_string());
builder.add_mapping(Mapping {
generated_line: 0,
generated_column: 0,
source_index: source_idx,
original_line: 0,
original_column: 0,
name_index: None,
});
let source_map = builder.build();
assert_eq!(source_map.version, 3);
assert_eq!(source_map.file, "output.js");
assert_eq!(source_map.sources.len(), 1);
assert!(!source_map.mappings.is_empty());
}
#[test]
fn test_generate_source_map() {
let source_map = generate_source_map(
"output.js",
"input.wj",
"fn main() { println!(\"hello\"); }",
"function main() { console.log(\"hello\"); }",
);
assert_eq!(source_map.file, "output.js");
assert_eq!(source_map.sources[0], "input.wj");
assert!(!source_map.mappings.is_empty());
}
}