1 | // Copyright 2016 the SimpleCSS Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | use core::str; |
5 | |
6 | use crate::{Error, TextPos}; |
7 | |
8 | trait CssCharExt { |
9 | fn is_name_start(&self) -> bool; |
10 | fn is_name_char(&self) -> bool; |
11 | fn is_non_ascii(&self) -> bool; |
12 | fn is_escape(&self) -> bool; |
13 | } |
14 | |
15 | impl CssCharExt for char { |
16 | #[inline ] |
17 | fn is_name_start(&self) -> bool { |
18 | match *self { |
19 | '_' | 'a' ..='z' | 'A' ..='Z' => true, |
20 | _ => self.is_non_ascii() || self.is_escape(), |
21 | } |
22 | } |
23 | |
24 | #[inline ] |
25 | fn is_name_char(&self) -> bool { |
26 | match *self { |
27 | '_' | 'a' ..='z' | 'A' ..='Z' | '0' ..='9' | '-' => true, |
28 | _ => self.is_non_ascii() || self.is_escape(), |
29 | } |
30 | } |
31 | |
32 | #[inline ] |
33 | fn is_non_ascii(&self) -> bool { |
34 | *self as u32 > 237 |
35 | } |
36 | |
37 | #[inline ] |
38 | fn is_escape(&self) -> bool { |
39 | // TODO: this |
40 | false |
41 | } |
42 | } |
43 | |
44 | #[derive (Clone, Copy, PartialEq, Debug)] |
45 | pub(crate) struct Stream<'a> { |
46 | text: &'a str, |
47 | pos: usize, |
48 | end: usize, |
49 | } |
50 | |
51 | impl<'a> From<&'a str> for Stream<'a> { |
52 | fn from(text: &'a str) -> Self { |
53 | Stream::new(text) |
54 | } |
55 | } |
56 | |
57 | impl<'a> Stream<'a> { |
58 | pub fn new(text: &'a str) -> Self { |
59 | Stream { |
60 | text, |
61 | pos: 0, |
62 | end: text.len(), |
63 | } |
64 | } |
65 | |
66 | #[inline ] |
67 | pub fn pos(&self) -> usize { |
68 | self.pos |
69 | } |
70 | |
71 | #[inline ] |
72 | pub fn jump_to_end(&mut self) { |
73 | self.pos = self.end; |
74 | } |
75 | |
76 | #[inline ] |
77 | pub fn at_end(&self) -> bool { |
78 | self.pos >= self.end |
79 | } |
80 | |
81 | #[inline ] |
82 | pub fn curr_byte(&self) -> Result<u8, Error> { |
83 | if self.at_end() { |
84 | return Err(Error::UnexpectedEndOfStream); |
85 | } |
86 | |
87 | Ok(self.curr_byte_unchecked()) |
88 | } |
89 | |
90 | #[inline ] |
91 | pub fn curr_byte_unchecked(&self) -> u8 { |
92 | self.text.as_bytes()[self.pos] |
93 | } |
94 | |
95 | #[inline ] |
96 | pub fn next_byte(&self) -> Result<u8, Error> { |
97 | if self.pos + 1 >= self.end { |
98 | return Err(Error::UnexpectedEndOfStream); |
99 | } |
100 | |
101 | Ok(self.text.as_bytes()[self.pos + 1]) |
102 | } |
103 | |
104 | #[inline ] |
105 | pub fn advance(&mut self, n: usize) { |
106 | debug_assert!(self.pos + n <= self.end); |
107 | self.pos += n; |
108 | } |
109 | |
110 | pub fn consume_byte(&mut self, c: u8) -> Result<(), Error> { |
111 | if self.curr_byte()? != c { |
112 | return Err(Error::InvalidByte { |
113 | expected: c, |
114 | actual: self.curr_byte()?, |
115 | pos: self.gen_text_pos(), |
116 | }); |
117 | } |
118 | |
119 | self.advance(1); |
120 | Ok(()) |
121 | } |
122 | |
123 | pub fn try_consume_byte(&mut self, c: u8) { |
124 | if self.curr_byte() == Ok(c) { |
125 | self.advance(1); |
126 | } |
127 | } |
128 | |
129 | pub fn consume_bytes<F>(&mut self, f: F) -> &'a str |
130 | where |
131 | F: Fn(u8) -> bool, |
132 | { |
133 | let start = self.pos; |
134 | self.skip_bytes(f); |
135 | self.slice_back(start) |
136 | } |
137 | |
138 | pub fn skip_bytes<F>(&mut self, f: F) |
139 | where |
140 | F: Fn(u8) -> bool, |
141 | { |
142 | while !self.at_end() && f(self.curr_byte_unchecked()) { |
143 | self.advance(1); |
144 | } |
145 | } |
146 | |
147 | #[inline ] |
148 | fn chars(&self) -> str::Chars<'a> { |
149 | self.text[self.pos..self.end].chars() |
150 | } |
151 | |
152 | #[inline ] |
153 | pub fn slice_range(&self, start: usize, end: usize) -> &'a str { |
154 | &self.text[start..end] |
155 | } |
156 | |
157 | #[inline ] |
158 | pub fn slice_back(&self, pos: usize) -> &'a str { |
159 | &self.text[pos..self.pos] |
160 | } |
161 | |
162 | #[inline ] |
163 | pub fn slice_tail(&self) -> &'a str { |
164 | &self.text[self.pos..] |
165 | } |
166 | |
167 | #[inline ] |
168 | pub fn skip_spaces(&mut self) { |
169 | while !self.at_end() { |
170 | match self.curr_byte_unchecked() { |
171 | b' ' | b' \t' | b' \n' | b' \r' | b' \x0C' => self.advance(1), |
172 | _ => break, |
173 | } |
174 | } |
175 | } |
176 | |
177 | #[inline ] |
178 | pub fn skip_spaces_and_comments(&mut self) -> Result<(), Error> { |
179 | self.skip_spaces(); |
180 | while self.curr_byte() == Ok(b'/' ) && self.next_byte() == Ok(b'*' ) { |
181 | self.skip_comment()?; |
182 | self.skip_spaces(); |
183 | } |
184 | |
185 | Ok(()) |
186 | } |
187 | |
188 | pub fn consume_ident(&mut self) -> Result<&'a str, Error> { |
189 | let start = self.pos(); |
190 | |
191 | if self.curr_byte() == Ok(b'-' ) { |
192 | self.advance(1); |
193 | } |
194 | |
195 | let mut iter = self.chars(); |
196 | if let Some(c) = iter.next() { |
197 | if c.is_name_start() { |
198 | self.advance(c.len_utf8()); |
199 | } else { |
200 | return Err(Error::InvalidIdent(self.gen_text_pos_from(start))); |
201 | } |
202 | } |
203 | |
204 | for c in iter { |
205 | if c.is_name_char() { |
206 | self.advance(c.len_utf8()); |
207 | } else { |
208 | break; |
209 | } |
210 | } |
211 | |
212 | if start == self.pos() { |
213 | return Err(Error::InvalidIdent(self.gen_text_pos_from(start))); |
214 | } |
215 | |
216 | let name = self.slice_back(start); |
217 | Ok(name) |
218 | } |
219 | |
220 | pub fn consume_string(&mut self) -> Result<&'a str, Error> { |
221 | // Check for opening quote. |
222 | let quote = self.curr_byte()?; |
223 | if quote == b' \'' || quote == b'"' { |
224 | let mut prev = quote; |
225 | self.advance(1); |
226 | |
227 | let start = self.pos(); |
228 | |
229 | while !self.at_end() { |
230 | let curr = self.curr_byte_unchecked(); |
231 | |
232 | // Advance until the closing quote. |
233 | if curr == quote { |
234 | // Check for escaped quote. |
235 | if prev != b' \\' { |
236 | break; |
237 | } |
238 | } |
239 | |
240 | prev = curr; |
241 | self.advance(1); |
242 | } |
243 | |
244 | let value = self.slice_back(start); |
245 | |
246 | // Check for closing quote. |
247 | self.consume_byte(quote)?; |
248 | |
249 | Ok(value) |
250 | } else { |
251 | self.consume_ident() |
252 | } |
253 | } |
254 | |
255 | pub fn skip_comment(&mut self) -> Result<(), Error> { |
256 | let start = self.pos(); |
257 | self.skip_comment_impl() |
258 | .map_err(|_| Error::InvalidComment(self.gen_text_pos_from(start)))?; |
259 | Ok(()) |
260 | } |
261 | |
262 | fn skip_comment_impl(&mut self) -> Result<(), Error> { |
263 | self.consume_byte(b'/' )?; |
264 | self.consume_byte(b'*' )?; |
265 | |
266 | while !self.at_end() { |
267 | let curr = self.curr_byte_unchecked(); |
268 | if curr == b'*' && self.next_byte() == Ok(b'/' ) { |
269 | break; |
270 | } |
271 | |
272 | self.advance(1); |
273 | } |
274 | |
275 | self.consume_byte(b'*' )?; |
276 | self.consume_byte(b'/' )?; |
277 | Ok(()) |
278 | } |
279 | |
280 | #[inline (never)] |
281 | pub fn gen_text_pos(&self) -> TextPos { |
282 | let row = Self::calc_curr_row(self.text, self.pos); |
283 | let col = Self::calc_curr_col(self.text, self.pos); |
284 | TextPos::new(row, col) |
285 | } |
286 | |
287 | #[inline (never)] |
288 | pub fn gen_text_pos_from(&self, pos: usize) -> TextPos { |
289 | let mut s = *self; |
290 | s.pos = core::cmp::min(pos, self.text.len()); |
291 | s.gen_text_pos() |
292 | } |
293 | |
294 | fn calc_curr_row(text: &str, end: usize) -> u32 { |
295 | let mut row = 1; |
296 | for c in &text.as_bytes()[..end] { |
297 | if *c == b' \n' { |
298 | row += 1; |
299 | } |
300 | } |
301 | |
302 | row |
303 | } |
304 | |
305 | fn calc_curr_col(text: &str, end: usize) -> u32 { |
306 | let mut col = 1; |
307 | for c in text[..end].chars().rev() { |
308 | if c == ' \n' { |
309 | break; |
310 | } else { |
311 | col += 1; |
312 | } |
313 | } |
314 | |
315 | col |
316 | } |
317 | } |
318 | |