source_map/
lib.rs

1#![allow(clippy::useless_conversion)]
2#![doc = include_str!("../README.md")]
3
4pub mod encodings;
5mod filesystem;
6mod lines_columns_indexes;
7mod source_id;
8mod span;
9mod to_string;
10
11use std::{
12    collections::{HashMap, HashSet},
13    convert::TryInto,
14};
15
16pub use filesystem::*;
17pub use lines_columns_indexes::LineStarts;
18pub use source_id::SourceId;
19pub use span::*;
20pub use to_string::*;
21
22const BASE64_ALPHABET: &[u8; 64] =
23    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
24
25/// Adapted from [vlq](https://github.com/Rich-Harris/vlq/blob/822db3f22bf09148b84e8ef58878d11f3bcd543e/src/vlq.ts#L63)
26fn vlq_encode_integer_to_buffer(buf: &mut String, mut value: isize) {
27    if value.is_negative() {
28        value = (-value << 1) | 1;
29    } else {
30        value <<= 1;
31    };
32
33    loop {
34        let mut clamped = value & 31;
35        value >>= 5;
36        if value > 0 {
37            clamped |= 32;
38        }
39        buf.push(BASE64_ALPHABET[clamped as usize] as char);
40        if value <= 0 {
41            break;
42        }
43    }
44}
45
46#[derive(Debug)]
47struct SourceMapping {
48    pub(crate) on_output_column: u32,
49    pub(crate) source_byte_start: u32,
50    pub(crate) from_source: SourceId,
51    // TODO are these needed
52    // pub(crate) on_output_line: usize,
53    // pub(crate) source_byte_end: usize,
54}
55
56#[derive(Debug)]
57enum MappingOrBreak {
58    Mapping(SourceMapping),
59    /// From new line in output. These are encoded as `;`
60    Break,
61}
62
63/// Struct for building a [source map (v3)](https://sourcemaps.info/spec.html)
64#[derive(Default)]
65pub struct SourceMapBuilder {
66    current_output_line: u32,
67    current_output_column: u32,
68    #[allow(dead_code)]
69    last_output_line: Option<u32>,
70    // last_output_column: usize,
71    mappings: Vec<MappingOrBreak>,
72    used_sources: HashSet<SourceId>,
73}
74
75impl SourceMapBuilder {
76    pub fn new() -> SourceMapBuilder {
77        SourceMapBuilder::default()
78    }
79
80    // Record a new line was added to output
81    pub fn add_new_line(&mut self) {
82        self.current_output_line += 1;
83        self.mappings.push(MappingOrBreak::Break);
84    }
85
86    // Record a new line was added to output
87    pub fn add_to_column(&mut self, length: usize) {
88        self.current_output_column += length as u32;
89    }
90
91    /// Original line and original column are one indexed
92    pub fn add_mapping(&mut self, source_position: &SpanWithSource, current_column: u32) {
93        let SpanWithSource {
94            start: source_byte_start,
95            // TODO should it read this
96            end: _source_byte_end,
97            source: from_source,
98        } = source_position;
99
100        self.used_sources.insert(*from_source);
101
102        self.mappings.push(MappingOrBreak::Mapping(SourceMapping {
103            from_source: *from_source,
104            source_byte_start: (*source_byte_start).try_into().unwrap(),
105            on_output_column: current_column,
106            // source_byte_end: *source_byte_end,
107            // on_output_line: self.current_output_line,
108        }));
109    }
110
111    /// Encodes the results into a string and builds the JSON representation thingy
112    ///
113    /// TODO not 100% certain that this code is a the correct implementation
114    ///
115    /// TODO are the accounts for SourceId::null valid here...?
116    pub fn build(self, fs: &impl FileSystem) -> SourceMap {
117        // Splits are indexes of new lines in the source
118        let mut source_line_splits = HashMap::<SourceId, LineStarts>::new();
119        let mut sources = Vec::<SourceId>::new();
120
121        for source_id in self.used_sources.into_iter().filter(|id| !id.is_null()) {
122            source_line_splits.insert(
123                source_id,
124                fs.get_source_by_id(source_id, |source| source.line_starts.clone()),
125            );
126            sources.push(source_id);
127        }
128
129        let mut mappings = String::new();
130
131        let mut last_was_break = None::<bool>;
132        let mut last_mapped_source_line = 0;
133        let mut last_mapped_source_column = 0;
134        let mut last_mapped_output_column = 0;
135
136        for mapping in self.mappings {
137            match mapping {
138                MappingOrBreak::Mapping(mapping) => {
139                    let SourceMapping {
140                        on_output_column,
141                        source_byte_start,
142                        // TODO are these needed:
143                        // on_output_line: _,
144                        // source_byte_end: _,
145                        from_source,
146                    } = mapping;
147
148                    if from_source.is_null() {
149                        continue;
150                    }
151
152                    if let Some(false) = last_was_break {
153                        mappings.push(',');
154                    }
155
156                    let output_column =
157                        on_output_column as isize - last_mapped_output_column as isize;
158
159                    vlq_encode_integer_to_buffer(&mut mappings, output_column);
160                    last_mapped_output_column = on_output_column;
161
162                    // Find index
163                    // TODO faster
164                    let idx = sources.iter().position(|sid| *sid == from_source).unwrap();
165
166                    // Encode index of source
167                    vlq_encode_integer_to_buffer(&mut mappings, idx as isize);
168
169                    let line_splits_for_this_file = source_line_splits.get(&from_source).unwrap();
170
171                    let (source_line, source_column) = line_splits_for_this_file
172                        .get_line_and_column_pos_is_on(source_byte_start as usize);
173
174                    let source_line_diff = source_line as isize - last_mapped_source_line as isize;
175                    vlq_encode_integer_to_buffer(&mut mappings, source_line_diff);
176
177                    last_mapped_source_line = source_line;
178
179                    let source_column_diff =
180                        source_column as isize - last_mapped_source_column as isize;
181                    vlq_encode_integer_to_buffer(&mut mappings, source_column_diff);
182
183                    last_mapped_source_column = source_column;
184
185                    // TODO names field?
186
187                    last_was_break = Some(false);
188                }
189                MappingOrBreak::Break => {
190                    mappings.push(';');
191                    last_was_break = Some(true);
192                    last_mapped_output_column = 0;
193                }
194            }
195        }
196
197        SourceMap { mappings, sources }
198    }
199}
200
201fn count_characters_on_last_line(s: &str) -> u32 {
202    let mut count = 0u32;
203    for b in s.as_bytes().iter().rev() {
204        if *b == b'\n' {
205            return count;
206        }
207        // I think the byte count should be fine
208        count += 1;
209    }
210    count
211}
212
213#[derive(Clone)]
214pub struct SourceMap {
215    pub mappings: String,
216    pub sources: Vec<SourceId>,
217}
218
219impl SourceMap {
220    pub fn to_json(self, filesystem: &impl FileSystem) -> String {
221        use std::fmt::Write;
222
223        let Self {
224            mappings,
225            sources: sources_used,
226        } = self;
227
228        let (mut sources, mut sources_content) = (String::new(), String::new());
229        for (idx, (path, content)) in sources_used
230            .into_iter()
231            .map(|source_id| filesystem.get_file_path_and_content(source_id))
232            .enumerate()
233        {
234            if idx != 0 {
235                sources.push(',');
236                sources_content.push(',');
237            }
238            write!(
239                sources,
240                "\"{}\"",
241                path.display().to_string().replace('\\', "/")
242            )
243            .unwrap();
244            write!(
245                sources_content,
246                "\"{}\"",
247                content
248                    .replace('\n', "\\n")
249                    .replace('\r', "\\r")
250                    .replace('"', "\\\"")
251            )
252            .unwrap();
253        }
254
255        format!(
256            r#"{{"version":3,"sourceRoot":"","sources":[{sources}],"sourcesContent":[{sources_content}],"names":[],"mappings":"{mappings}"}}"#,
257        )
258    }
259}
260
261#[cfg(test)]
262mod source_map_tests {
263    use super::vlq_encode_integer_to_buffer;
264
265    fn vlq_encode_integer(value: isize) -> String {
266        let mut buf = String::new();
267        vlq_encode_integer_to_buffer(&mut buf, value);
268        buf
269    }
270
271    #[test]
272    fn vlq_encoder() {
273        assert_eq!(vlq_encode_integer(0), "A");
274        assert_eq!(vlq_encode_integer(1), "C");
275        assert_eq!(vlq_encode_integer(-1), "D");
276        assert_eq!(vlq_encode_integer(123), "2H");
277        assert_eq!(vlq_encode_integer(123456789), "qxmvrH");
278    }
279}