Skip to main content

fallow_extract/
source_map.rs

1//! Source-position mapping for code extracted from container formats.
2
3use oxc_span::Span;
4
5#[derive(Debug, Clone)]
6struct FragmentMap {
7    generated_start: u32,
8    original_start: u32,
9    len: u32,
10}
11
12/// Extracted source plus byte-offset mappings back to the original file.
13#[derive(Debug, Clone, Default)]
14pub struct ExtractionResult {
15    /// Source text passed to the JavaScript parser.
16    pub body: String,
17    fragments: Vec<FragmentMap>,
18}
19
20impl ExtractionResult {
21    /// Build a mapped result for one contiguous slice.
22    #[must_use]
23    pub fn contiguous(body: &str, original_start: usize) -> Self {
24        let mut result = Self::default();
25        result.push_mapped(body, original_start);
26        result
27    }
28
29    /// Append original source text to the extracted body.
30    pub fn push_mapped(&mut self, text: &str, original_start: usize) {
31        if text.is_empty() {
32            return;
33        }
34        let generated_start = self.body.len();
35        self.body.push_str(text);
36        self.fragments.push(FragmentMap {
37            generated_start: generated_start as u32,
38            original_start: original_start as u32,
39            len: text.len() as u32,
40        });
41    }
42
43    /// Map an extracted-buffer byte offset back to the original source, taking
44    /// the fragment that begins at `offset` when two fragments meet there.
45    fn original_offset_start_biased(&self, offset: u32) -> Option<u32> {
46        let idx = self
47            .fragments
48            .partition_point(|fragment| fragment.generated_start <= offset)
49            .checked_sub(1)?;
50        let fragment = &self.fragments[idx];
51        let delta = offset.checked_sub(fragment.generated_start)?;
52        if delta <= fragment.len {
53            Some(fragment.original_start + delta)
54        } else {
55            None
56        }
57    }
58
59    /// Map an extracted-buffer byte offset back to the original source, taking
60    /// the fragment that ends at `offset` when two fragments meet there.
61    fn original_offset_end_biased(&self, offset: u32) -> Option<u32> {
62        let idx = self
63            .fragments
64            .partition_point(|fragment| fragment.generated_start < offset)
65            .checked_sub(1)?;
66        let fragment = &self.fragments[idx];
67        let delta = offset.checked_sub(fragment.generated_start)?;
68        if delta <= fragment.len {
69            Some(fragment.original_start + delta)
70        } else {
71            None
72        }
73    }
74
75    /// Remap a span from extracted-buffer offsets to original-source offsets.
76    #[must_use]
77    pub fn remap_span(&self, span: Span) -> Span {
78        if span.start == 0 && span.end == 0 {
79            return span;
80        }
81        let Some(start) = self.original_offset_start_biased(span.start) else {
82            return span;
83        };
84        let Some(end) = self.original_offset_end_biased(span.end) else {
85            return span;
86        };
87        Span::new(start, end)
88    }
89}
90
91/// One script section of a container file: the extracted script with its
92/// mapping back to the file, and the source type to parse it with.
93#[derive(Debug, Clone)]
94pub struct ScriptSection {
95    /// The extracted script and its byte mapping.
96    pub extraction: ExtractionResult,
97    /// The source type of the extracted script.
98    pub source_type: oxc_span::SourceType,
99}
100
101/// The inline script sections of a container file: the `<script>` blocks of a
102/// Vue or Svelte SFC, the frontmatter of an Astro component, or the
103/// `import` and `export` statements of an MDX document.
104///
105/// Returns `None` when `path` is not a container format. Then the caller
106/// parses the whole file.
107#[must_use]
108pub fn embedded_script_sections(
109    path: &std::path::Path,
110    source: &str,
111) -> Option<Vec<ScriptSection>> {
112    if crate::sfc::is_sfc_file(path) {
113        return Some(
114            crate::sfc::extract_sfc_scripts(source)
115                .iter()
116                .filter(|script| script.src.is_none())
117                .map(|script| ScriptSection {
118                    extraction: ExtractionResult::contiguous(&script.body, script.byte_offset),
119                    source_type: crate::sfc::source_type_for_script(script),
120                })
121                .collect(),
122        );
123    }
124    if crate::astro::is_astro_file(path) {
125        return Some(
126            crate::astro::extract_astro_frontmatter(source)
127                .iter()
128                .map(|script| ScriptSection {
129                    extraction: ExtractionResult::contiguous(&script.body, script.byte_offset),
130                    source_type: crate::sfc::source_type_for_script(script),
131                })
132                .collect(),
133        );
134    }
135    if crate::mdx::is_mdx_file(path) {
136        return Some(vec![ScriptSection {
137            extraction: crate::mdx::extract_mdx_statements_mapped(source),
138            source_type: oxc_span::SourceType::tsx(),
139        }]);
140    }
141    None
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    /// Two adjacent fragments: `"ab"` from original offset 10, `"cd"` from 100.
149    /// Generated offset 2 is the seam between them.
150    fn two_fragments() -> ExtractionResult {
151        let mut result = ExtractionResult::default();
152        result.push_mapped("ab", 10);
153        result.push_mapped("cd", 100);
154        result
155    }
156
157    #[test]
158    fn start_biased_lookup_takes_the_fragment_beginning_at_the_offset() {
159        let result = two_fragments();
160        assert_eq!(result.original_offset_start_biased(0), Some(10));
161        assert_eq!(result.original_offset_start_biased(1), Some(11));
162        // The seam belongs to the second fragment for a span start.
163        assert_eq!(result.original_offset_start_biased(2), Some(100));
164    }
165
166    #[test]
167    fn end_biased_lookup_takes_the_fragment_ending_at_the_offset() {
168        let result = two_fragments();
169        // The same seam stays in the first fragment for a span end.
170        assert_eq!(result.original_offset_end_biased(2), Some(12));
171        assert_eq!(result.original_offset_end_biased(4), Some(102));
172        // Nothing precedes offset 0, so an end-biased lookup finds no fragment.
173        assert_eq!(result.original_offset_end_biased(0), None);
174    }
175
176    #[test]
177    fn a_fragment_end_maps_but_an_offset_past_it_does_not() {
178        let result = ExtractionResult::contiguous("abcde", 100);
179        assert_eq!(result.original_offset_start_biased(5), Some(105));
180        assert_eq!(result.original_offset_end_biased(5), Some(105));
181        assert_eq!(result.original_offset_start_biased(6), None);
182        assert_eq!(result.original_offset_end_biased(6), None);
183    }
184
185    #[test]
186    fn an_empty_result_maps_nothing() {
187        let result = ExtractionResult::default();
188        assert_eq!(result.original_offset_start_biased(0), None);
189        assert_eq!(result.original_offset_end_biased(0), None);
190    }
191
192    #[test]
193    fn remap_span_pairs_the_start_biased_and_end_biased_lookups() {
194        let result = two_fragments();
195        assert_eq!(result.remap_span(Span::new(1, 4)), Span::new(11, 102));
196        // A zero span is returned untouched.
197        assert_eq!(result.remap_span(Span::new(0, 0)), Span::new(0, 0));
198        // An unmappable endpoint leaves the whole span untouched.
199        assert_eq!(result.remap_span(Span::new(1, 9)), Span::new(1, 9));
200    }
201}