1use serde::{Deserialize, Serialize};
4
5use crate::{Mapping, Result, SOURCE_MAP_VERSION, SourceMapError, vlq::vlq_decode_many};
6
7#[derive(Debug, Clone)]
9pub enum SourceMapInput {
10 Json(String),
12 Bytes(Vec<u8>),
14 File(std::path::PathBuf),
16}
17
18impl From<String> for SourceMapInput {
19 fn from(s: String) -> Self {
20 SourceMapInput::Json(s)
21 }
22}
23
24impl From<&str> for SourceMapInput {
25 fn from(s: &str) -> Self {
26 SourceMapInput::Json(s.to_string())
27 }
28}
29
30impl From<Vec<u8>> for SourceMapInput {
31 fn from(bytes: Vec<u8>) -> Self {
32 SourceMapInput::Bytes(bytes)
33 }
34}
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41pub struct SourceMap {
42 pub version: u8,
44 #[serde(default)]
46 pub sources: Vec<String>,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub sources_content: Vec<Option<String>>,
50 #[serde(default)]
52 pub names: Vec<String>,
53 #[serde(default)]
55 pub mappings: String,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub file: Option<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub source_root: Option<String>,
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
64 pub sections: Vec<SourceMapSection>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct SourceMapSection {
70 pub offset: SectionOffset,
72 pub map: Option<String>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct SectionOffset {
79 pub line: u32,
81 pub column: u32,
83}
84
85#[derive(Debug, Clone)]
87pub struct SourceMapMetadata {
88 pub sources_count: usize,
90 pub names_count: usize,
92 pub mappings_count: usize,
94 pub lines_count: usize,
96 pub has_sources_content: bool,
98 pub is_indexed: bool,
100}
101
102impl SourceMap {
103 pub fn new() -> Self {
105 Self { version: SOURCE_MAP_VERSION, ..Default::default() }
106 }
107
108 pub fn parse(json: impl Into<SourceMapInput>) -> Result<Self> {
110 let input = json.into();
111 let json_str = match input {
112 SourceMapInput::Json(s) => s,
113 SourceMapInput::Bytes(b) => String::from_utf8(b).map_err(|e| SourceMapError::JsonError(serde_json::from_str::<serde_json::Value>(&e.to_string()).unwrap_err()))?,
114 SourceMapInput::File(path) => {
115 let content = std::fs::read_to_string(&path)?;
116 content
117 }
118 };
119
120 let mut sm: SourceMap = serde_json::from_str(&json_str)?;
121
122 if sm.version != SOURCE_MAP_VERSION {
123 return Err(SourceMapError::InvalidVersion(sm.version));
124 }
125
126 Ok(sm)
127 }
128
129 pub fn to_json(&self) -> Result<String> {
131 serde_json::to_string(self).map_err(SourceMapError::from)
132 }
133
134 pub fn to_json_pretty(&self) -> Result<String> {
136 serde_json::to_string_pretty(self).map_err(SourceMapError::from)
137 }
138
139 pub fn add_source(&mut self, source: impl Into<String>) -> usize {
141 let source_str = source.into();
142 if let Some(idx) = self.sources.iter().position(|s| s == &source_str) {
143 idx
144 }
145 else {
146 self.sources.push(source_str);
147 self.sources_content.push(None);
148 self.sources.len() - 1
149 }
150 }
151
152 pub fn add_name(&mut self, name: impl Into<String>) -> usize {
154 let name_str = name.into();
155 if let Some(idx) = self.names.iter().position(|n| n == &name_str) {
156 idx
157 }
158 else {
159 self.names.push(name_str);
160 self.names.len() - 1
161 }
162 }
163
164 pub fn set_source_content(&mut self, index: usize, content: impl Into<String>) {
166 if index < self.sources_content.len() {
167 self.sources_content[index] = Some(content.into());
168 }
169 }
170
171 pub fn get_source(&self, index: usize) -> Option<&str> {
173 self.sources.get(index).map(|s| s.as_str())
174 }
175
176 pub fn get_name(&self, index: usize) -> Option<&str> {
178 self.names.get(index).map(|s| s.as_str())
179 }
180
181 pub fn get_source_content(&self, index: usize) -> Option<Option<&String>> {
183 self.sources_content.get(index).map(|c| c.as_ref())
184 }
185
186 pub fn parse_mappings(&self) -> Result<Vec<Mapping>> {
188 let mut mappings = Vec::new();
189 let mut generated_line = 0u32;
190 let mut generated_column = 0u32;
191 let mut source_index = 0u32;
192 let mut original_line = 0u32;
193 let mut original_column = 0u32;
194 let mut name_index = 0u32;
195
196 for line in self.mappings.split(';') {
197 if line.is_empty() {
198 generated_line += 1;
199 generated_column = 0;
200 continue;
201 }
202
203 for segment in line.split(',') {
204 if segment.is_empty() {
205 continue;
206 }
207
208 let values = vlq_decode_many(segment)?;
209
210 generated_column = (generated_column as i32 + values.get(0).copied().unwrap_or(0)) as u32;
211
212 let mut mapping = Mapping::generated_only(generated_line, generated_column);
213
214 if values.len() >= 5 {
215 source_index = (source_index as i32 + values[1]) as u32;
216 original_line = (original_line as i32 + values[2]) as u32;
217 original_column = (original_column as i32 + values[3]) as u32;
218
219 mapping.source_index = Some(source_index);
220 mapping.original_line = Some(original_line);
221 mapping.original_column = Some(original_column);
222
223 if values.len() >= 6 {
224 name_index = (name_index as i32 + values[5]) as u32;
225 mapping.name_index = Some(name_index);
226 }
227 }
228
229 mappings.push(mapping);
230 }
231
232 generated_line += 1;
233 generated_column = 0;
234 }
235
236 Ok(mappings)
237 }
238
239 pub fn metadata(&self) -> SourceMapMetadata {
241 let mappings = self.parse_mappings().ok();
242 let lines_count = self.mappings.split(';').count();
243
244 SourceMapMetadata {
245 sources_count: self.sources.len(),
246 names_count: self.names.len(),
247 mappings_count: mappings.map(|m| m.len()).unwrap_or(0),
248 lines_count,
249 has_sources_content: self.sources_content.iter().any(|c| c.is_some()),
250 is_indexed: !self.sections.is_empty(),
251 }
252 }
253
254 pub fn to_inline_comment(&self) -> Result<String> {
256 use base64::prelude::*;
257 let json = self.to_json()?;
258 let encoded = BASE64_STANDARD.encode(json.as_bytes());
259 Ok(format!("//# sourceMappingURL=data:application/json;base64,{}", encoded))
260 }
261
262 pub fn to_external_comment(&self, filename: &str) -> String {
264 format!("//# sourceMappingURL={}", filename)
265 }
266
267 pub fn is_indexed(&self) -> bool {
269 !self.sections.is_empty()
270 }
271
272 pub fn has_sources_content(&self) -> bool {
274 self.sources_content.iter().any(|c| c.is_some())
275 }
276
277 pub fn get_full_source_path(&self, index: usize) -> Option<String> {
279 self.sources.get(index).map(|source| if let Some(ref root) = self.source_root { format!("{}{}", root, source) } else { source.clone() })
280 }
281}
282
283impl Default for SourceMapMetadata {
284 fn default() -> Self {
285 Self { sources_count: 0, names_count: 0, mappings_count: 0, lines_count: 0, has_sources_content: false, is_indexed: false }
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_parse_minimal() {
295 let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
296 let sm = SourceMap::parse(json).unwrap();
297 assert_eq!(sm.version, 3);
298 assert!(sm.sources.is_empty());
299 }
300
301 #[test]
302 fn test_parse_with_sources() {
303 let json = r#"{"version":3,"sources":["foo.js","bar.js"],"names":[],"mappings":"AAAA"}"#;
304 let sm = SourceMap::parse(json).unwrap();
305 assert_eq!(sm.sources.len(), 2);
306 assert_eq!(sm.sources[0], "foo.js");
307 assert_eq!(sm.sources[1], "bar.js");
308 }
309
310 #[test]
311 fn test_invalid_version() {
312 let json = r#"{"version":2,"sources":[],"names":[],"mappings":""}"#;
313 let result = SourceMap::parse(json);
314 assert!(matches!(result, Err(SourceMapError::InvalidVersion(2))));
315 }
316
317 #[test]
318 fn test_add_source() {
319 let mut sm = SourceMap::new();
320 let idx = sm.add_source("test.js");
321 assert_eq!(idx, 0);
322 assert_eq!(sm.sources.len(), 1);
323
324 let idx2 = sm.add_source("test.js");
325 assert_eq!(idx2, 0);
326 assert_eq!(sm.sources.len(), 1);
327 }
328
329 #[test]
330 fn test_to_json() {
331 let mut sm = SourceMap::new();
332 sm.add_source("test.js");
333 let json = sm.to_json().unwrap();
334 assert!(json.contains("\"version\":3"));
335 assert!(json.contains("\"sources\":[\"test.js\"]"));
336 }
337
338 #[test]
339 fn test_parse_mappings() {
340 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;ACDA"}"#;
341 let sm = SourceMap::parse(json).unwrap();
342 let mappings = sm.parse_mappings().unwrap();
343 assert_eq!(mappings.len(), 2);
344 }
345}