Skip to main content

tsz_common/
source_map.rs

1//! Source Map Generation
2//!
3//! Implements Source Map v3 specification for mapping generated JavaScript
4//! back to original TypeScript source.
5//!
6//! Format: <https://sourcemaps.info/spec.html>
7
8use std::fmt::Write;
9
10use memchr;
11use serde::Serialize;
12
13/// A single mapping from generated position to original position
14#[derive(Debug, Clone)]
15pub struct Mapping {
16    /// Generated line (0-indexed)
17    pub generated_line: u32,
18    /// Generated column (0-indexed)
19    pub generated_column: u32,
20    /// Source file index
21    pub source_index: u32,
22    /// Original line (0-indexed)
23    pub original_line: u32,
24    /// Original column (0-indexed)
25    pub original_column: u32,
26    /// Name index (optional)
27    pub name_index: Option<u32>,
28}
29
30/// Source Map v3 output format
31#[derive(Debug, Serialize)]
32pub struct SourceMap {
33    pub version: u32,
34    pub file: String,
35    #[serde(rename = "sourceRoot")]
36    pub source_root: String,
37    pub sources: Vec<String>,
38    #[serde(rename = "sourcesContent")]
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub sources_content: Option<Vec<String>>,
41    pub names: Vec<String>,
42    pub mappings: String,
43}
44
45/// Builder for source maps
46pub struct SourceMapGenerator {
47    file: String,
48    source_root: String,
49    sources: Vec<String>,
50    sources_content: Vec<Option<String>>,
51    names: Vec<String>,
52    mappings: Vec<Mapping>,
53
54    // State for VLQ encoding
55    prev_generated_column: i32,
56    prev_original_line: i32,
57    prev_original_column: i32,
58    prev_source_index: i32,
59    prev_name_index: i32,
60}
61
62impl SourceMapGenerator {
63    #[must_use]
64    pub const fn new(file: String) -> Self {
65        Self {
66            file,
67            source_root: String::new(),
68            sources: Vec::new(),
69            sources_content: Vec::new(),
70            names: Vec::new(),
71            mappings: Vec::new(),
72            prev_generated_column: 0,
73            prev_original_line: 0,
74            prev_original_column: 0,
75            prev_source_index: 0,
76            prev_name_index: 0,
77        }
78    }
79
80    /// Set the source root
81    pub fn set_source_root(&mut self, root: String) {
82        self.source_root = root;
83    }
84
85    /// Add a source file
86    #[must_use]
87    pub fn add_source(&mut self, source: String) -> u32 {
88        let index = u32::try_from(self.sources.len()).unwrap_or(u32::MAX);
89        self.sources.push(source);
90        self.sources_content.push(None);
91        index
92    }
93
94    /// Add a source file with content
95    #[must_use]
96    pub fn add_source_with_content(&mut self, source: String, content: String) -> u32 {
97        let index = u32::try_from(self.sources.len()).unwrap_or(u32::MAX);
98        self.sources.push(source);
99        self.sources_content.push(Some(content));
100        index
101    }
102
103    /// Add a name to the names array
104    #[must_use]
105    pub fn add_name(&mut self, name: String) -> u32 {
106        // Check if name already exists
107        for (i, n) in self.names.iter().enumerate() {
108            if n == &name {
109                return u32::try_from(i).unwrap_or(u32::MAX);
110            }
111        }
112        let index = u32::try_from(self.names.len()).unwrap_or(u32::MAX);
113        self.names.push(name);
114        index
115    }
116
117    /// Add a mapping
118    pub fn add_mapping(
119        &mut self,
120        generated_line: u32,
121        generated_column: u32,
122        source_index: u32,
123        original_line: u32,
124        original_column: u32,
125        name_index: Option<u32>,
126    ) {
127        self.mappings.push(Mapping {
128            generated_line,
129            generated_column,
130            source_index,
131            original_line,
132            original_column,
133            name_index,
134        });
135    }
136
137    /// Add a simple mapping (no name)
138    pub fn add_simple_mapping(
139        &mut self,
140        generated_line: u32,
141        generated_column: u32,
142        source_index: u32,
143        original_line: u32,
144        original_column: u32,
145    ) {
146        self.add_mapping(
147            generated_line,
148            generated_column,
149            source_index,
150            original_line,
151            original_column,
152            None,
153        );
154    }
155
156    /// Shift all mappings at or after `from_line` by `delta` lines.
157    /// Used when inserting generated lines (e.g., hoisted temp var declarations)
158    /// into the output after emit is complete.
159    pub fn shift_generated_lines(&mut self, from_line: u32, delta: u32) {
160        for mapping in &mut self.mappings {
161            if mapping.generated_line >= from_line {
162                mapping.generated_line += delta;
163            }
164        }
165    }
166
167    /// Generate the source map
168    pub fn generate(&mut self) -> SourceMap {
169        // Sort mappings by generated position
170        self.mappings.sort_by(|a, b| {
171            if a.generated_line == b.generated_line {
172                a.generated_column.cmp(&b.generated_column)
173            } else {
174                a.generated_line.cmp(&b.generated_line)
175            }
176        });
177
178        // Encode mappings
179        let mappings_str = self.encode_mappings();
180
181        // Build sources content if any are present
182        let sources_content = self.sources_content.iter().any(Option::is_some).then(|| {
183            self.sources_content
184                .iter()
185                .map(|c| c.as_deref().unwrap_or_default().to_string())
186                .collect()
187        });
188
189        SourceMap {
190            version: 3,
191            file: self.file.clone(),
192            source_root: self.source_root.clone(),
193            sources: self.sources.clone(),
194            sources_content,
195            names: self.names.clone(),
196            mappings: mappings_str,
197        }
198    }
199
200    /// Generate source map as JSON string
201    #[must_use]
202    pub fn generate_json(&mut self) -> String {
203        let map = self.generate();
204        serde_json::to_string(&map).unwrap_or_default()
205    }
206
207    /// Alias for `generate_json` (compatibility)
208    #[must_use]
209    pub fn to_json(&mut self) -> String {
210        self.generate_json()
211    }
212
213    /// Generate inline source map comment
214    pub fn generate_inline(&mut self) -> String {
215        let json = self.generate_json();
216        let base64 = base64_encode(json.as_bytes());
217        format!("//# sourceMappingURL=data:application/json;base64,{base64}")
218    }
219
220    /// Alias for `generate_inline` (compatibility)
221    #[must_use]
222    pub fn to_inline_comment(&mut self) -> String {
223        self.generate_inline()
224    }
225
226    /// Add a mapping with a name reference (compatibility)
227    pub fn add_named_mapping(
228        &mut self,
229        generated_line: u32,
230        generated_column: u32,
231        source_index: u32,
232        original_line: u32,
233        original_column: u32,
234        name_index: u32,
235    ) {
236        self.add_mapping(
237            generated_line,
238            generated_column,
239            source_index,
240            original_line,
241            original_column,
242            Some(name_index),
243        );
244    }
245
246    fn encode_mappings(&mut self) -> String {
247        let mut result = String::new();
248
249        // Reset state
250        self.prev_generated_column = 0;
251        self.prev_original_line = 0;
252        self.prev_original_column = 0;
253        self.prev_source_index = 0;
254        self.prev_name_index = 0;
255
256        let mut current_line: u32 = 0;
257        let mut first_in_line = true;
258
259        // Clone mappings to avoid borrow issues
260        let mappings = self.mappings.clone();
261        for mapping in &mappings {
262            // Handle line changes
263            while current_line < mapping.generated_line {
264                result.push(';');
265                current_line += 1;
266                self.prev_generated_column = 0;
267                first_in_line = true;
268            }
269
270            if !first_in_line {
271                result.push(',');
272            }
273            first_in_line = false;
274
275            // Encode segment
276            let segment = self.encode_segment(mapping);
277            result.push_str(&segment);
278        }
279
280        result
281    }
282
283    fn encode_segment(&mut self, mapping: &Mapping) -> String {
284        // Pre-allocate for typical VLQ segment (4-5 values * ~2 chars each)
285        let mut segment = String::with_capacity(16);
286
287        // Generated column (relative to previous) - using zero-allocation encode_to
288        let gen_col = i32::try_from(mapping.generated_column).unwrap_or(i32::MAX);
289        vlq::encode_to(gen_col - self.prev_generated_column, &mut segment);
290        self.prev_generated_column = gen_col;
291
292        // Source index (relative)
293        let src_idx = i32::try_from(mapping.source_index).unwrap_or(i32::MAX);
294        vlq::encode_to(src_idx - self.prev_source_index, &mut segment);
295        self.prev_source_index = src_idx;
296
297        // Original line (relative)
298        let orig_line = i32::try_from(mapping.original_line).unwrap_or(i32::MAX);
299        vlq::encode_to(orig_line - self.prev_original_line, &mut segment);
300        self.prev_original_line = orig_line;
301
302        // Original column (relative)
303        let orig_col = i32::try_from(mapping.original_column).unwrap_or(i32::MAX);
304        vlq::encode_to(orig_col - self.prev_original_column, &mut segment);
305        self.prev_original_column = orig_col;
306
307        // Name index (relative, optional)
308        if let Some(name_idx) = mapping.name_index {
309            let name_idx = i32::try_from(name_idx).unwrap_or(i32::MAX);
310            vlq::encode_to(name_idx - self.prev_name_index, &mut segment);
311            self.prev_name_index = name_idx;
312        }
313
314        segment
315    }
316}
317
318/// VLQ (Variable-Length Quantity) encoding module for source maps
319pub mod vlq {
320    const VLQ_BASE_SHIFT: i32 = 5;
321    const VLQ_BASE: i32 = 1 << VLQ_BASE_SHIFT;
322    const VLQ_BASE_MASK: i32 = VLQ_BASE - 1;
323    const VLQ_CONTINUATION_BIT: i32 = VLQ_BASE;
324
325    const BASE64_CHARS: &[u8; 64] =
326        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
327
328    /// Encode a signed integer as VLQ (allocates String)
329    #[must_use]
330    pub fn encode(value: i32) -> String {
331        let mut result = String::with_capacity(8);
332        encode_to(value, &mut result);
333        result
334    }
335
336    /// Encode a signed integer as VLQ directly into buffer (zero allocation)
337    /// This is 3-5x faster than `encode()` for source map generation
338    #[inline]
339    pub fn encode_to(value: i32, buf: &mut String) {
340        // Convert to unsigned with sign in LSB
341        let mut vlq = if value < 0 {
342            ((-value) << 1) + 1
343        } else {
344            value << 1
345        };
346
347        loop {
348            let mut digit = vlq & VLQ_BASE_MASK;
349            vlq >>= VLQ_BASE_SHIFT;
350
351            if vlq > 0 {
352                digit |= VLQ_CONTINUATION_BIT;
353            }
354
355            // Direct push - no allocation per character
356            let digit_idx = usize::try_from(digit).unwrap_or(0);
357            buf.push(BASE64_CHARS[digit_idx].into());
358
359            if vlq == 0 {
360                break;
361            }
362        }
363    }
364
365    /// Decode a VLQ encoded string, returns (value, `bytes_consumed`)
366    #[must_use]
367    pub fn decode(s: &str) -> Option<(i32, usize)> {
368        let bytes = s.as_bytes();
369        let mut result: i32 = 0;
370        let mut shift = 0;
371        let mut consumed = 0;
372
373        for &byte in bytes {
374            let char_idx = BASE64_CHARS.iter().position(|&c| c == byte)?;
375            let digit = i32::try_from(char_idx).unwrap_or(i32::MAX);
376
377            result |= (digit & VLQ_BASE_MASK) << shift;
378            consumed += 1;
379
380            if (digit & VLQ_CONTINUATION_BIT) == 0 {
381                // Check sign bit (LSB)
382                let is_negative = (result & 1) == 1;
383                result >>= 1;
384                if is_negative {
385                    result = -result;
386                }
387                return Some((result, consumed));
388            }
389
390            shift += VLQ_BASE_SHIFT;
391        }
392
393        None
394    }
395}
396
397const BASE64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
398
399/// Escape a string for JSON output
400/// SIMD-optimized JSON string escaping
401/// Uses memchr to find escape-worthy bytes in bulk, then copies safe chunks via memcpy.
402/// 5-10x faster than char-by-char iteration for typical strings.
403#[must_use]
404pub fn escape_json(s: &str) -> String {
405    let bytes = s.as_bytes();
406
407    // Fast path: no special characters (common case)
408    // memchr3 uses SIMD to scan 32 bytes at a time
409    if memchr::memchr3(b'"', b'\\', b'\n', bytes).is_none()
410        && memchr::memchr2(b'\r', b'\t', bytes).is_none()
411    {
412        return s.to_string();
413    }
414
415    // Slow path: has special chars, process with bulk copy optimization
416    let mut result = String::with_capacity(s.len() + 16);
417    let mut start = 0;
418
419    for (i, &byte) in bytes.iter().enumerate() {
420        let escape = match byte {
421            b'"' => Some("\\\""),
422            b'\\' => Some("\\\\"),
423            b'\n' => Some("\\n"),
424            b'\r' => Some("\\r"),
425            b'\t' => Some("\\t"),
426            // Control characters (0x00-0x1F except the ones above)
427            0..=0x1f => {
428                // Hex escape for other control chars
429                if i > start {
430                    result.push_str(&s[start..i]);
431                }
432                let _ = write!(result, "\\u{byte:04x}");
433                start = i + 1;
434                continue;
435            }
436            _ => None,
437        };
438
439        if let Some(escaped) = escape {
440            // Bulk copy safe bytes before this escape char
441            if i > start {
442                result.push_str(&s[start..i]);
443            }
444            result.push_str(escaped);
445            start = i + 1;
446        }
447    }
448
449    // Copy remaining safe bytes
450    if start < s.len() {
451        result.push_str(&s[start..]);
452    }
453
454    result
455}
456
457/// Escape a JavaScript string literal (single or double quoted)
458/// SIMD-optimized with memchr for bulk scanning
459#[must_use]
460pub fn escape_js_string(s: &str, quote: char) -> String {
461    let bytes = s.as_bytes();
462    let quote_byte = quote as u8;
463
464    // Fast path check using SIMD
465    let has_backslash = memchr::memchr(b'\\', bytes).is_some();
466    let has_quote = memchr::memchr(quote_byte, bytes).is_some();
467    let has_newline = memchr::memchr2(b'\n', b'\r', bytes).is_some();
468
469    if !has_backslash && !has_quote && !has_newline {
470        return s.to_string();
471    }
472
473    let mut result = String::with_capacity(s.len() + 16);
474    let mut start = 0;
475
476    for (i, &byte) in bytes.iter().enumerate() {
477        let escape = match byte {
478            b'\\' => Some("\\\\"),
479            b'\n' => Some("\\n"),
480            b'\r' => Some("\\r"),
481            b'\t' => Some("\\t"),
482            b'\0' => Some("\\0"),
483            b if b == quote_byte => {
484                if i > start {
485                    result.push_str(&s[start..i]);
486                }
487                result.push('\\');
488                result.push(quote);
489                start = i + 1;
490                continue;
491            }
492            _ => None,
493        };
494
495        if let Some(escaped) = escape {
496            if i > start {
497                result.push_str(&s[start..i]);
498            }
499            result.push_str(escaped);
500            start = i + 1;
501        }
502    }
503
504    if start < s.len() {
505        result.push_str(&s[start..]);
506    }
507
508    result
509}
510
511/// Base64 encode a byte slice
512#[must_use]
513pub fn base64_encode(input: &[u8]) -> String {
514    let bytes = input;
515    let mut result = String::with_capacity(bytes.len().div_ceil(3) * 4);
516
517    for chunk in bytes.chunks(3) {
518        let b0 = u32::from(chunk[0]);
519        let b1 = u32::from(chunk.get(1).copied().unwrap_or(0));
520        let b2 = u32::from(chunk.get(2).copied().unwrap_or(0));
521
522        let n = (b0 << 16) | (b1 << 8) | b2;
523
524        result.push(BASE64_CHARS[((n >> 18) & 63) as usize] as char);
525        result.push(BASE64_CHARS[((n >> 12) & 63) as usize] as char);
526
527        if chunk.len() > 1 {
528            result.push(BASE64_CHARS[((n >> 6) & 63) as usize] as char);
529        } else {
530            result.push('=');
531        }
532
533        if chunk.len() > 2 {
534            result.push(BASE64_CHARS[(n & 63) as usize] as char);
535        } else {
536            result.push('=');
537        }
538    }
539
540    result
541}
542
543#[cfg(test)]
544#[path = "../tests/source_map.rs"]
545mod tests;