markdown/util/
location.rs

1//! Deal with positions in a file.
2//!
3//! * Convert between byte indices and unist points.
4//! * Convert between byte indices into a string which is built up of several
5//!   slices in a whole document, and byte indices into that whole document.
6
7use crate::unist::Point;
8use alloc::{vec, vec::Vec};
9
10/// Each stop represents a new slice, which contains the byte index into the
11/// corresponding string where the slice starts (`0`), and the byte index into
12/// the whole document where that slice starts (`1`).
13pub type Stop = (usize, usize);
14
15#[derive(Debug)]
16pub struct Location {
17    /// List, where each index is a line number (0-based), and each value is
18    /// the byte index *after* where the line ends.
19    indices: Vec<usize>,
20}
21
22impl Location {
23    /// Get an index for the given `bytes`.
24    ///
25    /// Port of <https://github.com/vfile/vfile-location/blob/main/index.js>
26    #[must_use]
27    pub fn new(bytes: &[u8]) -> Self {
28        let mut index = 0;
29        let mut location_index = Self { indices: vec![] };
30
31        while index < bytes.len() {
32            if bytes[index] == b'\r' {
33                if index + 1 < bytes.len() && bytes[index + 1] == b'\n' {
34                    location_index.indices.push(index + 2);
35                    index += 1;
36                } else {
37                    location_index.indices.push(index + 1);
38                }
39            } else if bytes[index] == b'\n' {
40                location_index.indices.push(index + 1);
41            }
42
43            index += 1;
44        }
45
46        location_index.indices.push(index + 1);
47        location_index
48    }
49
50    /// Get the line and column-based `point` for `offset` in the bound indices.
51    ///
52    /// Returns `None` when given out of bounds input.
53    ///
54    /// Port of <https://github.com/vfile/vfile-location/blob/main/index.js>
55    #[must_use]
56    pub fn to_point(&self, offset: usize) -> Option<Point> {
57        let mut index = 0;
58
59        if let Some(end) = self.indices.last() {
60            if offset < *end {
61                while index < self.indices.len() {
62                    if self.indices[index] > offset {
63                        break;
64                    }
65
66                    index += 1;
67                }
68
69                let previous = if index > 0 {
70                    self.indices[index - 1]
71                } else {
72                    0
73                };
74                return Some(Point::new(index + 1, offset + 1 - previous, offset));
75            }
76        }
77
78        None
79    }
80
81    /// Like `to_point`, but takes a relative offset from a certain string
82    /// instead of an absolute offset into the whole document.
83    ///
84    /// The relative offset is made absolute based on `stops`, which represent
85    /// where that certain string is in the whole document.
86    #[must_use]
87    pub fn relative_to_point(&self, stops: &[Stop], relative: usize) -> Option<Point> {
88        Location::relative_to_absolute(stops, relative).and_then(|absolute| self.to_point(absolute))
89    }
90
91    /// Turn a relative offset into an absolute offset.
92    #[must_use]
93    pub fn relative_to_absolute(stops: &[Stop], relative: usize) -> Option<usize> {
94        let mut index = 0;
95
96        while index < stops.len() && stops[index].0 <= relative {
97            index += 1;
98        }
99
100        // There are no points: that only occurs if there was an empty string.
101        if index == 0 {
102            None
103        } else {
104            let (stop_relative, stop_absolute) = &stops[index - 1];
105            Some(stop_absolute + (relative - stop_relative))
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_location_lf() {
116        let location = Location::new("ab\nc".as_bytes());
117        assert_eq!(
118            location.to_point(0), // `a`
119            Some(Point::new(1, 1, 0)),
120            "should support some points (1)"
121        );
122        assert_eq!(
123            location.to_point(1), // `b`
124            Some(Point::new(1, 2, 1)),
125            "should support some points (2)"
126        );
127        assert_eq!(
128            location.to_point(2), // `\n`
129            Some(Point::new(1, 3, 2)),
130            "should support some points (3)"
131        );
132        assert_eq!(
133            location.to_point(3), // `c`
134            Some(Point::new(2, 1, 3)),
135            "should support some points (4)"
136        );
137        assert_eq!(
138            location.to_point(4), // EOF
139            // Still gets a point, so that we can represent positions of things
140            // that end at the last character, the `c` end at `2:2`.
141            Some(Point::new(2, 2, 4)),
142            "should support some points (5)"
143        );
144        assert_eq!(
145            location.to_point(5), // Out of bounds
146            None,
147            "should support some points (6)"
148        );
149    }
150
151    #[test]
152    fn test_location_cr() {
153        let location = Location::new("a\rb".as_bytes());
154        assert_eq!(
155            location.to_point(0), // `a`
156            Some(Point::new(1, 1, 0)),
157            "should support some points (1)"
158        );
159        assert_eq!(
160            location.to_point(1), // `\r`
161            Some(Point::new(1, 2, 1)),
162            "should support some points (2)"
163        );
164        assert_eq!(
165            location.to_point(2), // `b`
166            Some(Point::new(2, 1, 2)),
167            "should support some points (3)"
168        );
169    }
170
171    #[test]
172    fn test_location_cr_lf() {
173        let location = Location::new("a\r\nb".as_bytes());
174        assert_eq!(
175            location.to_point(0), // `a`
176            Some(Point::new(1, 1, 0)),
177            "should support some points (1)"
178        );
179        assert_eq!(
180            location.to_point(1), // `\r`
181            Some(Point::new(1, 2, 1)),
182            "should support some points (2)"
183        );
184        assert_eq!(
185            location.to_point(2), // `\n`
186            Some(Point::new(1, 3, 2)),
187            "should support some points (3)"
188        );
189        assert_eq!(
190            location.to_point(3), // `b`
191            Some(Point::new(2, 1, 3)),
192            "should support some points (4)"
193        );
194    }
195    #[test]
196    fn test_empty() {
197        let location = Location::new("".as_bytes());
198        assert_eq!(location.to_point(0), Some(Point::new(1, 1, 0)), "to_point");
199        assert_eq!(
200            location.relative_to_point(&[], 0),
201            None,
202            "relative_to_point"
203        );
204        assert_eq!(
205            Location::relative_to_absolute(&[], 0),
206            None,
207            "relative_to_absolute"
208        );
209    }
210}