Skip to main content

oak_source_map/
source_map.rs

1//! Source Map data structure and parsing.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Mapping, Result, SOURCE_MAP_VERSION, SourceMapError, vlq::vlq_decode_many};
6
7/// Input source for source map parsing.
8#[derive(Debug, Clone)]
9pub enum SourceMapInput {
10    /// JSON string input.
11    Json(String),
12    /// Byte slice input.
13    Bytes(Vec<u8>),
14    /// File path input.
15    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/// Source Map v3 representation.
37///
38/// This is the main data structure for working with source maps.
39/// It follows the [Source Map v3 specification](https://sourcemaps.info/spec.html).
40#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41pub struct SourceMap {
42    /// Version (always 3).
43    pub version: u8,
44    /// List of source file paths.
45    #[serde(default)]
46    pub sources: Vec<String>,
47    /// List of source file contents (optional).
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub sources_content: Vec<Option<String>>,
50    /// List of symbol names.
51    #[serde(default)]
52    pub names: Vec<String>,
53    /// Encoded mappings string.
54    #[serde(default)]
55    pub mappings: String,
56    /// Output file path (optional).
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub file: Option<String>,
59    /// Source root (optional).
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub source_root: Option<String>,
62    /// Source map references (for indexed source maps).
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub sections: Vec<SourceMapSection>,
65}
66
67/// Section in an indexed source map.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct SourceMapSection {
70    /// Offset in the generated file.
71    pub offset: SectionOffset,
72    /// URL to the source map for this section.
73    pub map: Option<String>,
74}
75
76/// Offset for a section.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct SectionOffset {
79    /// Line offset.
80    pub line: u32,
81    /// Column offset.
82    pub column: u32,
83}
84
85/// Metadata about a source map.
86#[derive(Debug, Clone)]
87pub struct SourceMapMetadata {
88    /// Number of sources.
89    pub sources_count: usize,
90    /// Number of names.
91    pub names_count: usize,
92    /// Number of mappings.
93    pub mappings_count: usize,
94    /// Number of lines.
95    pub lines_count: usize,
96    /// Whether sources content is included.
97    pub has_sources_content: bool,
98    /// Whether it's an indexed source map.
99    pub is_indexed: bool,
100}
101
102impl SourceMap {
103    /// Creates a new empty source map.
104    pub fn new() -> Self {
105        Self { version: SOURCE_MAP_VERSION, ..Default::default() }
106    }
107
108    /// Parses a source map from JSON.
109    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    /// Converts the source map to JSON.
130    pub fn to_json(&self) -> Result<String> {
131        serde_json::to_string(self).map_err(SourceMapError::from)
132    }
133
134    /// Converts the source map to pretty-printed JSON.
135    pub fn to_json_pretty(&self) -> Result<String> {
136        serde_json::to_string_pretty(self).map_err(SourceMapError::from)
137    }
138
139    /// Adds a source file.
140    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    /// Adds a name.
153    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    /// Sets the content for a source.
165    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    /// Gets the source path at an index.
172    pub fn get_source(&self, index: usize) -> Option<&str> {
173        self.sources.get(index).map(|s| s.as_str())
174    }
175
176    /// Gets the name at an index.
177    pub fn get_name(&self, index: usize) -> Option<&str> {
178        self.names.get(index).map(|s| s.as_str())
179    }
180
181    /// Gets the source content at an index.
182    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    /// Parses all mappings into a vector.
187    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    /// Returns metadata about this source map.
240    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    /// Generates the inline source map comment.
255    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    /// Generates the external source map comment.
263    pub fn to_external_comment(&self, filename: &str) -> String {
264        format!("//# sourceMappingURL={}", filename)
265    }
266
267    /// Checks if this is an indexed source map.
268    pub fn is_indexed(&self) -> bool {
269        !self.sections.is_empty()
270    }
271
272    /// Checks if this source map has sources content.
273    pub fn has_sources_content(&self) -> bool {
274        self.sources_content.iter().any(|c| c.is_some())
275    }
276
277    /// Returns the full source path for a source index.
278    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}