Skip to main content

oak_source_map/
builder.rs

1//! Source Map Builder for incremental construction.
2
3use crate::{Mapping, SOURCE_MAP_VERSION, SourceMap, vlq::vlq_encode};
4
5/// Builder for incrementally constructing source maps.
6///
7/// # Example
8///
9/// ```
10/// use oak_source_map::SourceMapBuilder;
11///
12/// let mut builder = SourceMapBuilder::new();
13///
14/// // Add a source file
15/// let source_idx = builder.add_source("input.ts");
16///
17/// // Add mappings
18/// builder.add_mapping(0, 0, Some(source_idx), Some(0), Some(0), None);
19/// builder.add_mapping(0, 10, Some(source_idx), Some(0), Some(10), None);
20///
21/// // Build the final source map
22/// let source_map = builder.build();
23/// ```
24#[derive(Debug, Default)]
25pub struct SourceMapBuilder {
26    sources: Vec<String>,
27    sources_content: Vec<Option<String>>,
28    names: Vec<String>,
29    mappings: Vec<Vec<Mapping>>,
30    file: Option<String>,
31    source_root: Option<String>,
32    last_generated_column: u32,
33    last_source_index: u32,
34    last_original_line: u32,
35    last_original_column: u32,
36    last_name_index: u32,
37}
38
39impl SourceMapBuilder {
40    /// Creates a new source map builder.
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Sets the output file name.
46    pub fn file(mut self, file: impl Into<String>) -> Self {
47        self.file = Some(file.into());
48        self
49    }
50
51    /// Sets the source root.
52    pub fn source_root(mut self, root: impl Into<String>) -> Self {
53        self.source_root = Some(root.into());
54        self
55    }
56
57    /// Adds a source file and returns its index.
58    pub fn add_source(&mut self, source: impl Into<String>) -> u32 {
59        let source_str = source.into();
60        if let Some(idx) = self.sources.iter().position(|s| s == &source_str) {
61            idx as u32
62        }
63        else {
64            self.sources.push(source_str);
65            self.sources_content.push(None);
66            (self.sources.len() - 1) as u32
67        }
68    }
69
70    /// Sets the content for a source file.
71    pub fn set_source_content(&mut self, index: u32, content: impl Into<String>) {
72        let idx = index as usize;
73        if idx < self.sources_content.len() {
74            self.sources_content[idx] = Some(content.into());
75        }
76    }
77
78    /// Adds a name and returns its index.
79    pub fn add_name(&mut self, name: impl Into<String>) -> u32 {
80        let name_str = name.into();
81        if let Some(idx) = self.names.iter().position(|n| n == &name_str) {
82            idx as u32
83        }
84        else {
85            self.names.push(name_str);
86            (self.names.len() - 1) as u32
87        }
88    }
89
90    /// Adds a mapping.
91    ///
92    /// All line and column values are 0-indexed.
93    pub fn add_mapping(&mut self, generated_line: u32, generated_column: u32, source_index: Option<u32>, original_line: Option<u32>, original_column: Option<u32>, name_index: Option<u32>) {
94        while self.mappings.len() <= generated_line as usize {
95            self.mappings.push(Vec::new());
96        }
97
98        self.mappings[generated_line as usize].push(Mapping { generated_line, generated_column, source_index, original_line, original_column, name_index });
99    }
100
101    /// Adds a segment (more convenient API for simple cases).
102    pub fn add_segment(&mut self, generated_line: u32, generated_column: u32, source: Option<&str>, original_line: Option<u32>, original_column: Option<u32>, name: Option<&str>) {
103        let source_index = source.map(|s| self.add_source(s));
104        let name_index = name.map(|n| self.add_name(n));
105
106        self.add_mapping(generated_line, generated_column, source_index, original_line, original_column, name_index);
107    }
108
109    /// Builds the final source map.
110    pub fn build(self) -> SourceMap {
111        let mappings = self.encode_mappings();
112
113        SourceMap { version: SOURCE_MAP_VERSION, sources: self.sources, sources_content: self.sources_content, names: self.names, mappings, file: self.file, source_root: self.source_root, sections: Vec::new() }
114    }
115
116    fn encode_mappings(&self) -> String {
117        let mut result = String::new();
118
119        for (line_idx, line_mappings) in self.mappings.iter().enumerate() {
120            if line_idx > 0 {
121                result.push(';');
122            }
123
124            let mut last_col = 0u32;
125            let mut last_source = 0u32;
126            let mut last_orig_line = 0u32;
127            let mut last_orig_col = 0u32;
128            let mut last_name = 0u32;
129
130            for (seg_idx, mapping) in line_mappings.iter().enumerate() {
131                if seg_idx > 0 {
132                    result.push(',');
133                }
134
135                result.push_str(&vlq_encode(mapping.generated_column as i32 - last_col as i32));
136                last_col = mapping.generated_column;
137
138                if let Some(si) = mapping.source_index {
139                    result.push_str(&vlq_encode(si as i32 - last_source as i32));
140                    last_source = si;
141
142                    if let Some(ol) = mapping.original_line {
143                        result.push_str(&vlq_encode(ol as i32 - last_orig_line as i32));
144                        last_orig_line = ol;
145                    }
146
147                    if let Some(oc) = mapping.original_column {
148                        result.push_str(&vlq_encode(oc as i32 - last_orig_col as i32));
149                        last_orig_col = oc;
150                    }
151
152                    if let Some(ni) = mapping.name_index {
153                        result.push_str(&vlq_encode(ni as i32 - last_name as i32));
154                        last_name = ni;
155                    }
156                }
157            }
158        }
159
160        result
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_builder_basic() {
170        let mut builder = SourceMapBuilder::new();
171        let source_idx = builder.add_source("test.ts");
172        builder.add_mapping(0, 0, Some(source_idx), Some(0), Some(0), None);
173
174        let sm = builder.build();
175        assert_eq!(sm.version, 3);
176        assert_eq!(sm.sources.len(), 1);
177        assert!(!sm.mappings.is_empty());
178    }
179
180    #[test]
181    fn test_builder_multiple_mappings() {
182        let mut builder = SourceMapBuilder::new();
183        let source_idx = builder.add_source("test.ts");
184
185        builder.add_mapping(0, 0, Some(source_idx), Some(0), Some(0), None);
186        builder.add_mapping(0, 10, Some(source_idx), Some(0), Some(10), None);
187        builder.add_mapping(1, 0, Some(source_idx), Some(1), Some(0), None);
188
189        let sm = builder.build();
190        let mappings = sm.parse_mappings().unwrap();
191        assert_eq!(mappings.len(), 3);
192    }
193
194    #[test]
195    fn test_builder_with_names() {
196        let mut builder = SourceMapBuilder::new();
197        let source_idx = builder.add_source("test.ts");
198        let name_idx = builder.add_name("foo");
199
200        builder.add_mapping(0, 0, Some(source_idx), Some(0), Some(0), Some(name_idx));
201
202        let sm = builder.build();
203        assert_eq!(sm.names.len(), 1);
204        assert_eq!(sm.names[0], "foo");
205    }
206
207    #[test]
208    fn test_builder_source_content() {
209        let mut builder = SourceMapBuilder::new();
210        let source_idx = builder.add_source("test.ts");
211        builder.set_source_content(source_idx, "const x = 1;");
212
213        let sm = builder.build();
214        assert_eq!(sm.sources_content.len(), 1);
215        assert_eq!(sm.sources_content[0], Some("const x = 1;".to_string()));
216    }
217}