//! Deal with positions in a file.
//!
//! * Convert between byte indices and unist points.
//! * Convert between byte indices into a string which is built up of several
//! slices in a whole document, and byte indices into that whole document.
use crate::unist::Point;
use alloc::{vec, vec::Vec};
/// Each stop represents a new slice, which contains the byte index into the
/// corresponding string where the slice starts (`0`), and the byte index into
/// the whole document where that slice starts (`1`).
pub type Stop = (usize, usize);
#[derive(Debug)]
pub struct Location {
/// List, where each index is a line number (0-based), and each value is
/// the byte index *after* where the line ends.
indices: Vec<usize>,
}
impl Location {
/// Get an index for the given `bytes`.
///
/// Port of <https://github.com/vfile/vfile-location/blob/main/index.js>
#[must_use]
pub fn new(bytes: &[u8]) -> Self {
let mut index = 0;
let mut location_index = Self { indices: vec![] };
while index < bytes.len() {
if bytes[index] == b'\r' {
if index + 1 < bytes.len() && bytes[index + 1] == b'\n' {
location_index.indices.push(index + 2);
index += 1;
} else {
location_index.indices.push(index + 1);
}
} else if bytes[index] == b'\n' {
location_index.indices.push(index + 1);
}
index += 1;
}
location_index.indices.push(index + 1);
location_index
}
/// Get the line and column-based `point` for `offset` in the bound indices.
///
/// Returns `None` when given out of bounds input.
///
/// Port of <https://github.com/vfile/vfile-location/blob/main/index.js>
#[must_use]
pub fn to_point(&self, offset: usize) -> Option<Point> {
let mut index = 0;
if let Some(end) = self.indices.last() {
if offset < *end {
while index < self.indices.len() {
if self.indices[index] > offset {
break;
}
index += 1;
}
let previous = if index > 0 {
self.indices[index - 1]
} else {
0
};
return Some(Point {
line: index + 1,
column: offset + 1 - previous,
offset,
});
}
}
None
}
/// Like `to_point`, but takes a relative offset from a certain string
/// instead of an absolute offset into the whole document.
///
/// The relative offset is made absolute based on `stops`, which represent
/// where that certain string is in the whole document.
#[must_use]
pub fn relative_to_point(&self, stops: &[Stop], relative: usize) -> Option<Point> {
Location::relative_to_absolute(stops, relative).and_then(|absolute| self.to_point(absolute))
}
/// Turn a relative offset into an absolute offset.
#[must_use]
pub fn relative_to_absolute(stops: &[Stop], relative: usize) -> Option<usize> {
let mut index = 0;
while index < stops.len() && stops[index].0 <= relative {
index += 1;
}
// There are no points: that only occurs if there was an empty string.
if index == 0 {
None
} else {
let (stop_relative, stop_absolute) = &stops[index - 1];
Some(stop_absolute + (relative - stop_relative))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_location_lf() {
let location = Location::new("ab\nc".as_bytes());
assert_eq!(
location.to_point(0), // `a`
Some(Point::new(1, 1, 0)),
"should support some points (1)"
);
assert_eq!(
location.to_point(1), // `b`
Some(Point::new(1, 2, 1)),
"should support some points (2)"
);
assert_eq!(
location.to_point(2), // `\n`
Some(Point::new(1, 3, 2)),
"should support some points (3)"
);
assert_eq!(
location.to_point(3), // `c`
Some(Point::new(2, 1, 3)),
"should support some points (4)"
);
assert_eq!(
location.to_point(4), // EOF
// Still gets a point, so that we can represent positions of things
// that end at the last character, the `c` end at `2:2`.
Some(Point::new(2, 2, 4)),
"should support some points (5)"
);
assert_eq!(
location.to_point(5), // Out of bounds
None,
"should support some points (6)"
);
}
#[test]
fn test_location_cr() {
let location = Location::new("a\rb".as_bytes());
assert_eq!(
location.to_point(0), // `a`
Some(Point::new(1, 1, 0)),
"should support some points (1)"
);
assert_eq!(
location.to_point(1), // `\r`
Some(Point::new(1, 2, 1)),
"should support some points (2)"
);
assert_eq!(
location.to_point(2), // `b`
Some(Point::new(2, 1, 2)),
"should support some points (3)"
);
}
#[test]
fn test_location_cr_lf() {
let location = Location::new("a\r\nb".as_bytes());
assert_eq!(
location.to_point(0), // `a`
Some(Point::new(1, 1, 0)),
"should support some points (1)"
);
assert_eq!(
location.to_point(1), // `\r`
Some(Point::new(1, 2, 1)),
"should support some points (2)"
);
assert_eq!(
location.to_point(2), // `\n`
Some(Point::new(1, 3, 2)),
"should support some points (3)"
);
assert_eq!(
location.to_point(3), // `b`
Some(Point::new(2, 1, 3)),
"should support some points (4)"
);
}
}