#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SpanError {
#[error("Byte offset out of bounds: {start}..{end} (source length: {source_len})")]
OutOfBounds {
start: usize,
end: usize,
source_len: usize,
},
#[error("Invalid UTF-8 boundary: byte {byte}")]
InvalidUtf8Boundary { byte: usize },
#[error("Invalid span: uninitialized or default")]
InvalidSpan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Span {
pub start_line: usize,
pub start_col: usize,
pub end_line: usize,
pub end_col: usize,
pub start_byte: usize,
pub end_byte: usize,
}
impl Span {
pub fn new(
start_line: usize,
start_col: usize,
end_line: usize,
end_col: usize,
start_byte: usize,
end_byte: usize,
) -> Self {
Self {
start_line,
start_col,
end_line,
end_col,
start_byte,
end_byte,
}
}
pub fn extract_source<'a>(&self, source: &'a str) -> Result<&'a str, SpanError> {
if !self.is_valid() {
return Err(SpanError::InvalidSpan);
}
if self.start_byte > self.end_byte || self.end_byte > source.len() {
return Err(SpanError::OutOfBounds {
start: self.start_byte,
end: self.end_byte,
source_len: source.len(),
});
}
if !source.is_char_boundary(self.start_byte) {
return Err(SpanError::InvalidUtf8Boundary {
byte: self.start_byte,
});
}
if !source.is_char_boundary(self.end_byte) {
return Err(SpanError::InvalidUtf8Boundary {
byte: self.end_byte,
});
}
Ok(&source[self.start_byte..self.end_byte])
}
pub fn is_valid(&self) -> bool {
self.end_byte > 0
}
pub fn byte_len(&self) -> usize {
self.end_byte.saturating_sub(self.start_byte)
}
}
impl<'i> From<&pest::Span<'i>> for Span {
fn from(pest_span: &pest::Span<'i>) -> Self {
let (start_line, start_col) = pest_span.start_pos().line_col();
let (end_line, end_col) = pest_span.end_pos().line_col();
Self::new(
start_line,
start_col,
end_line,
end_col,
pest_span.start(),
pest_span.end(),
)
}
}