use std::borrow::Cow;
use crate::{
SourceViewToken,
decode::{JSONSourceMap, decode, decode_from_string},
encode::{encode, encode_to_string},
error::Result,
token::{Token, TokenChunk},
};
#[derive(Debug, Clone, Default)]
pub struct SourceMap<'a> {
pub(crate) file: Option<Cow<'a, str>>,
pub(crate) names: Vec<Cow<'a, str>>,
pub(crate) source_root: Option<Cow<'a, str>>,
pub(crate) sources: Vec<Cow<'a, str>>,
pub(crate) source_contents: Vec<Option<Cow<'a, str>>>,
pub(crate) tokens: Box<[Token]>,
pub(crate) token_chunks: Option<Vec<TokenChunk>>,
pub(crate) x_google_ignore_list: Option<Vec<u32>>,
pub(crate) debug_id: Option<Cow<'a, str>>,
}
impl<'a> SourceMap<'a> {
pub fn new(
file: Option<Cow<'a, str>>,
names: Vec<Cow<'a, str>>,
source_root: Option<Cow<'a, str>>,
sources: Vec<Cow<'a, str>>,
source_contents: Vec<Option<Cow<'a, str>>>,
tokens: Box<[Token]>,
token_chunks: Option<Vec<TokenChunk>>,
) -> Self {
Self {
file,
names,
source_root,
sources,
source_contents,
tokens,
token_chunks,
x_google_ignore_list: None,
debug_id: None,
}
}
pub fn from_json(value: JSONSourceMap) -> Result<SourceMap<'static>> {
decode(value)
}
pub fn from_json_string(value: &'a str) -> Result<SourceMap<'a>> {
decode_from_string(value)
}
pub fn to_json(&self) -> JSONSourceMap {
encode(self)
}
pub fn to_json_string(&self) -> String {
encode_to_string(self)
}
pub fn to_data_url(&self) -> String {
let base_64_str = base64_simd::STANDARD.encode_to_string(self.to_json_string().as_bytes());
format!("data:application/json;charset=utf-8;base64,{base_64_str}")
}
pub fn into_owned(self) -> SourceMap<'static> {
SourceMap {
file: self.file.map(|c| Cow::Owned(c.into_owned())),
names: self.names.into_iter().map(|c| Cow::Owned(c.into_owned())).collect(),
source_root: self.source_root.map(|c| Cow::Owned(c.into_owned())),
sources: self.sources.into_iter().map(|c| Cow::Owned(c.into_owned())).collect(),
source_contents: self
.source_contents
.into_iter()
.map(|opt| opt.map(|c| Cow::Owned(c.into_owned())))
.collect(),
tokens: self.tokens,
token_chunks: self.token_chunks,
x_google_ignore_list: self.x_google_ignore_list,
debug_id: self.debug_id.map(|c| Cow::Owned(c.into_owned())),
}
}
pub fn into_parts(self) -> SourceMapParts<'a> {
SourceMapParts {
file: self.file,
names: self.names,
source_root: self.source_root,
sources: self.sources,
source_contents: self.source_contents,
tokens: self.tokens,
token_chunks: self.token_chunks,
x_google_ignore_list: self.x_google_ignore_list,
debug_id: self.debug_id,
}
}
pub fn from_parts(parts: SourceMapParts<'a>) -> Self {
Self {
file: parts.file,
names: parts.names,
source_root: parts.source_root,
sources: parts.sources,
source_contents: parts.source_contents,
tokens: parts.tokens,
token_chunks: parts.token_chunks,
x_google_ignore_list: parts.x_google_ignore_list,
debug_id: parts.debug_id,
}
}
pub fn get_file(&self) -> Option<&str> {
self.file.as_deref()
}
pub fn set_file(&mut self, file: &str) {
self.file = Some(Cow::Owned(file.to_owned()));
}
pub fn get_source_root(&self) -> Option<&str> {
self.source_root.as_deref()
}
pub fn get_x_google_ignore_list(&self) -> Option<&[u32]> {
self.x_google_ignore_list.as_deref()
}
pub fn set_x_google_ignore_list(&mut self, x_google_ignore_list: Vec<u32>) {
self.x_google_ignore_list = Some(x_google_ignore_list);
}
pub fn set_debug_id(&mut self, debug_id: &str) {
self.debug_id = Some(Cow::Owned(debug_id.to_owned()));
}
pub fn get_debug_id(&self) -> Option<&str> {
self.debug_id.as_deref()
}
pub fn get_names(&self) -> impl Iterator<Item = &str> {
self.names.iter().map(AsRef::as_ref)
}
pub fn set_sources<S: AsRef<str>, I: IntoIterator<Item = S>>(&mut self, sources: I) {
self.sources = sources.into_iter().map(|s| Cow::Owned(s.as_ref().to_owned())).collect();
}
pub fn get_sources(&self) -> impl Iterator<Item = &str> {
self.sources.iter().map(AsRef::as_ref)
}
pub fn set_source_contents(&mut self, source_contents: Vec<Option<&str>>) {
self.source_contents =
source_contents.into_iter().map(|v| v.map(|s| Cow::Owned(s.to_owned()))).collect();
}
pub fn get_source_contents(&self) -> impl Iterator<Item = Option<&str>> {
self.source_contents.iter().map(|item| item.as_deref())
}
pub fn get_token(&self, index: u32) -> Option<Token> {
self.tokens.get(index as usize).copied()
}
pub fn get_source_view_token(&self, index: u32) -> Option<SourceViewToken<'_, 'a>> {
self.tokens.get(index as usize).copied().map(|token| SourceViewToken::new(token, self))
}
pub fn get_tokens(&self) -> impl Iterator<Item = Token> {
self.tokens.iter().copied()
}
pub fn get_source_view_tokens(&self) -> impl Iterator<Item = SourceViewToken<'_, 'a>> {
self.tokens.iter().map(|&token| SourceViewToken::new(token, self))
}
pub fn get_name(&self, id: u32) -> Option<&str> {
self.names.get(id as usize).map(AsRef::as_ref)
}
pub fn get_source(&self, id: u32) -> Option<&str> {
self.sources.get(id as usize).map(AsRef::as_ref)
}
pub fn get_source_content(&self, id: u32) -> Option<&str> {
self.source_contents.get(id as usize).and_then(|item| item.as_deref())
}
pub fn get_source_and_content(&self, id: u32) -> Option<(&str, &str)> {
let source = self.get_source(id)?;
let content = self.get_source_content(id)?;
Some((source, content))
}
pub fn generate_lookup_table(&self) -> Vec<LineLookupTable<'_>> {
if let Some(last_token) = self.tokens.last() {
let mut table = vec![&self.tokens[..0]; last_token.dst_line as usize + 1];
let mut prev_start_idx = 0u32;
let mut prev_dst_line = 0u32;
for (idx, token) in self.tokens.iter().enumerate() {
if token.dst_line != prev_dst_line {
table[prev_dst_line as usize] = &self.tokens[prev_start_idx as usize..idx];
prev_start_idx = idx as u32;
prev_dst_line = token.dst_line;
}
}
table[prev_dst_line as usize] = &self.tokens[prev_start_idx as usize..];
table
} else {
vec![]
}
}
pub fn lookup_token(
&self,
lookup_table: &[LineLookupTable],
line: u32,
col: u32,
) -> Option<Token> {
if line >= lookup_table.len() as u32 {
return None;
}
let token = greatest_lower_bound(lookup_table[line as usize], &(line, col), |token| {
(token.dst_line, token.dst_col)
})?;
Some(*token)
}
pub fn lookup_source_view_token(
&self,
lookup_table: &[LineLookupTable],
line: u32,
col: u32,
) -> Option<SourceViewToken<'_, 'a>> {
self.lookup_token(lookup_table, line, col).map(|token| SourceViewToken::new(token, self))
}
}
#[derive(Debug, Clone, Default)]
pub struct SourceMapParts<'a> {
pub file: Option<Cow<'a, str>>,
pub names: Vec<Cow<'a, str>>,
pub source_root: Option<Cow<'a, str>>,
pub sources: Vec<Cow<'a, str>>,
pub source_contents: Vec<Option<Cow<'a, str>>>,
pub tokens: Box<[Token]>,
pub token_chunks: Option<Vec<TokenChunk>>,
pub x_google_ignore_list: Option<Vec<u32>>,
pub debug_id: Option<Cow<'a, str>>,
}
impl<'a> From<SourceMapParts<'a>> for SourceMap<'a> {
fn from(parts: SourceMapParts<'a>) -> Self {
SourceMap::from_parts(parts)
}
}
type LineLookupTable<'a> = &'a [Token];
fn greatest_lower_bound<'a, T, K: Ord, F: Fn(&'a T) -> K>(
slice: &'a [T],
key: &K,
map: F,
) -> Option<&'a T> {
let mut idx = match slice.binary_search_by_key(key, &map) {
Ok(index) => index,
Err(index) => {
return slice.get(index.checked_sub(1)?);
}
};
for i in (0..idx).rev() {
if map(&slice[i]) == *key {
idx = i;
} else {
break;
}
}
slice.get(idx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lookup_token() {
let input = r#"{
"version": 3,
"sources": ["coolstuff.js"],
"sourceRoot": "x",
"names": ["x","alert"],
"mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM"
}"#;
let sm = SourceMap::from_json_string(input).unwrap();
let lookup_table = sm.generate_lookup_table();
assert_eq!(
sm.lookup_source_view_token(&lookup_table, 0, 0).unwrap().to_tuple(),
(Some("coolstuff.js"), 0, 0, None)
);
assert_eq!(
sm.lookup_source_view_token(&lookup_table, 0, 3).unwrap().to_tuple(),
(Some("coolstuff.js"), 0, 4, Some("x"))
);
assert_eq!(
sm.lookup_source_view_token(&lookup_table, 0, 24).unwrap().to_tuple(),
(Some("coolstuff.js"), 2, 8, None)
);
assert_eq!(
sm.lookup_source_view_token(&lookup_table, 0, 1000).unwrap().to_tuple(),
(Some("coolstuff.js"), 2, 8, None)
);
assert!(sm.lookup_source_view_token(&lookup_table, 1000, 0).is_none());
assert!(sm.lookup_token(&lookup_table, 0, 0).is_some());
}
#[test]
fn source_view_token() {
let sm = SourceMap::new(
None,
vec![Cow::Borrowed("foo")],
None,
vec![Cow::Borrowed("foo.js")],
vec![],
vec![Token::new(1, 1, 1, 1, Some(0), Some(0))].into_boxed_slice(),
None,
);
let mut source_view_tokens = sm.get_source_view_tokens();
assert_eq!(
source_view_tokens.next().unwrap().to_tuple(),
(Some("foo.js"), 1, 1, Some("foo"))
);
assert!(sm.get_source_view_token(0).is_some());
assert!(sm.get_source_view_token(99).is_none());
}
#[test]
fn mut_sourcemap() {
let mut sm = SourceMap::default();
sm.set_file("index.js");
sm.set_sources(vec!["foo.js"]);
sm.set_source_contents(vec![Some("foo")]);
assert_eq!(sm.get_file(), Some("index.js"));
assert_eq!(sm.get_source(0), Some("foo.js"));
assert_eq!(sm.get_source_content(0), Some("foo"));
}
#[test]
fn from_json_value() {
let json = SourceMap::from_json_string(
r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#,
)
.unwrap()
.to_json();
let sm = SourceMap::from_json(json).unwrap();
assert_eq!(sm.get_source(0), Some("a.js"));
}
#[test]
fn to_data_url() {
let sm =
SourceMap::from_json_string(r#"{"version":3,"sources":[],"names":[],"mappings":""}"#)
.unwrap();
assert!(sm.to_data_url().starts_with("data:application/json;charset=utf-8;base64,"));
}
#[test]
fn source_and_content() {
let sm = SourceMap::from_json_string(
r#"{"version":3,"sources":["a.js"],"sourcesContent":["CONTENT"],"names":[],"mappings":"AAAA"}"#,
)
.unwrap();
assert_eq!(sm.get_source_and_content(0), Some(("a.js", "CONTENT")));
assert_eq!(sm.get_source_and_content(99), None);
let no_content = SourceMap::new(
None,
vec![],
None,
vec![Cow::Borrowed("a.js")],
vec![],
vec![].into_boxed_slice(),
None,
);
assert_eq!(no_content.get_source(0), Some("a.js"));
assert_eq!(no_content.get_source_and_content(0), None);
}
#[test]
fn parts_roundtrip() {
let sm = SourceMap::from_json_string(
r#"{"version":3,"file":"f.js","sources":["a.js"],"names":["n"],"mappings":"AAAAA"}"#,
)
.unwrap();
let parts = sm.into_parts();
assert_eq!(parts.file.as_deref(), Some("f.js"));
let rebuilt = SourceMap::from_parts(parts);
assert_eq!(rebuilt.get_file(), Some("f.js"));
let from_parts: SourceMap = rebuilt.into_parts().into();
assert_eq!(from_parts.get_source(0), Some("a.js"));
}
#[test]
fn empty_lookup_table() {
let sm = SourceMap::default();
assert!(sm.generate_lookup_table().is_empty());
assert!(sm.lookup_token(&[], 0, 0).is_none());
}
#[test]
fn lookup_before_first_token() {
let sm = SourceMap::new(
None,
vec![],
None,
vec![Cow::Borrowed("a.js")],
vec![],
vec![Token::new(0, 5, 0, 0, Some(0), None)].into_boxed_slice(),
None,
);
let table = sm.generate_lookup_table();
assert!(sm.lookup_token(&table, 0, 0).is_none());
}
#[test]
fn lookup_with_duplicate_columns() {
let mut tokens = vec![Token::new(0, 0, 9, 9, Some(0), None)];
for src_line in 1..=5 {
tokens.push(Token::new(0, 7, src_line, 0, Some(0), None));
}
let sm = SourceMap::new(
None,
vec![],
None,
vec![Cow::Borrowed("a.js")],
vec![],
tokens.into_boxed_slice(),
None,
);
let table = sm.generate_lookup_table();
assert_eq!(sm.lookup_token(&table, 0, 7).unwrap().get_src_line(), 1);
}
}