Markdown parser fork with extended syntax for personal use.
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}