oxc/crates/oxc_sourcemap/src/encode.rs

224 lines
7.6 KiB
Rust

#[cfg(feature = "concurrent")]
use rayon::prelude::*;
use crate::error::{Error, Result};
use crate::JSONSourceMap;
/// Port from https://github.com/getsentry/rust-sourcemap/blob/master/src/encoder.rs
/// It is a helper for encode `SourceMap` to vlq sourcemap string, but here some different.
/// - Quote `source_content` at parallel.
/// - If you using `ConcatSourceMapBuilder`, serialize `tokens` to vlq `mappings` at parallel.
use crate::{token::TokenChunk, SourceMap, Token};
pub fn encode(sourcemap: &SourceMap) -> JSONSourceMap {
JSONSourceMap {
file: sourcemap.get_file().map(ToString::to_string),
mappings: Some(serialize_sourcemap_mappings(sourcemap)),
source_root: sourcemap.get_source_root().map(ToString::to_string),
sources: Some(sourcemap.sources.iter().map(ToString::to_string).map(Some).collect()),
sources_content: sourcemap
.source_contents
.as_ref()
.map(|x| x.iter().map(ToString::to_string).map(Some).collect()),
names: Some(sourcemap.names.iter().map(ToString::to_string).collect()),
}
}
// Here using `serde_json::to_string` to serialization `names/source_contents/sources`.
// It will escape the string to avoid invalid JSON string.
pub fn encode_to_string(sourcemap: &SourceMap) -> Result<String> {
let mut buf = String::new();
buf.push_str("{\"version\":3,");
if let Some(file) = sourcemap.get_file() {
buf.push_str("\"file\":\"");
buf.push_str(file);
buf.push_str("\",");
}
if let Some(source_root) = sourcemap.get_source_root() {
buf.push_str("\"sourceRoot\":\"");
buf.push_str(source_root);
buf.push_str("\",");
}
buf.push_str("\"names\":[");
let names = sourcemap
.names
.iter()
.map(|x| serde_json::to_string(x.as_ref()))
.collect::<std::result::Result<Vec<_>, serde_json::Error>>()
.map_err(Error::from)?;
buf.push_str(&names.join(","));
buf.push_str("],\"sources\":[");
let sources = sourcemap
.sources
.iter()
.map(|x| serde_json::to_string(x.as_ref()))
.collect::<std::result::Result<Vec<_>, serde_json::Error>>()
.map_err(Error::from)?;
buf.push_str(&sources.join(","));
// Quote `source_content` at parallel.
if let Some(source_contents) = &sourcemap.source_contents {
buf.push_str("],\"sourcesContent\":[");
cfg_if::cfg_if! {
if #[cfg(feature = "concurrent")] {
let quote_source_contents = source_contents
.par_iter()
.map(|x| serde_json::to_string(x.as_ref()))
.collect::<std::result::Result<Vec<_>, serde_json::Error>>()
.map_err(Error::from)?;
} else {
let quote_source_contents = source_contents
.iter()
.map(|x| serde_json::to_string(x.as_ref()))
.collect::<std::result::Result<Vec<_>, serde_json::Error>>()
.map_err(Error::from)?;
}
};
buf.push_str(&quote_source_contents.join(","));
}
if let Some(x_google_ignore_list) = &sourcemap.x_google_ignore_list {
buf.push_str("],\"x_google_ignoreList\":[");
let x_google_ignore_list =
x_google_ignore_list.iter().map(ToString::to_string).collect::<Vec<_>>();
buf.push_str(&x_google_ignore_list.join(","));
}
buf.push_str("],\"mappings\":\"");
buf.push_str(&serialize_sourcemap_mappings(sourcemap));
buf.push_str("\"}");
Ok(buf)
}
#[allow(clippy::cast_possible_truncation)]
fn serialize_sourcemap_mappings(sm: &SourceMap) -> String {
sm.token_chunks.as_ref().map_or_else(
|| {
serialize_mappings(
&sm.tokens,
&TokenChunk::new(0, sm.tokens.len() as u32, 0, 0, 0, 0, 0, 0),
)
},
|token_chunks| {
// Serialize `tokens` to vlq `mappings` at parallel.
cfg_if::cfg_if! {
if #[cfg(feature = "concurrent")] {
token_chunks
.par_iter()
.map(|token_chunk| serialize_mappings(&sm.tokens, token_chunk))
.collect::<String>()
} else {
token_chunks
.iter()
.map(|token_chunk| serialize_mappings(&sm.tokens, token_chunk))
.collect::<String>()
}
}
},
)
}
fn serialize_mappings(tokens: &[Token], token_chunk: &TokenChunk) -> String {
let mut rv = String::new();
let TokenChunk {
start,
end,
mut prev_dst_line,
mut prev_dst_col,
mut prev_src_line,
mut prev_src_col,
mut prev_name_id,
mut prev_source_id,
} = *token_chunk;
for (idx, token) in tokens[start as usize..end as usize].iter().enumerate() {
let index = start as usize + idx;
if token.get_dst_line() != prev_dst_line {
prev_dst_col = 0;
while token.get_dst_line() != prev_dst_line {
rv.push(';');
prev_dst_line += 1;
}
} else if index > 0 {
if Some(token) == tokens.get(index - 1) {
continue;
}
rv.push(',');
}
encode_vlq_diff(&mut rv, token.get_dst_col(), prev_dst_col);
prev_dst_col = token.get_dst_col();
if let Some(source_id) = token.get_source_id() {
encode_vlq_diff(&mut rv, source_id, prev_source_id);
prev_source_id = source_id;
encode_vlq_diff(&mut rv, token.get_src_line(), prev_src_line);
prev_src_line = token.get_src_line();
encode_vlq_diff(&mut rv, token.get_src_col(), prev_src_col);
prev_src_col = token.get_src_col();
if let Some(name_id) = token.get_name_id() {
encode_vlq_diff(&mut rv, name_id, prev_name_id);
prev_name_id = name_id;
}
}
}
rv
}
#[inline]
fn encode_vlq_diff(out: &mut String, a: u32, b: u32) {
encode_vlq(out, i64::from(a) - i64::from(b));
}
const B64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn encode_vlq(out: &mut String, num: i64) {
let mut num = if num < 0 { ((-num) << 1) + 1 } else { num << 1 };
loop {
let mut digit = num & 0b11111;
num >>= 5;
if num > 0 {
digit |= 1 << 5;
}
out.push(B64_CHARS[digit as usize] as char);
if num == 0 {
break;
}
}
}
#[test]
fn test_encode() {
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 sm2 = SourceMap::from_json_string(&sm.to_json_string().unwrap()).unwrap();
for (tok1, tok2) in sm.get_tokens().zip(sm2.get_tokens()) {
assert_eq!(tok1, tok2);
}
}
#[test]
fn test_encode_escape_string() {
// '\0' should be escaped.
let mut sm = SourceMap::new(
None,
vec!["\0".into()],
None,
vec!["\0".into()],
Some(vec!["\0".into()]),
vec![],
None,
);
sm.set_x_google_ignore_list(vec![0]);
assert_eq!(
sm.to_json_string().unwrap(),
r#"{"version":3,"names":["\u0000"],"sources":["\u0000"],"sourcesContent":["\u0000"],"x_google_ignoreList":[0],"mappings":""}"#
);
}