]>
Commit | Line | Data |
---|---|---|
8faf50e0 XL |
1 | // MIT License |
2 | // | |
3 | // Copyright (c) 2018 Guillaume Gomez | |
4 | // | |
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy | |
6 | // of this software and associated documentation files (the "Software"), to deal | |
7 | // in the Software without restriction, including without limitation the rights | |
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
9 | // copies of the Software, and to permit persons to whom the Software is | |
10 | // furnished to do so, subject to the following conditions: | |
11 | // | |
12 | // The above copyright notice and this permission notice shall be included in all | |
13 | // copies or substantial portions of the Software. | |
14 | // | |
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
21 | // SOFTWARE. | |
22 | ||
6a06907d | 23 | use std::convert::TryFrom; |
8faf50e0 XL |
24 | use std::fmt; |
25 | use std::iter::Peekable; | |
26 | use std::str::CharIndices; | |
27 | ||
8faf50e0 XL |
28 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] |
29 | pub enum ReservedChar { | |
30 | Comma, | |
31 | SuperiorThan, | |
32 | OpenParenthese, | |
33 | CloseParenthese, | |
34 | OpenCurlyBrace, | |
35 | CloseCurlyBrace, | |
36 | OpenBracket, | |
37 | CloseBracket, | |
38 | Colon, | |
39 | SemiColon, | |
40 | Slash, | |
41 | Plus, | |
42 | EqualSign, | |
43 | Space, | |
44 | Tab, | |
45 | Backline, | |
46 | Star, | |
47 | Quote, | |
48 | DoubleQuote, | |
49 | Pipe, | |
50 | Tilde, | |
51 | Dollar, | |
52 | Circumflex, | |
53 | } | |
54 | ||
55 | impl fmt::Display for ReservedChar { | |
56 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
6a06907d XL |
57 | write!( |
58 | f, | |
59 | "{}", | |
60 | match *self { | |
61 | ReservedChar::Comma => ',', | |
62 | ReservedChar::OpenParenthese => '(', | |
63 | ReservedChar::CloseParenthese => ')', | |
64 | ReservedChar::OpenCurlyBrace => '{', | |
65 | ReservedChar::CloseCurlyBrace => '}', | |
66 | ReservedChar::OpenBracket => '[', | |
67 | ReservedChar::CloseBracket => ']', | |
68 | ReservedChar::Colon => ':', | |
69 | ReservedChar::SemiColon => ';', | |
70 | ReservedChar::Slash => '/', | |
71 | ReservedChar::Star => '*', | |
72 | ReservedChar::Plus => '+', | |
73 | ReservedChar::EqualSign => '=', | |
74 | ReservedChar::Space => ' ', | |
75 | ReservedChar::Tab => '\t', | |
76 | ReservedChar::Backline => '\n', | |
77 | ReservedChar::SuperiorThan => '>', | |
78 | ReservedChar::Quote => '\'', | |
79 | ReservedChar::DoubleQuote => '"', | |
80 | ReservedChar::Pipe => '|', | |
81 | ReservedChar::Tilde => '~', | |
82 | ReservedChar::Dollar => '$', | |
83 | ReservedChar::Circumflex => '^', | |
84 | } | |
85 | ) | |
8faf50e0 XL |
86 | } |
87 | } | |
88 | ||
6a06907d | 89 | impl TryFrom<char> for ReservedChar { |
8faf50e0 XL |
90 | type Error = &'static str; |
91 | ||
92 | fn try_from(value: char) -> Result<ReservedChar, Self::Error> { | |
93 | match value { | |
94 | '\'' => Ok(ReservedChar::Quote), | |
6a06907d XL |
95 | '"' => Ok(ReservedChar::DoubleQuote), |
96 | ',' => Ok(ReservedChar::Comma), | |
97 | '(' => Ok(ReservedChar::OpenParenthese), | |
98 | ')' => Ok(ReservedChar::CloseParenthese), | |
99 | '{' => Ok(ReservedChar::OpenCurlyBrace), | |
100 | '}' => Ok(ReservedChar::CloseCurlyBrace), | |
101 | '[' => Ok(ReservedChar::OpenBracket), | |
102 | ']' => Ok(ReservedChar::CloseBracket), | |
103 | ':' => Ok(ReservedChar::Colon), | |
104 | ';' => Ok(ReservedChar::SemiColon), | |
105 | '/' => Ok(ReservedChar::Slash), | |
106 | '*' => Ok(ReservedChar::Star), | |
107 | '+' => Ok(ReservedChar::Plus), | |
108 | '=' => Ok(ReservedChar::EqualSign), | |
109 | ' ' => Ok(ReservedChar::Space), | |
8faf50e0 | 110 | '\t' => Ok(ReservedChar::Tab), |
6a06907d XL |
111 | '\n' | '\r' => Ok(ReservedChar::Backline), |
112 | '>' => Ok(ReservedChar::SuperiorThan), | |
113 | '|' => Ok(ReservedChar::Pipe), | |
114 | '~' => Ok(ReservedChar::Tilde), | |
115 | '$' => Ok(ReservedChar::Dollar), | |
116 | '^' => Ok(ReservedChar::Circumflex), | |
117 | _ => Err("Unknown reserved char"), | |
8faf50e0 XL |
118 | } |
119 | } | |
120 | } | |
121 | ||
122 | impl ReservedChar { | |
123 | fn is_useless(&self) -> bool { | |
6a06907d XL |
124 | *self == ReservedChar::Space |
125 | || *self == ReservedChar::Tab | |
126 | || *self == ReservedChar::Backline | |
8faf50e0 | 127 | } |
0bf4aa26 XL |
128 | |
129 | fn is_operator(&self) -> bool { | |
130 | Operator::try_from(*self).is_ok() | |
131 | } | |
132 | } | |
133 | ||
134 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] | |
135 | pub enum Operator { | |
136 | Plus, | |
137 | Multiply, | |
138 | Minus, | |
139 | Modulo, | |
140 | Divide, | |
141 | } | |
142 | ||
143 | impl fmt::Display for Operator { | |
144 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
6a06907d XL |
145 | write!( |
146 | f, | |
147 | "{}", | |
148 | match *self { | |
149 | Operator::Plus => '+', | |
150 | Operator::Multiply => '*', | |
151 | Operator::Minus => '-', | |
152 | Operator::Modulo => '%', | |
153 | Operator::Divide => '/', | |
154 | } | |
155 | ) | |
0bf4aa26 XL |
156 | } |
157 | } | |
158 | ||
6a06907d | 159 | impl TryFrom<char> for Operator { |
0bf4aa26 XL |
160 | type Error = &'static str; |
161 | ||
162 | fn try_from(value: char) -> Result<Operator, Self::Error> { | |
163 | match value { | |
164 | '+' => Ok(Operator::Plus), | |
165 | '*' => Ok(Operator::Multiply), | |
166 | '-' => Ok(Operator::Minus), | |
167 | '%' => Ok(Operator::Modulo), | |
168 | '/' => Ok(Operator::Divide), | |
6a06907d | 169 | _ => Err("Unknown operator"), |
0bf4aa26 XL |
170 | } |
171 | } | |
172 | } | |
173 | ||
6a06907d | 174 | impl TryFrom<ReservedChar> for Operator { |
0bf4aa26 XL |
175 | type Error = &'static str; |
176 | ||
177 | fn try_from(value: ReservedChar) -> Result<Operator, Self::Error> { | |
178 | match value { | |
6a06907d XL |
179 | ReservedChar::Slash => Ok(Operator::Divide), |
180 | ReservedChar::Star => Ok(Operator::Multiply), | |
181 | ReservedChar::Plus => Ok(Operator::Plus), | |
182 | _ => Err("Unknown operator"), | |
0bf4aa26 XL |
183 | } |
184 | } | |
8faf50e0 XL |
185 | } |
186 | ||
187 | #[derive(Eq, PartialEq, Clone, Debug)] | |
188 | pub enum SelectorElement<'a> { | |
189 | PseudoClass(&'a str), | |
190 | Class(&'a str), | |
191 | Id(&'a str), | |
192 | Tag(&'a str), | |
193 | Media(&'a str), | |
194 | } | |
195 | ||
6a06907d | 196 | impl<'a> TryFrom<&'a str> for SelectorElement<'a> { |
8faf50e0 XL |
197 | type Error = &'static str; |
198 | ||
199 | fn try_from(value: &'a str) -> Result<SelectorElement, Self::Error> { | |
6a06907d XL |
200 | if let Some(value) = value.strip_prefix('.') { |
201 | if value.is_empty() { | |
8faf50e0 | 202 | Err("cannot determine selector") |
8faf50e0 | 203 | } else { |
6a06907d | 204 | Ok(SelectorElement::Class(value)) |
8faf50e0 | 205 | } |
6a06907d XL |
206 | } else if let Some(value) = value.strip_prefix('#') { |
207 | if value.is_empty() { | |
8faf50e0 | 208 | Err("cannot determine selector") |
6a06907d XL |
209 | } else { |
210 | Ok(SelectorElement::Id(value)) | |
8faf50e0 | 211 | } |
6a06907d XL |
212 | } else if let Some(value) = value.strip_prefix('@') { |
213 | if value.is_empty() { | |
214 | Err("cannot determine selector") | |
8faf50e0 | 215 | } else { |
6a06907d XL |
216 | Ok(SelectorElement::Media(value)) |
217 | } | |
218 | } else if let Some(value) = value.strip_prefix(':') { | |
219 | if value.is_empty() { | |
8faf50e0 | 220 | Err("cannot determine selector") |
6a06907d XL |
221 | } else { |
222 | Ok(SelectorElement::PseudoClass(value)) | |
8faf50e0 XL |
223 | } |
224 | } else if value.chars().next().unwrap_or(' ').is_alphabetic() { | |
225 | Ok(SelectorElement::Tag(value)) | |
226 | } else { | |
227 | Err("unknown selector") | |
228 | } | |
229 | } | |
230 | } | |
231 | ||
232 | impl<'a> fmt::Display for SelectorElement<'a> { | |
233 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
234 | match *self { | |
235 | SelectorElement::Class(c) => write!(f, ".{}", c), | |
236 | SelectorElement::Id(i) => write!(f, "#{}", i), | |
237 | SelectorElement::Tag(t) => write!(f, "{}", t), | |
238 | SelectorElement::Media(m) => write!(f, "@{} ", m), | |
239 | SelectorElement::PseudoClass(pc) => write!(f, ":{}", pc), | |
240 | } | |
241 | } | |
242 | } | |
243 | ||
244 | #[derive(Eq, PartialEq, Clone, Debug, Copy)] | |
245 | pub enum SelectorOperator { | |
246 | /// `~=` | |
247 | OneAttributeEquals, | |
248 | /// `|=` | |
249 | EqualsOrStartsWithFollowedByDash, | |
250 | /// `$=` | |
251 | EndsWith, | |
252 | /// `^=` | |
253 | FirstStartsWith, | |
254 | /// `*=` | |
255 | Contains, | |
256 | } | |
257 | ||
258 | impl fmt::Display for SelectorOperator { | |
259 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
260 | match *self { | |
6a06907d XL |
261 | SelectorOperator::OneAttributeEquals => write!(f, "~="), |
262 | SelectorOperator::EqualsOrStartsWithFollowedByDash => write!(f, "|="), | |
263 | SelectorOperator::EndsWith => write!(f, "$="), | |
264 | SelectorOperator::FirstStartsWith => write!(f, "^="), | |
265 | SelectorOperator::Contains => write!(f, "*="), | |
8faf50e0 XL |
266 | } |
267 | } | |
268 | } | |
269 | ||
270 | #[derive(Eq, PartialEq, Clone, Debug)] | |
271 | pub enum Token<'a> { | |
272 | /// Comment. | |
273 | Comment(&'a str), | |
274 | /// Comment starting with `/**`. | |
275 | License(&'a str), | |
276 | Char(ReservedChar), | |
277 | Other(&'a str), | |
278 | SelectorElement(SelectorElement<'a>), | |
279 | String(&'a str), | |
280 | SelectorOperator(SelectorOperator), | |
0bf4aa26 | 281 | Operator(Operator), |
8faf50e0 XL |
282 | } |
283 | ||
284 | impl<'a> fmt::Display for Token<'a> { | |
285 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
286 | match *self { | |
287 | // Token::AtRule(at_rule) => write!(f, "{}", at_rule, content), | |
288 | // Token::ElementRule(selectors) => write!(f, "{}", x), | |
289 | Token::Comment(c) => write!(f, "{}", c), | |
290 | Token::License(l) => writeln!(f, "/*!{}*/", l), | |
291 | Token::Char(c) => write!(f, "{}", c), | |
292 | Token::Other(s) => write!(f, "{}", s), | |
293 | Token::SelectorElement(ref se) => write!(f, "{}", se), | |
294 | Token::String(s) => write!(f, "{}", s), | |
295 | Token::SelectorOperator(so) => write!(f, "{}", so), | |
0bf4aa26 | 296 | Token::Operator(op) => write!(f, "{}", op), |
8faf50e0 XL |
297 | } |
298 | } | |
299 | } | |
300 | ||
301 | impl<'a> Token<'a> { | |
302 | fn is_comment(&self) -> bool { | |
6a06907d | 303 | matches!(*self, Token::Comment(_)) |
8faf50e0 XL |
304 | } |
305 | ||
306 | fn is_char(&self) -> bool { | |
6a06907d | 307 | matches!(*self, Token::Char(_)) |
8faf50e0 XL |
308 | } |
309 | ||
310 | fn get_char(&self) -> Option<ReservedChar> { | |
311 | match *self { | |
312 | Token::Char(c) => Some(c), | |
313 | _ => None, | |
314 | } | |
315 | } | |
316 | ||
317 | fn is_useless(&self) -> bool { | |
318 | match *self { | |
319 | Token::Char(c) => c.is_useless(), | |
320 | _ => false, | |
321 | } | |
322 | } | |
323 | ||
324 | fn is_media(&self, media: &str) -> bool { | |
325 | match *self { | |
326 | Token::SelectorElement(SelectorElement::Media(s)) => s == media, | |
327 | _ => false, | |
328 | } | |
329 | } | |
330 | ||
331 | fn is_a_media(&self) -> bool { | |
6a06907d | 332 | matches!(*self, Token::SelectorElement(SelectorElement::Media(_))) |
8faf50e0 XL |
333 | } |
334 | ||
335 | fn is_a_license(&self) -> bool { | |
6a06907d | 336 | matches!(*self, Token::License(_)) |
8faf50e0 | 337 | } |
0bf4aa26 XL |
338 | |
339 | fn is_operator(&self) -> bool { | |
340 | match *self { | |
341 | Token::Operator(_) => true, | |
342 | Token::Char(c) => c.is_operator(), | |
343 | _ => false, | |
344 | } | |
345 | } | |
8faf50e0 XL |
346 | } |
347 | ||
348 | impl<'a> PartialEq<ReservedChar> for Token<'a> { | |
349 | fn eq(&self, other: &ReservedChar) -> bool { | |
350 | match *self { | |
351 | Token::Char(c) => c == *other, | |
352 | _ => false, | |
353 | } | |
354 | } | |
355 | } | |
356 | ||
6a06907d XL |
357 | fn get_line_comment<'a>( |
358 | source: &'a str, | |
359 | iterator: &mut Peekable<CharIndices>, | |
360 | start_pos: &mut usize, | |
361 | ) -> Option<Token<'a>> { | |
8faf50e0 | 362 | *start_pos += 1; |
6a06907d | 363 | for (pos, c) in iterator { |
8faf50e0 XL |
364 | if let Ok(c) = ReservedChar::try_from(c) { |
365 | if c == ReservedChar::Backline { | |
366 | let ret = Some(Token::Comment(&source[*start_pos..pos])); | |
367 | *start_pos = pos; | |
368 | return ret; | |
369 | } | |
370 | } | |
371 | } | |
372 | None | |
373 | } | |
374 | ||
6a06907d XL |
375 | fn get_comment<'a>( |
376 | source: &'a str, | |
377 | iterator: &mut Peekable<CharIndices>, | |
378 | start_pos: &mut usize, | |
379 | ) -> Option<Token<'a>> { | |
8faf50e0 XL |
380 | let mut prev = ReservedChar::Quote; |
381 | *start_pos += 1; | |
382 | let builder = if let Some((_, c)) = iterator.next() { | |
383 | if c == '!' || c == '*' { | |
384 | *start_pos += 1; | |
385 | Token::License | |
386 | } else { | |
387 | if let Ok(c) = ReservedChar::try_from(c) { | |
388 | prev = c; | |
389 | } | |
390 | Token::Comment | |
391 | } | |
392 | } else { | |
393 | Token::Comment | |
394 | }; | |
395 | ||
6a06907d | 396 | for (pos, c) in iterator { |
8faf50e0 XL |
397 | if let Ok(c) = ReservedChar::try_from(c) { |
398 | if c == ReservedChar::Slash && prev == ReservedChar::Star { | |
399 | let ret = Some(builder(&source[*start_pos..pos - 1])); | |
400 | *start_pos = pos; | |
401 | return ret; | |
402 | } | |
403 | prev = c; | |
404 | } else { | |
405 | prev = ReservedChar::Space; | |
406 | } | |
407 | } | |
408 | None | |
409 | } | |
410 | ||
6a06907d XL |
411 | fn get_string<'a>( |
412 | source: &'a str, | |
413 | iterator: &mut Peekable<CharIndices>, | |
414 | start_pos: &mut usize, | |
415 | start: ReservedChar, | |
416 | ) -> Option<Token<'a>> { | |
8faf50e0 XL |
417 | while let Some((pos, c)) = iterator.next() { |
418 | if c == '\\' { | |
419 | // we skip next character | |
420 | iterator.next(); | |
6a06907d | 421 | continue; |
8faf50e0 XL |
422 | } |
423 | if let Ok(c) = ReservedChar::try_from(c) { | |
424 | if c == start { | |
425 | let ret = Some(Token::String(&source[*start_pos..pos + 1])); | |
426 | *start_pos = pos; | |
427 | return ret; | |
428 | } | |
429 | } | |
430 | } | |
431 | None | |
432 | } | |
433 | ||
6a06907d XL |
434 | fn fill_other<'a>( |
435 | source: &'a str, | |
436 | v: &mut Vec<Token<'a>>, | |
437 | start: usize, | |
438 | pos: usize, | |
439 | is_in_block: isize, | |
440 | is_in_media: bool, | |
441 | is_in_attribute_selector: bool, | |
442 | ) { | |
8faf50e0 | 443 | if start < pos { |
6a06907d XL |
444 | if !is_in_attribute_selector |
445 | && ((is_in_block == 0 && !is_in_media) || (is_in_media && is_in_block == 1)) | |
446 | { | |
8faf50e0 XL |
447 | let mut is_pseudo_class = false; |
448 | let mut add = 0; | |
449 | if let Some(&Token::Char(ReservedChar::Colon)) = v.last() { | |
450 | is_pseudo_class = true; | |
451 | add = 1; | |
452 | } | |
453 | if let Ok(s) = SelectorElement::try_from(&source[start - add..pos]) { | |
454 | if is_pseudo_class { | |
455 | v.pop(); | |
456 | } | |
457 | v.push(Token::SelectorElement(s)); | |
458 | } else { | |
459 | let s = &source[start..pos]; | |
6a06907d XL |
460 | if !s.starts_with(':') |
461 | && !s.starts_with('.') | |
462 | && !s.starts_with('#') | |
463 | && !s.starts_with('@') | |
464 | { | |
b7449926 | 465 | v.push(Token::Other(s)); |
6a06907d | 466 | } |
8faf50e0 XL |
467 | } |
468 | } else { | |
469 | v.push(Token::Other(&source[start..pos])); | |
470 | } | |
471 | } | |
472 | } | |
473 | ||
6a06907d | 474 | #[allow(clippy::comparison_chain)] |
8faf50e0 XL |
475 | pub fn tokenize<'a>(source: &'a str) -> Result<Tokens<'a>, &'static str> { |
476 | let mut v = Vec::with_capacity(1000); | |
477 | let mut iterator = source.char_indices().peekable(); | |
478 | let mut start = 0; | |
479 | let mut is_in_block: isize = 0; | |
480 | let mut is_in_media = false; | |
481 | let mut is_in_attribute_selector = false; | |
482 | ||
483 | loop { | |
484 | let (mut pos, c) = match iterator.next() { | |
485 | Some(x) => x, | |
486 | None => { | |
6a06907d XL |
487 | fill_other( |
488 | source, | |
489 | &mut v, | |
490 | start, | |
491 | source.len(), | |
492 | is_in_block, | |
493 | is_in_media, | |
494 | is_in_attribute_selector, | |
495 | ); | |
496 | break; | |
8faf50e0 XL |
497 | } |
498 | }; | |
499 | if let Ok(c) = ReservedChar::try_from(c) { | |
6a06907d XL |
500 | fill_other( |
501 | source, | |
502 | &mut v, | |
503 | start, | |
504 | pos, | |
505 | is_in_block, | |
506 | is_in_media, | |
507 | is_in_attribute_selector, | |
508 | ); | |
509 | is_in_media = is_in_media | |
510 | || v.last() | |
511 | .unwrap_or(&Token::Char(ReservedChar::Space)) | |
512 | .is_media("media"); | |
b7449926 XL |
513 | if_match! { |
514 | c == ReservedChar::Quote || c == ReservedChar::DoubleQuote => { | |
515 | if let Some(s) = get_string(source, &mut iterator, &mut pos, c) { | |
516 | v.push(s); | |
8faf50e0 | 517 | } |
b7449926 XL |
518 | }, |
519 | c == ReservedChar::Star && | |
520 | *v.last().unwrap_or(&Token::Char(ReservedChar::Space)) == ReservedChar::Slash => { | |
521 | v.pop(); | |
522 | if let Some(s) = get_comment(source, &mut iterator, &mut pos) { | |
523 | v.push(s); | |
524 | } | |
525 | }, | |
526 | c == ReservedChar::Slash && | |
527 | *v.last().unwrap_or(&Token::Char(ReservedChar::Space)) == ReservedChar::Slash => { | |
528 | v.pop(); | |
529 | if let Some(s) = get_line_comment(source, &mut iterator, &mut pos) { | |
530 | v.push(s); | |
531 | } | |
532 | }, | |
533 | c == ReservedChar::OpenBracket => { | |
534 | if is_in_attribute_selector { | |
535 | return Err("Already in attribute selector"); | |
536 | } | |
537 | is_in_attribute_selector = true; | |
538 | v.push(Token::Char(c)); | |
539 | }, | |
540 | c == ReservedChar::CloseBracket => { | |
541 | if !is_in_attribute_selector { | |
542 | return Err("Unexpected ']'"); | |
543 | } | |
544 | is_in_attribute_selector = false; | |
545 | v.push(Token::Char(c)); | |
546 | }, | |
547 | c == ReservedChar::OpenCurlyBrace => { | |
548 | is_in_block += 1; | |
549 | v.push(Token::Char(c)); | |
550 | }, | |
551 | c == ReservedChar::CloseCurlyBrace => { | |
552 | is_in_block -= 1; | |
553 | if is_in_block < 0 { | |
554 | return Err("Too much '}'"); | |
555 | } else if is_in_block == 0 { | |
556 | is_in_media = false; | |
557 | } | |
558 | v.push(Token::Char(c)); | |
559 | }, | |
560 | c == ReservedChar::EqualSign => { | |
561 | match match v.last() | |
562 | .unwrap_or(&Token::Char(ReservedChar::Space)) | |
563 | .get_char() | |
564 | .unwrap_or(ReservedChar::Space) { | |
565 | ReservedChar::Tilde => Some(SelectorOperator::OneAttributeEquals), | |
566 | ReservedChar::Pipe => Some(SelectorOperator::EqualsOrStartsWithFollowedByDash), | |
567 | ReservedChar::Dollar => Some(SelectorOperator::EndsWith), | |
568 | ReservedChar::Circumflex => Some(SelectorOperator::FirstStartsWith), | |
569 | ReservedChar::Star => Some(SelectorOperator::Contains), | |
570 | _ => None, | |
571 | } { | |
572 | Some(r) => { | |
573 | v.pop(); | |
574 | v.push(Token::SelectorOperator(r)); | |
575 | } | |
576 | None => v.push(Token::Char(c)), | |
577 | } | |
578 | }, | |
579 | !c.is_useless() => { | |
580 | v.push(Token::Char(c)); | |
581 | }, | |
582 | !v.last().unwrap_or(&Token::Char(ReservedChar::Space)).is_useless() && | |
583 | (!v.last().unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace)).is_char() || | |
0bf4aa26 | 584 | v.last().unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace)).is_operator() || |
b7449926 | 585 | v.last().unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace)) |
6a06907d XL |
586 | .get_char() == Some(ReservedChar::CloseParenthese) || |
587 | v.last().unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace)) | |
588 | .get_char() == Some(ReservedChar::CloseBracket)) => { | |
b7449926 XL |
589 | v.push(Token::Char(ReservedChar::Space)); |
590 | }, | |
0bf4aa26 XL |
591 | let Ok(op) = Operator::try_from(c) => { |
592 | v.push(Token::Operator(op)); | |
593 | }, | |
8faf50e0 XL |
594 | } |
595 | start = pos + 1; | |
596 | } | |
597 | } | |
598 | Ok(Tokens(clean_tokens(v))) | |
599 | } | |
600 | ||
6a06907d | 601 | fn clean_tokens(mut v: Vec<Token<'_>>) -> Vec<Token<'_>> { |
8faf50e0 | 602 | let mut i = 0; |
0bf4aa26 XL |
603 | let mut is_in_calc = false; |
604 | let mut paren = 0; | |
8faf50e0 XL |
605 | |
606 | while i < v.len() { | |
0bf4aa26 XL |
607 | if v[i] == Token::Other("calc") { |
608 | is_in_calc = true; | |
6a06907d | 609 | } else if is_in_calc { |
0bf4aa26 XL |
610 | if v[i] == Token::Char(ReservedChar::CloseParenthese) { |
611 | paren -= 1; | |
612 | is_in_calc = paren != 0; | |
613 | } else if v[i] == Token::Char(ReservedChar::OpenParenthese) { | |
614 | paren += 1; | |
615 | } | |
616 | } | |
6a06907d | 617 | |
8faf50e0 | 618 | if v[i].is_useless() { |
6a06907d XL |
619 | if i > 0 && v[i - 1] == Token::Char(ReservedChar::CloseBracket) { |
620 | if i + 1 < v.len() | |
621 | && (v[i + 1].is_useless() | |
622 | || v[i + 1] == Token::Char(ReservedChar::OpenCurlyBrace)) | |
623 | { | |
624 | v.remove(i); | |
625 | continue; | |
626 | } | |
627 | } else if i > 0 && v[i - 1] == Token::Other("and") { | |
628 | // retain the space after an and | |
629 | } else if (is_in_calc && v[i - 1].is_useless()) | |
630 | || !is_in_calc | |
631 | && ((i > 0 | |
632 | && ((v[i - 1].is_char() | |
633 | && v[i - 1] != Token::Char(ReservedChar::CloseParenthese)) | |
634 | || v[i - 1].is_a_media() | |
635 | || v[i - 1].is_a_license())) | |
636 | || (i < v.len() - 1 && v[i + 1].is_char())) | |
637 | { | |
8faf50e0 | 638 | v.remove(i); |
6a06907d | 639 | continue; |
8faf50e0 XL |
640 | } |
641 | } else if v[i].is_comment() { | |
642 | v.remove(i); | |
6a06907d | 643 | continue; |
8faf50e0 XL |
644 | } |
645 | i += 1; | |
646 | } | |
647 | v | |
648 | } | |
649 | ||
650 | #[derive(Debug, PartialEq, Eq, Clone)] | |
651 | pub struct Tokens<'a>(pub Vec<Token<'a>>); | |
652 | ||
653 | impl<'a> fmt::Display for Tokens<'a> { | |
654 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
655 | for token in self.0.iter() { | |
656 | write!(f, "{}", token)?; | |
657 | } | |
658 | Ok(()) | |
659 | } | |
660 | } | |
661 | ||
662 | #[test] | |
663 | fn css_basic() { | |
664 | let s = r#" | |
665 | /*! just some license */ | |
666 | .foo > #bar p:hover { | |
667 | color: blue; | |
668 | background: "blue"; | |
669 | } | |
670 | ||
671 | // a comment! | |
672 | @media screen and (max-width: 640px) { | |
673 | .block:hover { | |
674 | display: block; | |
675 | } | |
676 | }"#; | |
6a06907d XL |
677 | let expected = vec![ |
678 | Token::License(" just some license "), | |
679 | Token::SelectorElement(SelectorElement::Class("foo")), | |
680 | Token::Char(ReservedChar::SuperiorThan), | |
681 | Token::SelectorElement(SelectorElement::Id("bar")), | |
682 | Token::Char(ReservedChar::Space), | |
683 | Token::SelectorElement(SelectorElement::Tag("p")), | |
684 | Token::SelectorElement(SelectorElement::PseudoClass("hover")), | |
685 | Token::Char(ReservedChar::OpenCurlyBrace), | |
686 | Token::Other("color"), | |
687 | Token::Char(ReservedChar::Colon), | |
688 | Token::Other("blue"), | |
689 | Token::Char(ReservedChar::SemiColon), | |
690 | Token::Other("background"), | |
691 | Token::Char(ReservedChar::Colon), | |
692 | Token::String("\"blue\""), | |
693 | Token::Char(ReservedChar::SemiColon), | |
694 | Token::Char(ReservedChar::CloseCurlyBrace), | |
695 | Token::SelectorElement(SelectorElement::Media("media")), | |
696 | Token::Other("screen"), | |
697 | Token::Char(ReservedChar::Space), | |
698 | Token::Other("and"), | |
699 | Token::Char(ReservedChar::Space), | |
700 | Token::Char(ReservedChar::OpenParenthese), | |
701 | Token::Other("max-width"), | |
702 | Token::Char(ReservedChar::Colon), | |
703 | Token::Other("640px"), | |
704 | Token::Char(ReservedChar::CloseParenthese), | |
705 | Token::Char(ReservedChar::OpenCurlyBrace), | |
706 | Token::SelectorElement(SelectorElement::Class("block")), | |
707 | Token::SelectorElement(SelectorElement::PseudoClass("hover")), | |
708 | Token::Char(ReservedChar::OpenCurlyBrace), | |
709 | Token::Other("display"), | |
710 | Token::Char(ReservedChar::Colon), | |
711 | Token::Other("block"), | |
712 | Token::Char(ReservedChar::SemiColon), | |
713 | Token::Char(ReservedChar::CloseCurlyBrace), | |
714 | Token::Char(ReservedChar::CloseCurlyBrace), | |
715 | ]; | |
8faf50e0 XL |
716 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
717 | } | |
718 | ||
719 | #[test] | |
720 | fn elem_selector() { | |
721 | let s = r#" | |
722 | /** just some license */ | |
723 | a[href*="example"] { | |
724 | background: yellow; | |
725 | } | |
726 | a[href$=".org"] { | |
727 | font-style: italic; | |
728 | } | |
729 | span[lang|="zh"] { | |
730 | color: red; | |
731 | } | |
732 | a[href^="/"] { | |
733 | background-color: gold; | |
734 | } | |
735 | div[value~="test"] { | |
736 | border-width: 1px; | |
737 | } | |
738 | span[lang="pt"] { | |
739 | font-size: 12em; // I love big fonts | |
740 | } | |
741 | "#; | |
6a06907d XL |
742 | let expected = vec![ |
743 | Token::License(" just some license "), | |
744 | Token::SelectorElement(SelectorElement::Tag("a")), | |
745 | Token::Char(ReservedChar::OpenBracket), | |
746 | Token::Other("href"), | |
747 | Token::SelectorOperator(SelectorOperator::Contains), | |
748 | Token::String("\"example\""), | |
749 | Token::Char(ReservedChar::CloseBracket), | |
750 | Token::Char(ReservedChar::OpenCurlyBrace), | |
751 | Token::Other("background"), | |
752 | Token::Char(ReservedChar::Colon), | |
753 | Token::Other("yellow"), | |
754 | Token::Char(ReservedChar::SemiColon), | |
755 | Token::Char(ReservedChar::CloseCurlyBrace), | |
756 | Token::SelectorElement(SelectorElement::Tag("a")), | |
757 | Token::Char(ReservedChar::OpenBracket), | |
758 | Token::Other("href"), | |
759 | Token::SelectorOperator(SelectorOperator::EndsWith), | |
760 | Token::String("\".org\""), | |
761 | Token::Char(ReservedChar::CloseBracket), | |
762 | Token::Char(ReservedChar::OpenCurlyBrace), | |
763 | Token::Other("font-style"), | |
764 | Token::Char(ReservedChar::Colon), | |
765 | Token::Other("italic"), | |
766 | Token::Char(ReservedChar::SemiColon), | |
767 | Token::Char(ReservedChar::CloseCurlyBrace), | |
768 | Token::SelectorElement(SelectorElement::Tag("span")), | |
769 | Token::Char(ReservedChar::OpenBracket), | |
770 | Token::Other("lang"), | |
771 | Token::SelectorOperator(SelectorOperator::EqualsOrStartsWithFollowedByDash), | |
772 | Token::String("\"zh\""), | |
773 | Token::Char(ReservedChar::CloseBracket), | |
774 | Token::Char(ReservedChar::OpenCurlyBrace), | |
775 | Token::Other("color"), | |
776 | Token::Char(ReservedChar::Colon), | |
777 | Token::Other("red"), | |
778 | Token::Char(ReservedChar::SemiColon), | |
779 | Token::Char(ReservedChar::CloseCurlyBrace), | |
780 | Token::SelectorElement(SelectorElement::Tag("a")), | |
781 | Token::Char(ReservedChar::OpenBracket), | |
782 | Token::Other("href"), | |
783 | Token::SelectorOperator(SelectorOperator::FirstStartsWith), | |
784 | Token::String("\"/\""), | |
785 | Token::Char(ReservedChar::CloseBracket), | |
786 | Token::Char(ReservedChar::OpenCurlyBrace), | |
787 | Token::Other("background-color"), | |
788 | Token::Char(ReservedChar::Colon), | |
789 | Token::Other("gold"), | |
790 | Token::Char(ReservedChar::SemiColon), | |
791 | Token::Char(ReservedChar::CloseCurlyBrace), | |
792 | Token::SelectorElement(SelectorElement::Tag("div")), | |
793 | Token::Char(ReservedChar::OpenBracket), | |
794 | Token::Other("value"), | |
795 | Token::SelectorOperator(SelectorOperator::OneAttributeEquals), | |
796 | Token::String("\"test\""), | |
797 | Token::Char(ReservedChar::CloseBracket), | |
798 | Token::Char(ReservedChar::OpenCurlyBrace), | |
799 | Token::Other("border-width"), | |
800 | Token::Char(ReservedChar::Colon), | |
801 | Token::Other("1px"), | |
802 | Token::Char(ReservedChar::SemiColon), | |
803 | Token::Char(ReservedChar::CloseCurlyBrace), | |
804 | Token::SelectorElement(SelectorElement::Tag("span")), | |
805 | Token::Char(ReservedChar::OpenBracket), | |
806 | Token::Other("lang"), | |
807 | Token::Char(ReservedChar::EqualSign), | |
808 | Token::String("\"pt\""), | |
809 | Token::Char(ReservedChar::CloseBracket), | |
810 | Token::Char(ReservedChar::OpenCurlyBrace), | |
811 | Token::Other("font-size"), | |
812 | Token::Char(ReservedChar::Colon), | |
813 | Token::Other("12em"), | |
814 | Token::Char(ReservedChar::SemiColon), | |
815 | Token::Char(ReservedChar::CloseCurlyBrace), | |
816 | ]; | |
8faf50e0 XL |
817 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
818 | } | |
819 | ||
820 | #[test] | |
821 | fn check_media() { | |
822 | let s = "@media (max-width: 700px) { color: red; }"; | |
823 | ||
6a06907d XL |
824 | let expected = vec![ |
825 | Token::SelectorElement(SelectorElement::Media("media")), | |
826 | Token::Char(ReservedChar::OpenParenthese), | |
827 | Token::Other("max-width"), | |
828 | Token::Char(ReservedChar::Colon), | |
829 | Token::Other("700px"), | |
830 | Token::Char(ReservedChar::CloseParenthese), | |
831 | Token::Char(ReservedChar::OpenCurlyBrace), | |
832 | Token::SelectorElement(SelectorElement::Tag("color")), | |
833 | Token::Char(ReservedChar::Colon), | |
834 | Token::Other("red"), | |
835 | Token::Char(ReservedChar::SemiColon), | |
836 | Token::Char(ReservedChar::CloseCurlyBrace), | |
837 | ]; | |
b7449926 XL |
838 | |
839 | assert_eq!(tokenize(s), Ok(Tokens(expected))); | |
840 | } | |
841 | ||
842 | #[test] | |
843 | fn check_calc() { | |
844 | let s = ".foo { width: calc(100% - 34px); }"; | |
845 | ||
6a06907d XL |
846 | let expected = vec![ |
847 | Token::SelectorElement(SelectorElement::Class("foo")), | |
848 | Token::Char(ReservedChar::OpenCurlyBrace), | |
849 | Token::Other("width"), | |
850 | Token::Char(ReservedChar::Colon), | |
851 | Token::Other("calc"), | |
852 | Token::Char(ReservedChar::OpenParenthese), | |
853 | Token::Other("100%"), | |
854 | Token::Char(ReservedChar::Space), | |
855 | Token::Other("-"), | |
856 | Token::Char(ReservedChar::Space), | |
857 | Token::Other("34px"), | |
858 | Token::Char(ReservedChar::CloseParenthese), | |
859 | Token::Char(ReservedChar::SemiColon), | |
860 | Token::Char(ReservedChar::CloseCurlyBrace), | |
861 | ]; | |
8faf50e0 XL |
862 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
863 | } |