/rust/registry/src/index.crates.io-1949cf8c6b5b557f/pest-2.9.5/src/position.rs
Line | Count | Source |
1 | | // pest. The Elegant Parser |
2 | | // Copyright (c) 2018 Dragoș Tiselice |
3 | | // |
4 | | // Licensed under the Apache License, Version 2.0 |
5 | | // <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT |
6 | | // license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
7 | | // option. All files in the project carrying such notice may not be copied, |
8 | | // modified, or distributed except according to those terms. |
9 | | |
10 | | use core::cmp::Ordering; |
11 | | use core::fmt; |
12 | | use core::hash::{Hash, Hasher}; |
13 | | use core::ops::Range; |
14 | | use core::ptr; |
15 | | use core::str; |
16 | | |
17 | | use crate::span; |
18 | | |
19 | | /// A cursor position in a `&str` which provides useful methods to manually parse that string. |
20 | | #[derive(Clone, Copy)] |
21 | | pub struct Position<'i> { |
22 | | input: &'i str, |
23 | | pos: usize, |
24 | | } |
25 | | |
26 | | impl<'i> Position<'i> { |
27 | | /// Create a new `Position` without checking invariants. (Checked with `debug_assertions`.) |
28 | 0 | pub(crate) fn new_internal(input: &str, pos: usize) -> Position<'_> { |
29 | 0 | debug_assert!(input.get(pos..).is_some()); |
30 | 0 | Position { input, pos } |
31 | 0 | } |
32 | | |
33 | | /// Attempts to create a new `Position` at the given position. If the specified position is |
34 | | /// an invalid index, or the specified position is not a valid UTF8 boundary, then None is |
35 | | /// returned. |
36 | | /// |
37 | | /// # Examples |
38 | | /// ``` |
39 | | /// # use pest::Position; |
40 | | /// let cheart = '💖'; |
41 | | /// let heart = "💖"; |
42 | | /// assert_eq!(Position::new(heart, 1), None); |
43 | | /// assert_ne!(Position::new(heart, cheart.len_utf8()), None); |
44 | | /// ``` |
45 | 0 | pub fn new(input: &str, pos: usize) -> Option<Position<'_>> { |
46 | 0 | input.get(pos..).map(|_| Position { input, pos }) |
47 | 0 | } |
48 | | |
49 | | /// Creates a `Position` at the start of a `&str`. |
50 | | /// |
51 | | /// # Examples |
52 | | /// |
53 | | /// ``` |
54 | | /// # use pest::Position; |
55 | | /// let start = Position::from_start(""); |
56 | | /// assert_eq!(start.pos(), 0); |
57 | | /// ``` |
58 | | #[inline] |
59 | 0 | pub fn from_start(input: &'i str) -> Position<'i> { |
60 | | // Position 0 is always safe because it's always a valid UTF-8 border. |
61 | 0 | Position { input, pos: 0 } |
62 | 0 | } Unexecuted instantiation: <pest::position::Position>::from_start Unexecuted instantiation: <pest::position::Position>::from_start |
63 | | |
64 | | /// Returns the byte position of this `Position` as a `usize`. |
65 | | /// |
66 | | /// # Examples |
67 | | /// |
68 | | /// ``` |
69 | | /// # use pest::Position; |
70 | | /// let input = "ab"; |
71 | | /// let mut start = Position::from_start(input); |
72 | | /// |
73 | | /// assert_eq!(start.pos(), 0); |
74 | | /// ``` |
75 | | #[inline] |
76 | 0 | pub fn pos(&self) -> usize { |
77 | 0 | self.pos |
78 | 0 | } Unexecuted instantiation: <pest::position::Position>::pos Unexecuted instantiation: <pest::position::Position>::pos |
79 | | |
80 | | /// Creates a `Span` from two `Position`s. |
81 | | /// |
82 | | /// # Panics |
83 | | /// |
84 | | /// Panics if the positions come from different inputs. |
85 | | /// |
86 | | /// # Examples |
87 | | /// |
88 | | /// ``` |
89 | | /// # use pest::Position; |
90 | | /// let input = "ab"; |
91 | | /// let start = Position::from_start(input); |
92 | | /// let span = start.span(&start.clone()); |
93 | | /// |
94 | | /// assert_eq!(span.start(), 0); |
95 | | /// assert_eq!(span.end(), 0); |
96 | | /// ``` |
97 | | #[inline] |
98 | 0 | pub fn span(&self, other: &Position<'i>) -> span::Span<'i> { |
99 | 0 | if ptr::eq(self.input, other.input) |
100 | | /* && self.input.get(self.pos..other.pos).is_some() */ |
101 | | { |
102 | 0 | span::Span::new_internal(self.input, self.pos, other.pos) |
103 | | } else { |
104 | | // TODO: maybe a panic if self.pos < other.pos |
105 | 0 | panic!("span created from positions from different inputs") |
106 | | } |
107 | 0 | } |
108 | | |
109 | | /// Returns the line and column number of this `Position`. |
110 | | /// |
111 | | /// This is an O(n) operation, where n is the number of chars in the input. |
112 | | /// You better use [`pair.line_col()`](struct.Pair.html#method.line_col) instead. |
113 | | /// |
114 | | /// # Panics |
115 | | /// |
116 | | /// Panics if the position is out of bounds. |
117 | | /// |
118 | | /// # Examples |
119 | | /// |
120 | | /// ``` |
121 | | /// # use pest; |
122 | | /// # #[allow(non_camel_case_types)] |
123 | | /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
124 | | /// enum Rule {} |
125 | | /// |
126 | | /// let input = "\na"; |
127 | | /// let mut state: Box<pest::ParserState<'_, Rule>> = pest::ParserState::new(input); |
128 | | /// let mut result = state.match_string("\na"); |
129 | | /// assert!(result.is_ok()); |
130 | | /// assert_eq!(result.unwrap().position().line_col(), (2, 2)); |
131 | | /// ``` |
132 | | #[inline] |
133 | 0 | pub fn line_col(&self) -> (usize, usize) { |
134 | 0 | if self.pos > self.input.len() { |
135 | 0 | panic!("position out of bounds"); |
136 | 0 | } |
137 | 0 | let mut pos = self.pos; |
138 | 0 | let slice = &self.input[..pos]; |
139 | 0 | let mut chars = slice.chars().peekable(); |
140 | | |
141 | 0 | let mut line_col = (1, 1); |
142 | | |
143 | 0 | while pos != 0 { |
144 | 0 | match chars.next() { |
145 | | Some('\r') => { |
146 | 0 | if let Some(&'\n') = chars.peek() { |
147 | 0 | chars.next(); |
148 | | |
149 | 0 | if pos == 1 { |
150 | 0 | pos -= 1; |
151 | 0 | } else { |
152 | 0 | pos -= 2; |
153 | 0 | } |
154 | | |
155 | 0 | line_col = (line_col.0 + 1, 1); |
156 | 0 | } else { |
157 | 0 | pos -= 1; |
158 | 0 | line_col = (line_col.0, line_col.1 + 1); |
159 | 0 | } |
160 | | } |
161 | 0 | Some('\n') => { |
162 | 0 | pos -= 1; |
163 | 0 | line_col = (line_col.0 + 1, 1); |
164 | 0 | } |
165 | 0 | Some(c) => { |
166 | 0 | pos -= c.len_utf8(); |
167 | 0 | line_col = (line_col.0, line_col.1 + 1); |
168 | 0 | } |
169 | 0 | None => unreachable!(), |
170 | | } |
171 | | } |
172 | | |
173 | 0 | line_col |
174 | 0 | } Unexecuted instantiation: <pest::position::Position>::line_col Unexecuted instantiation: <pest::position::Position>::line_col |
175 | | |
176 | | /// Returns the entire line of the input that contains this `Position`. |
177 | | /// |
178 | | /// # Panics |
179 | | /// |
180 | | /// Panics if the position is out of bounds (e.g., beyond the end of the input). |
181 | | /// |
182 | | /// # Examples |
183 | | /// |
184 | | /// ``` |
185 | | /// # use pest; |
186 | | /// # #[allow(non_camel_case_types)] |
187 | | /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
188 | | /// enum Rule {} |
189 | | /// |
190 | | /// let input = "\na"; |
191 | | /// let mut state: Box<pest::ParserState<'_, Rule>> = pest::ParserState::new(input); |
192 | | /// let mut result = state.match_string("\na"); |
193 | | /// assert!(result.is_ok()); |
194 | | /// assert_eq!(result.unwrap().position().line_of(), "a"); |
195 | | /// ``` |
196 | | #[inline] |
197 | 0 | pub fn line_of(&self) -> &'i str { |
198 | 0 | if self.pos > self.input.len() { |
199 | 0 | panic!("position out of bounds"); |
200 | 0 | }; |
201 | | // Safe since start and end can only be valid UTF-8 borders. |
202 | 0 | &self.input[self.find_line_start()..self.find_line_end()] |
203 | 0 | } Unexecuted instantiation: <pest::position::Position>::line_of Unexecuted instantiation: <pest::position::Position>::line_of |
204 | | |
205 | 0 | pub(crate) fn find_line_start(&self) -> usize { |
206 | 0 | if self.input.is_empty() { |
207 | 0 | return 0; |
208 | 0 | }; |
209 | | // Position's pos is always a UTF-8 border. |
210 | 0 | let start = self |
211 | 0 | .input |
212 | 0 | .char_indices() |
213 | 0 | .rev() |
214 | 0 | .skip_while(|&(i, _)| i >= self.pos) |
215 | 0 | .find(|&(_, c)| c == '\n'); |
216 | 0 | match start { |
217 | 0 | Some((i, _)) => i + 1, |
218 | 0 | None => 0, |
219 | | } |
220 | 0 | } |
221 | | |
222 | 0 | pub(crate) fn find_line_end(&self) -> usize { |
223 | 0 | if self.input.is_empty() { |
224 | 0 | 0 |
225 | 0 | } else if self.pos == self.input.len() - 1 { |
226 | 0 | self.input.len() |
227 | | } else { |
228 | | // Position's pos is always a UTF-8 border. |
229 | 0 | let end = self |
230 | 0 | .input |
231 | 0 | .char_indices() |
232 | 0 | .skip_while(|&(i, _)| i < self.pos) |
233 | 0 | .find(|&(_, c)| c == '\n'); |
234 | 0 | match end { |
235 | 0 | Some((i, _)) => i + 1, |
236 | 0 | None => self.input.len(), |
237 | | } |
238 | | } |
239 | 0 | } |
240 | | |
241 | | /// Returns `true` when the `Position` points to the start of the input `&str`. |
242 | | #[inline] |
243 | 0 | pub(crate) fn at_start(&self) -> bool { |
244 | 0 | self.pos == 0 |
245 | 0 | } Unexecuted instantiation: <pest::position::Position>::at_start Unexecuted instantiation: <pest::position::Position>::at_start |
246 | | |
247 | | /// Returns `true` when the `Position` points to the end of the input `&str`. |
248 | | #[inline] |
249 | 0 | pub(crate) fn at_end(&self) -> bool { |
250 | 0 | self.pos == self.input.len() |
251 | 0 | } Unexecuted instantiation: <pest::position::Position>::at_end Unexecuted instantiation: <pest::position::Position>::at_end |
252 | | |
253 | | /// Skips `n` `char`s from the `Position` and returns `true` if the skip was possible or `false` |
254 | | /// otherwise. If the return value is `false`, `pos` will not be updated. |
255 | | #[inline] |
256 | 0 | pub(crate) fn skip(&mut self, n: usize) -> bool { |
257 | 0 | let skipped = { |
258 | 0 | let mut len = 0; |
259 | | // Position's pos is always a UTF-8 border. |
260 | 0 | let mut chars = self.input[self.pos..].chars(); |
261 | 0 | for _ in 0..n { |
262 | 0 | if let Some(c) = chars.next() { |
263 | 0 | len += c.len_utf8(); |
264 | 0 | } else { |
265 | 0 | return false; |
266 | | } |
267 | | } |
268 | 0 | len |
269 | | }; |
270 | | |
271 | 0 | self.pos += skipped; |
272 | 0 | true |
273 | 0 | } |
274 | | |
275 | | /// Goes back `n` `char`s from the `Position` and returns `true` if the skip was possible or `false` |
276 | | /// otherwise. If the return value is `false`, `pos` will not be updated. |
277 | | #[inline] |
278 | 0 | pub(crate) fn skip_back(&mut self, n: usize) -> bool { |
279 | 0 | let skipped = { |
280 | 0 | let mut len = 0; |
281 | | // Position's pos is always a UTF-8 border. |
282 | 0 | let mut chars = self.input[..self.pos].chars().rev(); |
283 | 0 | for _ in 0..n { |
284 | 0 | if let Some(c) = chars.next() { |
285 | 0 | len += c.len_utf8(); |
286 | 0 | } else { |
287 | 0 | return false; |
288 | | } |
289 | | } |
290 | 0 | len |
291 | | }; |
292 | | |
293 | 0 | self.pos -= skipped; |
294 | 0 | true |
295 | 0 | } |
296 | | |
297 | | /// Skips until one of the given `strings` is found. If none of the `strings` can be found, |
298 | | /// this function will return `false` but its `pos` will *still* be updated. |
299 | | #[inline] |
300 | 0 | pub(crate) fn skip_until(&mut self, strings: &[&str]) -> bool { |
301 | | #[cfg(not(feature = "memchr"))] |
302 | | { |
303 | | self.skip_until_basic(strings) |
304 | | } |
305 | | #[cfg(feature = "memchr")] |
306 | | { |
307 | 0 | match strings { |
308 | 0 | [] => (), |
309 | 0 | [s1] => { |
310 | 0 | if let Some(from) = |
311 | 0 | memchr::memmem::find(&self.input.as_bytes()[self.pos..], s1.as_bytes()) |
312 | | { |
313 | 0 | self.pos += from; |
314 | 0 | return true; |
315 | 0 | } |
316 | | } |
317 | 0 | [s1, s2] if !s1.is_empty() && !s2.is_empty() => { |
318 | 0 | let b1 = s1.as_bytes()[0]; |
319 | 0 | let b2 = s2.as_bytes()[0]; |
320 | 0 | let miter = memchr::memchr2_iter(b1, b2, &self.input.as_bytes()[self.pos..]); |
321 | 0 | for from in miter { |
322 | 0 | let start = &self.input[self.pos + from..]; |
323 | 0 | if start.starts_with(s1) || start.starts_with(s2) { |
324 | 0 | self.pos += from; |
325 | 0 | return true; |
326 | 0 | } |
327 | | } |
328 | | } |
329 | 0 | [s1, s2, s3] if !s1.is_empty() && !s2.is_empty() && !s3.is_empty() => { |
330 | 0 | let b1 = s1.as_bytes()[0]; |
331 | 0 | let b2 = s2.as_bytes()[0]; |
332 | 0 | let b3 = s3.as_bytes()[0]; |
333 | 0 | let miter = |
334 | 0 | memchr::memchr3_iter(b1, b2, b3, &self.input.as_bytes()[self.pos..]); |
335 | 0 | for from in miter { |
336 | 0 | let start = &self.input[self.pos + from..]; |
337 | 0 | if start.starts_with(s1) || start.starts_with(s2) || start.starts_with(s3) { |
338 | 0 | self.pos += from; |
339 | 0 | return true; |
340 | 0 | } |
341 | | } |
342 | | } |
343 | | _ => { |
344 | 0 | return self.skip_until_basic(strings); |
345 | | } |
346 | | } |
347 | 0 | self.pos = self.input.len(); |
348 | 0 | false |
349 | | } |
350 | 0 | } |
351 | | |
352 | | #[inline] |
353 | 0 | fn skip_until_basic(&mut self, strings: &[&str]) -> bool { |
354 | | // TODO: optimize with Aho-Corasick, e.g. https://crates.io/crates/daachorse? |
355 | 0 | for from in self.pos..self.input.len() { |
356 | 0 | let bytes = if let Some(string) = self.input.get(from..) { |
357 | 0 | string.as_bytes() |
358 | | } else { |
359 | 0 | continue; |
360 | | }; |
361 | | |
362 | 0 | for slice in strings.iter() { |
363 | 0 | let to = slice.len(); |
364 | 0 | if Some(slice.as_bytes()) == bytes.get(0..to) { |
365 | 0 | self.pos = from; |
366 | 0 | return true; |
367 | 0 | } |
368 | | } |
369 | | } |
370 | | |
371 | 0 | self.pos = self.input.len(); |
372 | 0 | false |
373 | 0 | } |
374 | | |
375 | | /// Matches the char at the `Position` against a specified character and returns `true` if a match |
376 | | /// was made. If no match was made, returns `false`. |
377 | | /// `pos` will not be updated in either case. |
378 | | #[inline] |
379 | 0 | pub(crate) fn match_char(&self, c: char) -> bool { |
380 | 0 | matches!(self.input[self.pos..].chars().next(), Some(cc) if c == cc) |
381 | 0 | } Unexecuted instantiation: <pest::position::Position>::match_char Unexecuted instantiation: <pest::position::Position>::match_char |
382 | | |
383 | | /// Matches the char at the `Position` against a filter function and returns `true` if a match |
384 | | /// was made. If no match was made, returns `false` and `pos` will not be updated. |
385 | | #[inline] |
386 | 0 | pub(crate) fn match_char_by<F>(&mut self, f: F) -> bool |
387 | 0 | where |
388 | 0 | F: FnOnce(char) -> bool, |
389 | | { |
390 | 0 | if let Some(c) = self.input[self.pos..].chars().next() { |
391 | 0 | if f(c) { |
392 | 0 | self.pos += c.len_utf8(); |
393 | 0 | true |
394 | | } else { |
395 | 0 | false |
396 | | } |
397 | | } else { |
398 | 0 | false |
399 | | } |
400 | 0 | } |
401 | | |
402 | | /// Matches `string` from the `Position` and returns `true` if a match was made or `false` |
403 | | /// otherwise. If no match was made, `pos` will not be updated. |
404 | | #[inline] |
405 | 0 | pub(crate) fn match_string(&mut self, string: &str) -> bool { |
406 | 0 | let to = self.pos + string.len(); |
407 | | |
408 | 0 | if Some(string.as_bytes()) == self.input.as_bytes().get(self.pos..to) { |
409 | 0 | self.pos = to; |
410 | 0 | true |
411 | | } else { |
412 | 0 | false |
413 | | } |
414 | 0 | } Unexecuted instantiation: <pest::position::Position>::match_string Unexecuted instantiation: <pest::position::Position>::match_string |
415 | | |
416 | | /// Case-insensitively matches `string` from the `Position` and returns `true` if a match was |
417 | | /// made or `false` otherwise. If no match was made, `pos` will not be updated. |
418 | | #[inline] |
419 | 0 | pub(crate) fn match_insensitive(&mut self, string: &str) -> bool { |
420 | 0 | let matched = { |
421 | 0 | let slice = &self.input[self.pos..]; |
422 | 0 | if let Some(slice) = slice.get(0..string.len()) { |
423 | 0 | slice.eq_ignore_ascii_case(string) |
424 | | } else { |
425 | 0 | false |
426 | | } |
427 | | }; |
428 | | |
429 | 0 | if matched { |
430 | 0 | self.pos += string.len(); |
431 | 0 | true |
432 | | } else { |
433 | 0 | false |
434 | | } |
435 | 0 | } |
436 | | |
437 | | /// Matches `char` `range` from the `Position` and returns `true` if a match was made or `false` |
438 | | /// otherwise. If no match was made, `pos` will not be updated. |
439 | | #[inline] |
440 | 0 | pub(crate) fn match_range(&mut self, range: Range<char>) -> bool { |
441 | 0 | if let Some(c) = self.input[self.pos..].chars().next() { |
442 | 0 | if range.start <= c && c <= range.end { |
443 | 0 | self.pos += c.len_utf8(); |
444 | 0 | return true; |
445 | 0 | } |
446 | 0 | } |
447 | | |
448 | 0 | false |
449 | 0 | } Unexecuted instantiation: <pest::position::Position>::match_range Unexecuted instantiation: <pest::position::Position>::match_range |
450 | | } |
451 | | |
452 | | impl fmt::Debug for Position<'_> { |
453 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
454 | 0 | f.debug_struct("Position").field("pos", &self.pos).finish() |
455 | 0 | } |
456 | | } |
457 | | |
458 | | impl<'i> PartialEq for Position<'i> { |
459 | 0 | fn eq(&self, other: &Position<'i>) -> bool { |
460 | 0 | ptr::eq(self.input, other.input) && self.pos == other.pos |
461 | 0 | } |
462 | | } |
463 | | |
464 | | impl Eq for Position<'_> {} |
465 | | |
466 | | #[allow(clippy::non_canonical_partial_ord_impl)] |
467 | | impl<'i> PartialOrd for Position<'i> { |
468 | 0 | fn partial_cmp(&self, other: &Position<'i>) -> Option<Ordering> { |
469 | 0 | if ptr::eq(self.input, other.input) { |
470 | 0 | self.pos.partial_cmp(&other.pos) |
471 | | } else { |
472 | 0 | None |
473 | | } |
474 | 0 | } |
475 | | } |
476 | | |
477 | | /// # Panics |
478 | | /// |
479 | | /// Panics when comparing positions from different input strings. |
480 | | impl<'i> Ord for Position<'i> { |
481 | 0 | fn cmp(&self, other: &Position<'i>) -> Ordering { |
482 | 0 | self.partial_cmp(other) |
483 | 0 | .expect("cannot compare positions from different strs") |
484 | 0 | } |
485 | | } |
486 | | |
487 | | impl Hash for Position<'_> { |
488 | 0 | fn hash<H: Hasher>(&self, state: &mut H) { |
489 | 0 | (self.input as *const str).hash(state); |
490 | 0 | self.pos.hash(state); |
491 | 0 | } |
492 | | } |
493 | | |
494 | | #[cfg(test)] |
495 | | mod tests { |
496 | | use super::*; |
497 | | |
498 | | #[test] |
499 | | fn empty() { |
500 | | let input = ""; |
501 | | assert!(Position::new(input, 0).unwrap().match_string("")); |
502 | | assert!(!Position::new(input, 0).unwrap().match_string("a")); |
503 | | } |
504 | | |
505 | | #[test] |
506 | | fn parts() { |
507 | | let input = "asdasdf"; |
508 | | |
509 | | assert!(Position::new(input, 0).unwrap().match_string("asd")); |
510 | | assert!(Position::new(input, 3).unwrap().match_string("asdf")); |
511 | | } |
512 | | |
513 | | #[test] |
514 | | fn line_col() { |
515 | | let input = "a\rb\nc\r\nd嗨"; |
516 | | |
517 | | assert_eq!(Position::new(input, 0).unwrap().line_col(), (1, 1)); |
518 | | assert_eq!(Position::new(input, 1).unwrap().line_col(), (1, 2)); |
519 | | assert_eq!(Position::new(input, 2).unwrap().line_col(), (1, 3)); |
520 | | assert_eq!(Position::new(input, 3).unwrap().line_col(), (1, 4)); |
521 | | assert_eq!(Position::new(input, 4).unwrap().line_col(), (2, 1)); |
522 | | assert_eq!(Position::new(input, 5).unwrap().line_col(), (2, 2)); |
523 | | assert_eq!(Position::new(input, 6).unwrap().line_col(), (2, 3)); |
524 | | assert_eq!(Position::new(input, 7).unwrap().line_col(), (3, 1)); |
525 | | assert_eq!(Position::new(input, 8).unwrap().line_col(), (3, 2)); |
526 | | assert_eq!(Position::new(input, 11).unwrap().line_col(), (3, 3)); |
527 | | let input = "abcd嗨"; |
528 | | assert_eq!(Position::new(input, 7).unwrap().line_col(), (1, 6)); |
529 | | } |
530 | | |
531 | | #[test] |
532 | | fn line_of() { |
533 | | let input = "a\rb\nc\r\nd嗨"; |
534 | | |
535 | | assert_eq!(Position::new(input, 0).unwrap().line_of(), "a\rb\n"); |
536 | | assert_eq!(Position::new(input, 1).unwrap().line_of(), "a\rb\n"); |
537 | | assert_eq!(Position::new(input, 2).unwrap().line_of(), "a\rb\n"); |
538 | | assert_eq!(Position::new(input, 3).unwrap().line_of(), "a\rb\n"); |
539 | | assert_eq!(Position::new(input, 4).unwrap().line_of(), "c\r\n"); |
540 | | assert_eq!(Position::new(input, 5).unwrap().line_of(), "c\r\n"); |
541 | | assert_eq!(Position::new(input, 6).unwrap().line_of(), "c\r\n"); |
542 | | assert_eq!(Position::new(input, 7).unwrap().line_of(), "d嗨"); |
543 | | assert_eq!(Position::new(input, 8).unwrap().line_of(), "d嗨"); |
544 | | assert_eq!(Position::new(input, 11).unwrap().line_of(), "d嗨"); |
545 | | } |
546 | | |
547 | | #[test] |
548 | | fn line_of_empty() { |
549 | | let input = ""; |
550 | | |
551 | | assert_eq!(Position::new(input, 0).unwrap().line_of(), ""); |
552 | | } |
553 | | |
554 | | #[test] |
555 | | fn line_of_new_line() { |
556 | | let input = "\n"; |
557 | | |
558 | | assert_eq!(Position::new(input, 0).unwrap().line_of(), "\n"); |
559 | | } |
560 | | |
561 | | #[test] |
562 | | fn line_of_between_new_line() { |
563 | | let input = "\n\n"; |
564 | | |
565 | | assert_eq!(Position::new(input, 1).unwrap().line_of(), "\n"); |
566 | | } |
567 | | |
568 | | fn measure_skip(input: &str, pos: usize, n: usize) -> Option<usize> { |
569 | | let mut p = Position::new(input, pos).unwrap(); |
570 | | if p.skip(n) { |
571 | | Some(p.pos - pos) |
572 | | } else { |
573 | | None |
574 | | } |
575 | | } |
576 | | |
577 | | #[test] |
578 | | fn skip_empty() { |
579 | | let input = ""; |
580 | | |
581 | | assert_eq!(measure_skip(input, 0, 0), Some(0)); |
582 | | assert_eq!(measure_skip(input, 0, 1), None); |
583 | | } |
584 | | |
585 | | #[test] |
586 | | fn skip() { |
587 | | let input = "d嗨"; |
588 | | |
589 | | assert_eq!(measure_skip(input, 0, 0), Some(0)); |
590 | | assert_eq!(measure_skip(input, 0, 1), Some(1)); |
591 | | assert_eq!(measure_skip(input, 1, 1), Some(3)); |
592 | | } |
593 | | |
594 | | #[test] |
595 | | fn skip_until() { |
596 | | let input = "ab ac"; |
597 | | let pos = Position::from_start(input); |
598 | | |
599 | | let mut test_pos = pos; |
600 | | test_pos.skip_until(&["a", "b"]); |
601 | | assert_eq!(test_pos.pos(), 0); |
602 | | |
603 | | test_pos = pos; |
604 | | test_pos.skip_until(&["b"]); |
605 | | assert_eq!(test_pos.pos(), 1); |
606 | | |
607 | | test_pos = pos; |
608 | | test_pos.skip_until(&["ab"]); |
609 | | assert_eq!(test_pos.pos(), 0); |
610 | | |
611 | | test_pos = pos; |
612 | | test_pos.skip_until(&["ac", "z"]); |
613 | | assert_eq!(test_pos.pos(), 3); |
614 | | |
615 | | test_pos = pos; |
616 | | assert!(!test_pos.skip_until(&["z"])); |
617 | | assert_eq!(test_pos.pos(), 5); |
618 | | } |
619 | | |
620 | | #[test] |
621 | | fn skip_until_three_strings() { |
622 | | let input = "xx yy b"; |
623 | | let pos = Position::from_start(input); |
624 | | |
625 | | // All three strings non-empty: the three-string fast path must find "b". |
626 | | let mut test_pos = pos; |
627 | | assert!(test_pos.skip_until(&["a", "c", "b"])); |
628 | | assert_eq!(test_pos.pos(), 6); |
629 | | |
630 | | // A string that does not occur: nothing matches, position goes to the end. |
631 | | test_pos = pos; |
632 | | assert!(!test_pos.skip_until(&["q", "r", "z"])); |
633 | | assert_eq!(test_pos.pos(), input.len()); |
634 | | } |
635 | | |
636 | | #[test] |
637 | | fn skip_until_empty_third_string() { |
638 | | // The empty string matches at the current position, so skip_until must |
639 | | // return true without advancing. Regression test for the inverted guard |
640 | | // and the wrong `b3` binding in the three-string fast path. |
641 | | let input = "xyb"; |
642 | | let pos = Position::from_start(input); |
643 | | |
644 | | let mut test_pos = pos; |
645 | | assert!(test_pos.skip_until(&["a", "b", ""])); |
646 | | assert_eq!(test_pos.pos(), 0); |
647 | | |
648 | | // Empty string first: still matches immediately. |
649 | | test_pos = pos; |
650 | | assert!(test_pos.skip_until(&["", "a", "b"])); |
651 | | assert_eq!(test_pos.pos(), 0); |
652 | | |
653 | | // Only the third string is empty, no "a"/"b" present: still matches at once. |
654 | | let input2 = "xyz"; |
655 | | let pos2 = Position::from_start(input2); |
656 | | let mut test_pos = pos2; |
657 | | assert!(test_pos.skip_until(&["a", "b", ""])); |
658 | | assert_eq!(test_pos.pos(), 0); |
659 | | } |
660 | | |
661 | | #[test] |
662 | | fn match_range() { |
663 | | let input = "b"; |
664 | | |
665 | | assert!(Position::new(input, 0).unwrap().match_range('a'..'c')); |
666 | | assert!(Position::new(input, 0).unwrap().match_range('b'..'b')); |
667 | | assert!(!Position::new(input, 0).unwrap().match_range('a'..'a')); |
668 | | assert!(!Position::new(input, 0).unwrap().match_range('c'..'c')); |
669 | | assert!(Position::new(input, 0).unwrap().match_range('a'..'嗨')); |
670 | | } |
671 | | |
672 | | #[test] |
673 | | fn match_insensitive() { |
674 | | let input = "AsdASdF"; |
675 | | |
676 | | assert!(Position::new(input, 0).unwrap().match_insensitive("asd")); |
677 | | assert!(Position::new(input, 3).unwrap().match_insensitive("asdf")); |
678 | | } |
679 | | |
680 | | #[test] |
681 | | fn cmp() { |
682 | | let input = "a"; |
683 | | let start = Position::from_start(input); |
684 | | let mut end = start; |
685 | | |
686 | | assert!(end.skip(1)); |
687 | | let result = start.cmp(&end); |
688 | | |
689 | | assert_eq!(result, Ordering::Less); |
690 | | } |
691 | | |
692 | | #[test] |
693 | | #[should_panic] |
694 | | fn cmp_panic() { |
695 | | let input1 = "a"; |
696 | | let input2 = "b"; |
697 | | let pos1 = Position::from_start(input1); |
698 | | let pos2 = Position::from_start(input2); |
699 | | |
700 | | let _ = pos1.cmp(&pos2); |
701 | | } |
702 | | |
703 | | #[test] |
704 | | #[cfg(feature = "std")] |
705 | | fn hash() { |
706 | | use std::collections::HashSet; |
707 | | |
708 | | let input = "a"; |
709 | | let start = Position::from_start(input); |
710 | | let mut positions = HashSet::new(); |
711 | | |
712 | | positions.insert(start); |
713 | | } |
714 | | } |