prettify_js/
source_map_generator.rs

1use serde::Serialize;
2
3/// Zero-based line number
4#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
5pub struct SourceMapLine(pub u32);
6
7/// Zero-based column number, in UTF16 code units
8#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
9pub struct SourceMapColumn(pub u32);
10
11/// Source coordinate, line+column
12#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
13pub struct SourceCoord {
14    pub line: SourceMapLine,
15    pub column: SourceMapColumn,
16}
17
18/// Points that correspond in the original/generated source code.
19pub struct SourceMapping {
20    /// Coordinate in the "original source code"
21    pub from: SourceCoord,
22    /// Coordinate in the "generated source code"
23    pub to: SourceCoord,
24}
25
26#[derive(Serialize)]
27#[serde(rename_all = "camelCase")]
28struct SourceMapJson {
29    version: i32,
30    sources: Vec<String>,
31    sources_content: Vec<String>,
32    names: Vec<String>,
33    mappings: String,
34}
35
36fn encode_digit(value: u8) -> char {
37    let b = match value {
38        0..=25 => b'A' + value,
39        26..=51 => b'a' + (value - 26),
40        52..=61 => b'0' + (value - 52),
41        62 => b'+',
42        63 => b'/',
43        _ => panic!("Invalid digit"),
44    };
45    b as char
46}
47
48fn write_vlq(value: i64, output: &mut String) {
49    let mut val = if value >= 0 {
50        (value as u64) << 1
51    } else if value == i64::MIN {
52        output.push('g');
53        1 << (63 - 4)
54    } else {
55        ((-value as u64) << 1) + 1
56    };
57    loop {
58        let mut digit = val as u8 & ((1 << 5) - 1);
59        val >>= 5;
60        if val > 0 {
61            digit |= 1 << 5;
62        }
63        output.push(encode_digit(digit));
64        if val == 0 {
65            break;
66        }
67    }
68}
69
70/// Generate a source-map as a string, given the original source file name,
71/// file data, and a list of mappings from the original source to the generated
72/// source.
73///
74/// `from_name` is the name of the "original source code" file.
75/// `from_content` is the content of that file. We always insert the content inline
76/// into the source map.
77/// Currently we only support mappings where a single source file is mapped to a single
78/// generated file.
79pub fn generate_source_map(
80    from_name: String,
81    from_content: String,
82    mappings: Vec<SourceMapping>,
83) -> String {
84    let mut map = SourceMapJson {
85        version: 3,
86        sources: vec![from_name],
87        sources_content: vec![from_content],
88        names: Vec::new(),
89        mappings: String::new(),
90    };
91    let mut last_to_line = SourceMapLine(0);
92    let mut last_to_column = SourceMapColumn(0);
93    let mut last_from_line = SourceMapLine(0);
94    let mut last_from_column = SourceMapColumn(0);
95    for m in mappings {
96        if last_to_line < m.to.line {
97            while last_to_line < m.to.line {
98                map.mappings.push(';');
99                last_to_column = SourceMapColumn(0);
100                last_to_line.0 += 1;
101            }
102        } else {
103            if !map.mappings.is_empty() {
104                map.mappings.push(',');
105            }
106        }
107        write_vlq(
108            m.to.column.0 as i64 - last_to_column.0 as i64,
109            &mut map.mappings,
110        );
111        last_to_column = m.to.column;
112        // Sources index is always 0 since we only support one "from" file currently.
113        write_vlq(0, &mut map.mappings);
114        write_vlq(
115            m.from.line.0 as i64 - last_from_line.0 as i64,
116            &mut map.mappings,
117        );
118        last_from_line = m.from.line;
119        write_vlq(
120            m.from.column.0 as i64 - last_from_column.0 as i64,
121            &mut map.mappings,
122        );
123        last_from_column = m.from.column;
124    }
125    serde_json::to_string(&map).unwrap()
126}