1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//! A character cursor over the source with line/column tracking.
//!
//! SCSS's lexical grammar is context-sensitive (a `:` can begin a
//! declaration value or a pseudo-class selector), so the parser scans
//! characters directly rather than over a fixed token stream. This
//! cursor is the shared primitive it builds on.
/// A 1-based source position.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pos {
/// 1-based line.
pub line: usize,
/// 1-based column.
pub col: usize,
}
/// An immutable snapshot of the cursor, used for backtracking lookahead.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Mark {
pos: usize,
line: usize,
col: usize,
}
pub(crate) struct Scanner {
chars: Vec<char>,
pos: usize,
line: usize,
col: usize,
}
impl Scanner {
pub(crate) fn new(src: &str) -> Self {
// A leading UTF-8 byte-order mark is part of the encoding, not the
// document; dart-sass strips it before parsing so it never reaches the
// output (and never triggers a spurious `@charset`).
let src = src.strip_prefix('\u{FEFF}').unwrap_or(src);
Scanner {
chars: src.chars().collect(),
pos: 0,
line: 1,
col: 1,
}
}
pub(crate) fn position(&self) -> Pos {
Pos {
line: self.line,
col: self.col,
}
}
pub(crate) fn mark(&self) -> Mark {
Mark {
pos: self.pos,
line: self.line,
col: self.col,
}
}
/// The number of source **bytes** (UTF-8) consumed since `m` was taken.
/// Used to size a diagnostic span's caret underline (dart-sass measures
/// span lengths in bytes).
pub(crate) fn byte_len_from(&self, m: Mark) -> usize {
let start = m.pos.min(self.chars.len());
let end = self.pos.min(self.chars.len());
self.chars[start..end].iter().map(|c| c.len_utf8()).sum()
}
pub(crate) fn reset(&mut self, m: Mark) {
self.pos = m.pos;
self.line = m.line;
self.col = m.col;
}
pub(crate) fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
pub(crate) fn peek_at(&self, offset: usize) -> Option<char> {
self.chars.get(self.pos + offset).copied()
}
/// The remaining unconsumed characters, for non-consuming lookahead.
pub(crate) fn rest(&self) -> &[char] {
let start = self.pos.min(self.chars.len());
&self.chars[start..]
}
pub(crate) fn bump(&mut self) -> Option<char> {
let c = self.chars.get(self.pos).copied();
if let Some(ch) = c {
self.pos += 1;
if ch == '\n' {
self.line += 1;
self.col = 1;
} else {
self.col += 1;
}
}
c
}
/// The source text consumed since `m` was taken (for verbatim capture,
/// e.g. a plain CSS `@import` URL emitted unchanged).
pub(crate) fn slice_from(&self, m: Mark) -> String {
let start = m.pos.min(self.chars.len());
let end = self.pos.min(self.chars.len());
self.chars[start..end].iter().collect()
}
/// Consume `c` if it is next; report whether it was consumed.
pub(crate) fn eat(&mut self, c: char) -> bool {
if self.peek() == Some(c) {
self.bump();
true
} else {
false
}
}
}