use rustlavel_core::Error;
pub fn line_column(source: &str, offset: usize) -> (usize, usize) {
let offset = offset.min(source.len());
let consumed = &source.as_bytes()[..offset];
let line = consumed.iter().filter(|byte| **byte == b'\n').count() + 1;
let column =
consumed.iter().rposition(|byte| *byte == b'\n').map_or(offset, |i| offset - i - 1) + 1;
(line, column)
}
pub fn syntax_error(
file: &str,
source: &str,
offset: usize,
message: impl Into<String>,
) -> Error {
let (line, column) = line_column(source, offset);
Error::Template { file: file.to_string(), line, column, message: message.into() }
}
#[derive(Debug, Clone, Copy)]
pub struct Span<'a> {
pub file: &'a str,
pub source: &'a str,
pub offset: usize,
pub text: &'a str,
}
impl<'a> Span<'a> {
pub fn new(file: &'a str, source: &'a str, offset: usize, len: usize) -> Self {
Span { file, source, offset, text: &source[offset..offset + len] }
}
pub fn error(&self, at: usize, message: impl Into<String>) -> Error {
syntax_error(self.file, self.source, self.offset + at, message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_lines_and_columns_from_one() {
let source = "hello\n world\n";
assert_eq!(line_column(source, 0), (1, 1));
assert_eq!(line_column(source, 4), (1, 5));
assert_eq!(line_column(source, 6), (2, 1));
assert_eq!(line_column(source, 8), (2, 3));
}
#[test]
fn an_offset_past_the_end_lands_on_the_last_position() {
assert_eq!(line_column("ab", 99), (1, 3));
}
#[test]
fn a_span_reports_positions_relative_to_its_fragment() {
let source = "line one\n@if(x == )\n";
let span = Span::new("home", source, 13, 5);
assert_eq!(span.text, "x == ");
match span.error(5, "unexpected end of expression") {
Error::Template { file, line, column, .. } => {
assert_eq!((file.as_str(), line, column), ("home", 2, 10));
}
other => panic!("expected a template error, got {other:?}"),
}
}
}