Skip to main content

srcmap_codec/
encode.rs

1use crate::SourceMapMappings;
2use crate::vlq::vlq_encode;
3
4/// Encode decoded source map mappings back into a VLQ-encoded string.
5///
6/// This is the inverse of [`decode`](crate::decode). Values are delta-encoded:
7/// generated column resets per line, all other fields are cumulative.
8///
9/// Empty segments are silently skipped.
10pub fn encode(mappings: &SourceMapMappings) -> String {
11    if mappings.is_empty() {
12        return String::new();
13    }
14
15    // Estimate capacity: ~4 bytes per segment on average
16    let segment_count: usize = mappings.iter().map(|line| line.len()).sum();
17    let mut buf: Vec<u8> = Vec::with_capacity(segment_count * 4 + mappings.len());
18
19    // Cumulative state
20    let mut prev_source: i64 = 0;
21    let mut prev_original_line: i64 = 0;
22    let mut prev_original_column: i64 = 0;
23    let mut prev_name: i64 = 0;
24
25    for (line_idx, line) in mappings.iter().enumerate() {
26        if line_idx > 0 {
27            buf.push(b';');
28        }
29
30        // Generated column resets per line
31        let mut prev_generated_column: i64 = 0;
32        let mut wrote_segment = false;
33
34        for segment in line.iter() {
35            if segment.is_empty() {
36                continue;
37            }
38
39            if wrote_segment {
40                buf.push(b',');
41            }
42            wrote_segment = true;
43
44            // Field 1: generated column (delta from previous in this line)
45            vlq_encode(&mut buf, segment[0] - prev_generated_column);
46            prev_generated_column = segment[0];
47
48            if segment.len() >= 4 {
49                // Field 2: source index (cumulative delta)
50                vlq_encode(&mut buf, segment[1] - prev_source);
51                prev_source = segment[1];
52
53                // Field 3: original line (cumulative delta)
54                vlq_encode(&mut buf, segment[2] - prev_original_line);
55                prev_original_line = segment[2];
56
57                // Field 4: original column (cumulative delta)
58                vlq_encode(&mut buf, segment[3] - prev_original_column);
59                prev_original_column = segment[3];
60
61                if segment.len() >= 5 {
62                    // Field 5: name index (cumulative delta)
63                    vlq_encode(&mut buf, segment[4] - prev_name);
64                    prev_name = segment[4];
65                }
66            }
67        }
68    }
69
70    // SAFETY: buf contains only ASCII base64 chars, semicolons, and commas
71    unsafe { String::from_utf8_unchecked(buf) }
72}
73
74/// Encode a single line's segments to VLQ bytes.
75///
76/// Generated column resets per line (starts at 0).
77/// Cumulative state (source, original line/column, name) is passed in.
78#[cfg(feature = "parallel")]
79fn encode_line_to_bytes(
80    segments: &[crate::Segment],
81    init_source: i64,
82    init_original_line: i64,
83    init_original_column: i64,
84    init_name: i64,
85) -> Vec<u8> {
86    let mut buf = Vec::with_capacity(segments.len() * 6);
87    let mut prev_generated_column: i64 = 0;
88    let mut prev_source = init_source;
89    let mut prev_original_line = init_original_line;
90    let mut prev_original_column = init_original_column;
91    let mut prev_name = init_name;
92    let mut wrote_segment = false;
93
94    for segment in segments {
95        if segment.is_empty() {
96            continue;
97        }
98
99        if wrote_segment {
100            buf.push(b',');
101        }
102        wrote_segment = true;
103
104        vlq_encode(&mut buf, segment[0] - prev_generated_column);
105        prev_generated_column = segment[0];
106
107        if segment.len() >= 4 {
108            vlq_encode(&mut buf, segment[1] - prev_source);
109            prev_source = segment[1];
110
111            vlq_encode(&mut buf, segment[2] - prev_original_line);
112            prev_original_line = segment[2];
113
114            vlq_encode(&mut buf, segment[3] - prev_original_column);
115            prev_original_column = segment[3];
116
117            if segment.len() >= 5 {
118                vlq_encode(&mut buf, segment[4] - prev_name);
119                prev_name = segment[4];
120            }
121        }
122    }
123
124    buf
125}
126
127/// Encode source map mappings using parallel encoding with rayon.
128///
129/// Uses the same delta-encoding as [`encode`], but distributes line encoding
130/// across threads. Falls back to sequential [`encode`] for small maps.
131///
132/// Two-phase approach:
133/// 1. **Sequential scan** — compute cumulative state at each line boundary
134/// 2. **Parallel encode** — encode each line independently via rayon
135#[cfg(feature = "parallel")]
136pub fn encode_parallel(mappings: &SourceMapMappings) -> String {
137    use rayon::prelude::*;
138
139    if mappings.is_empty() {
140        return String::new();
141    }
142
143    let total_segments: usize = mappings.iter().map(|l| l.len()).sum();
144    if mappings.len() < 1024 || total_segments < 4096 {
145        return encode(mappings);
146    }
147
148    // Pass 1 (sequential): compute cumulative state at each line boundary
149    let mut states: Vec<(i64, i64, i64, i64)> = Vec::with_capacity(mappings.len());
150    let mut prev_source: i64 = 0;
151    let mut prev_original_line: i64 = 0;
152    let mut prev_original_column: i64 = 0;
153    let mut prev_name: i64 = 0;
154
155    for line in mappings.iter() {
156        states.push((
157            prev_source,
158            prev_original_line,
159            prev_original_column,
160            prev_name,
161        ));
162        for segment in line.iter() {
163            if segment.len() >= 4 {
164                prev_source = segment[1];
165                prev_original_line = segment[2];
166                prev_original_column = segment[3];
167                if segment.len() >= 5 {
168                    prev_name = segment[4];
169                }
170            }
171        }
172    }
173
174    // Pass 2 (parallel): encode each line independently
175    let encoded_lines: Vec<Vec<u8>> = mappings
176        .par_iter()
177        .zip(states.par_iter())
178        .map(|(line, &(src, ol, oc, name))| encode_line_to_bytes(line, src, ol, oc, name))
179        .collect();
180
181    // Join with semicolons
182    let total_len: usize =
183        encoded_lines.iter().map(|l| l.len()).sum::<usize>() + encoded_lines.len() - 1;
184    let mut buf: Vec<u8> = Vec::with_capacity(total_len);
185    for (i, line_bytes) in encoded_lines.iter().enumerate() {
186        if i > 0 {
187            buf.push(b';');
188        }
189        buf.extend_from_slice(line_bytes);
190    }
191
192    // SAFETY: VLQ output is always valid ASCII/UTF-8
193    unsafe { String::from_utf8_unchecked(buf) }
194}