use oxc_span::Span;
#[derive(Debug, Clone)]
struct FragmentMap {
generated_start: u32,
original_start: u32,
len: u32,
}
#[derive(Debug, Clone, Default)]
pub struct ExtractionResult {
pub body: String,
fragments: Vec<FragmentMap>,
}
impl ExtractionResult {
#[must_use]
pub fn contiguous(body: &str, original_start: usize) -> Self {
let mut result = Self::default();
result.push_mapped(body, original_start);
result
}
pub fn push_mapped(&mut self, text: &str, original_start: usize) {
if text.is_empty() {
return;
}
let generated_start = self.body.len();
self.body.push_str(text);
self.fragments.push(FragmentMap {
generated_start: generated_start as u32,
original_start: original_start as u32,
len: text.len() as u32,
});
}
fn original_offset_start_biased(&self, offset: u32) -> Option<u32> {
let idx = self
.fragments
.partition_point(|fragment| fragment.generated_start <= offset)
.checked_sub(1)?;
let fragment = &self.fragments[idx];
let delta = offset.checked_sub(fragment.generated_start)?;
if delta <= fragment.len {
Some(fragment.original_start + delta)
} else {
None
}
}
fn original_offset_end_biased(&self, offset: u32) -> Option<u32> {
let idx = self
.fragments
.partition_point(|fragment| fragment.generated_start < offset)
.checked_sub(1)?;
let fragment = &self.fragments[idx];
let delta = offset.checked_sub(fragment.generated_start)?;
if delta <= fragment.len {
Some(fragment.original_start + delta)
} else {
None
}
}
#[must_use]
pub fn remap_span(&self, span: Span) -> Span {
if span.start == 0 && span.end == 0 {
return span;
}
let Some(start) = self.original_offset_start_biased(span.start) else {
return span;
};
let Some(end) = self.original_offset_end_biased(span.end) else {
return span;
};
Span::new(start, end)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn two_fragments() -> ExtractionResult {
let mut result = ExtractionResult::default();
result.push_mapped("ab", 10);
result.push_mapped("cd", 100);
result
}
#[test]
fn start_biased_lookup_takes_the_fragment_beginning_at_the_offset() {
let result = two_fragments();
assert_eq!(result.original_offset_start_biased(0), Some(10));
assert_eq!(result.original_offset_start_biased(1), Some(11));
assert_eq!(result.original_offset_start_biased(2), Some(100));
}
#[test]
fn end_biased_lookup_takes_the_fragment_ending_at_the_offset() {
let result = two_fragments();
assert_eq!(result.original_offset_end_biased(2), Some(12));
assert_eq!(result.original_offset_end_biased(4), Some(102));
assert_eq!(result.original_offset_end_biased(0), None);
}
#[test]
fn a_fragment_end_maps_but_an_offset_past_it_does_not() {
let result = ExtractionResult::contiguous("abcde", 100);
assert_eq!(result.original_offset_start_biased(5), Some(105));
assert_eq!(result.original_offset_end_biased(5), Some(105));
assert_eq!(result.original_offset_start_biased(6), None);
assert_eq!(result.original_offset_end_biased(6), None);
}
#[test]
fn an_empty_result_maps_nothing() {
let result = ExtractionResult::default();
assert_eq!(result.original_offset_start_biased(0), None);
assert_eq!(result.original_offset_end_biased(0), None);
}
#[test]
fn remap_span_pairs_the_start_biased_and_end_biased_lookups() {
let result = two_fragments();
assert_eq!(result.remap_span(Span::new(1, 4)), Span::new(11, 102));
assert_eq!(result.remap_span(Span::new(0, 0)), Span::new(0, 0));
assert_eq!(result.remap_span(Span::new(1, 9)), Span::new(1, 9));
}
}