]>
Commit | Line | Data |
---|---|---|
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 | ||
23 | use std::fmt; | |
24 | use std::iter::Peekable; | |
25 | use std::str::CharIndices; | |
26 | ||
27 | pub trait MyTryFrom<T>: Sized { | |
28 | type Error; | |
29 | fn try_from(value: T) -> Result<Self, Self::Error>; | |
30 | } | |
31 | ||
32 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] | |
33 | pub enum ReservedChar { | |
34 | Comma, | |
35 | SuperiorThan, | |
36 | OpenParenthese, | |
37 | CloseParenthese, | |
38 | OpenCurlyBrace, | |
39 | CloseCurlyBrace, | |
40 | OpenBracket, | |
41 | CloseBracket, | |
42 | Colon, | |
43 | SemiColon, | |
44 | Slash, | |
45 | Plus, | |
46 | EqualSign, | |
47 | Space, | |
48 | Tab, | |
49 | Backline, | |
50 | Star, | |
51 | Quote, | |
52 | DoubleQuote, | |
53 | Pipe, | |
54 | Tilde, | |
55 | Dollar, | |
56 | Circumflex, | |
57 | } | |
58 | ||
59 | impl fmt::Display for ReservedChar { | |
60 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
61 | write!(f, "{}", | |
62 | match *self { | |
63 | ReservedChar::Comma => ',', | |
64 | ReservedChar::OpenParenthese => '(', | |
65 | ReservedChar::CloseParenthese => ')', | |
66 | ReservedChar::OpenCurlyBrace => '{', | |
67 | ReservedChar::CloseCurlyBrace => '}', | |
68 | ReservedChar::OpenBracket => '[', | |
69 | ReservedChar::CloseBracket => ']', | |
70 | ReservedChar::Colon => ':', | |
71 | ReservedChar::SemiColon => ';', | |
72 | ReservedChar::Slash => '/', | |
73 | ReservedChar::Star => '*', | |
74 | ReservedChar::Plus => '+', | |
75 | ReservedChar::EqualSign => '=', | |
76 | ReservedChar::Space => ' ', | |
77 | ReservedChar::Tab => '\t', | |
78 | ReservedChar::Backline => '\n', | |
79 | ReservedChar::SuperiorThan => '>', | |
80 | ReservedChar::Quote => '\'', | |
81 | ReservedChar::DoubleQuote => '"', | |
82 | ReservedChar::Pipe => '|', | |
83 | ReservedChar::Tilde => '~', | |
84 | ReservedChar::Dollar => '$', | |
85 | ReservedChar::Circumflex => '^', | |
86 | }) | |
87 | } | |
88 | } | |
89 | ||
90 | impl MyTryFrom<char> for ReservedChar { | |
91 | type Error = &'static str; | |
92 | ||
93 | fn try_from(value: char) -> Result<ReservedChar, Self::Error> { | |
94 | match value { | |
95 | '\'' => Ok(ReservedChar::Quote), | |
96 | '"' => Ok(ReservedChar::DoubleQuote), | |
97 | ',' => Ok(ReservedChar::Comma), | |
98 | '(' => Ok(ReservedChar::OpenParenthese), | |
99 | ')' => Ok(ReservedChar::CloseParenthese), | |
100 | '{' => Ok(ReservedChar::OpenCurlyBrace), | |
101 | '}' => Ok(ReservedChar::CloseCurlyBrace), | |
102 | '[' => Ok(ReservedChar::OpenBracket), | |
103 | ']' => Ok(ReservedChar::CloseBracket), | |
104 | ':' => Ok(ReservedChar::Colon), | |
105 | ';' => Ok(ReservedChar::SemiColon), | |
106 | '/' => Ok(ReservedChar::Slash), | |
107 | '*' => Ok(ReservedChar::Star), | |
108 | '+' => Ok(ReservedChar::Plus), | |
109 | '=' => Ok(ReservedChar::EqualSign), | |
110 | ' ' => Ok(ReservedChar::Space), | |
111 | '\t' => Ok(ReservedChar::Tab), | |
112 | '\n' | | |
113 | '\r' => Ok(ReservedChar::Backline), | |
114 | '>' => Ok(ReservedChar::SuperiorThan), | |
115 | '|' => Ok(ReservedChar::Pipe), | |
116 | '~' => Ok(ReservedChar::Tilde), | |
117 | '$' => Ok(ReservedChar::Dollar), | |
118 | '^' => Ok(ReservedChar::Circumflex), | |
119 | _ => Err("Unknown reserved char"), | |
120 | } | |
121 | } | |
122 | } | |
123 | ||
124 | impl ReservedChar { | |
125 | fn is_useless(&self) -> bool { | |
126 | *self == ReservedChar::Space || | |
127 | *self == ReservedChar::Tab || | |
128 | *self == ReservedChar::Backline | |
129 | } | |
130 | } | |
131 | ||
132 | #[derive(Eq, PartialEq, Clone, Debug)] | |
133 | pub enum SelectorElement<'a> { | |
134 | PseudoClass(&'a str), | |
135 | Class(&'a str), | |
136 | Id(&'a str), | |
137 | Tag(&'a str), | |
138 | Media(&'a str), | |
139 | } | |
140 | ||
141 | impl<'a> MyTryFrom<&'a str> for SelectorElement<'a> { | |
142 | type Error = &'static str; | |
143 | ||
144 | fn try_from(value: &'a str) -> Result<SelectorElement, Self::Error> { | |
145 | if value.starts_with('.') { | |
146 | if value.len() > 1 { | |
147 | Ok(SelectorElement::Class(&value[1..])) | |
148 | } else { | |
149 | Err("cannot determine selector") | |
150 | } | |
151 | } else if value.starts_with('#') { | |
152 | if value.len() > 1 { | |
153 | Ok(SelectorElement::Id(&value[1..])) | |
154 | } else { | |
155 | Err("cannot determine selector") | |
156 | } | |
157 | } else if value.starts_with('@') { | |
158 | if value.len() > 1 { | |
159 | Ok(SelectorElement::Media(&value[1..])) | |
160 | } else { | |
161 | Err("cannot determine selector") | |
162 | } | |
163 | } else if value.starts_with(':') { | |
164 | if value.len() > 1 { | |
165 | Ok(SelectorElement::PseudoClass(&value[1..])) | |
166 | } else { | |
167 | Err("cannot determine selector") | |
168 | } | |
169 | } else if value.chars().next().unwrap_or(' ').is_alphabetic() { | |
170 | Ok(SelectorElement::Tag(value)) | |
171 | } else { | |
172 | Err("unknown selector") | |
173 | } | |
174 | } | |
175 | } | |
176 | ||
177 | impl<'a> fmt::Display for SelectorElement<'a> { | |
178 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
179 | match *self { | |
180 | SelectorElement::Class(c) => write!(f, ".{}", c), | |
181 | SelectorElement::Id(i) => write!(f, "#{}", i), | |
182 | SelectorElement::Tag(t) => write!(f, "{}", t), | |
183 | SelectorElement::Media(m) => write!(f, "@{} ", m), | |
184 | SelectorElement::PseudoClass(pc) => write!(f, ":{}", pc), | |
185 | } | |
186 | } | |
187 | } | |
188 | ||
189 | #[derive(Eq, PartialEq, Clone, Debug, Copy)] | |
190 | pub enum SelectorOperator { | |
191 | /// `~=` | |
192 | OneAttributeEquals, | |
193 | /// `|=` | |
194 | EqualsOrStartsWithFollowedByDash, | |
195 | /// `$=` | |
196 | EndsWith, | |
197 | /// `^=` | |
198 | FirstStartsWith, | |
199 | /// `*=` | |
200 | Contains, | |
201 | } | |
202 | ||
203 | impl fmt::Display for SelectorOperator { | |
204 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
205 | match *self { | |
206 | SelectorOperator::OneAttributeEquals => write!(f, "{}", "~="), | |
207 | SelectorOperator::EqualsOrStartsWithFollowedByDash => write!(f, "{}", "|="), | |
208 | SelectorOperator::EndsWith => write!(f, "{}", "$="), | |
209 | SelectorOperator::FirstStartsWith => write!(f, "{}", "^="), | |
210 | SelectorOperator::Contains => write!(f, "{}", "*="), | |
211 | } | |
212 | } | |
213 | } | |
214 | ||
215 | #[derive(Eq, PartialEq, Clone, Debug)] | |
216 | pub enum Token<'a> { | |
217 | /// Comment. | |
218 | Comment(&'a str), | |
219 | /// Comment starting with `/**`. | |
220 | License(&'a str), | |
221 | Char(ReservedChar), | |
222 | Other(&'a str), | |
223 | SelectorElement(SelectorElement<'a>), | |
224 | String(&'a str), | |
225 | SelectorOperator(SelectorOperator), | |
226 | } | |
227 | ||
228 | impl<'a> fmt::Display for Token<'a> { | |
229 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
230 | match *self { | |
231 | // Token::AtRule(at_rule) => write!(f, "{}", at_rule, content), | |
232 | // Token::ElementRule(selectors) => write!(f, "{}", x), | |
233 | Token::Comment(c) => write!(f, "{}", c), | |
234 | Token::License(l) => writeln!(f, "/*!{}*/", l), | |
235 | Token::Char(c) => write!(f, "{}", c), | |
236 | Token::Other(s) => write!(f, "{}", s), | |
237 | Token::SelectorElement(ref se) => write!(f, "{}", se), | |
238 | Token::String(s) => write!(f, "{}", s), | |
239 | Token::SelectorOperator(so) => write!(f, "{}", so), | |
240 | } | |
241 | } | |
242 | } | |
243 | ||
244 | impl<'a> Token<'a> { | |
245 | fn is_comment(&self) -> bool { | |
246 | match *self { | |
247 | Token::Comment(_) => true, | |
248 | _ => false, | |
249 | } | |
250 | } | |
251 | ||
252 | fn is_char(&self) -> bool { | |
253 | match *self { | |
254 | Token::Char(_) => true, | |
255 | _ => false, | |
256 | } | |
257 | } | |
258 | ||
259 | fn get_char(&self) -> Option<ReservedChar> { | |
260 | match *self { | |
261 | Token::Char(c) => Some(c), | |
262 | _ => None, | |
263 | } | |
264 | } | |
265 | ||
266 | fn is_useless(&self) -> bool { | |
267 | match *self { | |
268 | Token::Char(c) => c.is_useless(), | |
269 | _ => false, | |
270 | } | |
271 | } | |
272 | ||
273 | fn is_media(&self, media: &str) -> bool { | |
274 | match *self { | |
275 | Token::SelectorElement(SelectorElement::Media(s)) => s == media, | |
276 | _ => false, | |
277 | } | |
278 | } | |
279 | ||
280 | fn is_a_media(&self) -> bool { | |
281 | match *self { | |
282 | Token::SelectorElement(SelectorElement::Media(_)) => true, | |
283 | _ => false, | |
284 | } | |
285 | } | |
286 | ||
287 | fn is_a_license(&self) -> bool { | |
288 | match *self { | |
289 | Token::License(_) => true, | |
290 | _ => false, | |
291 | } | |
292 | } | |
293 | } | |
294 | ||
295 | impl<'a> PartialEq<ReservedChar> for Token<'a> { | |
296 | fn eq(&self, other: &ReservedChar) -> bool { | |
297 | match *self { | |
298 | Token::Char(c) => c == *other, | |
299 | _ => false, | |
300 | } | |
301 | } | |
302 | } | |
303 | ||
304 | fn get_line_comment<'a>(source: &'a str, iterator: &mut Peekable<CharIndices>, | |
305 | start_pos: &mut usize) -> Option<Token<'a>> { | |
306 | *start_pos += 1; | |
307 | while let Some((pos, c)) = iterator.next() { | |
308 | if let Ok(c) = ReservedChar::try_from(c) { | |
309 | if c == ReservedChar::Backline { | |
310 | let ret = Some(Token::Comment(&source[*start_pos..pos])); | |
311 | *start_pos = pos; | |
312 | return ret; | |
313 | } | |
314 | } | |
315 | } | |
316 | None | |
317 | } | |
318 | ||
319 | fn get_comment<'a>(source: &'a str, iterator: &mut Peekable<CharIndices>, | |
320 | start_pos: &mut usize) -> Option<Token<'a>> { | |
321 | let mut prev = ReservedChar::Quote; | |
322 | *start_pos += 1; | |
323 | let builder = if let Some((_, c)) = iterator.next() { | |
324 | if c == '!' || c == '*' { | |
325 | *start_pos += 1; | |
326 | Token::License | |
327 | } else { | |
328 | if let Ok(c) = ReservedChar::try_from(c) { | |
329 | prev = c; | |
330 | } | |
331 | Token::Comment | |
332 | } | |
333 | } else { | |
334 | Token::Comment | |
335 | }; | |
336 | ||
337 | while let Some((pos, c)) = iterator.next() { | |
338 | if let Ok(c) = ReservedChar::try_from(c) { | |
339 | if c == ReservedChar::Slash && prev == ReservedChar::Star { | |
340 | let ret = Some(builder(&source[*start_pos..pos - 1])); | |
341 | *start_pos = pos; | |
342 | return ret; | |
343 | } | |
344 | prev = c; | |
345 | } else { | |
346 | prev = ReservedChar::Space; | |
347 | } | |
348 | } | |
349 | None | |
350 | } | |
351 | ||
352 | fn get_string<'a>(source: &'a str, iterator: &mut Peekable<CharIndices>, start_pos: &mut usize, | |
353 | start: ReservedChar) -> Option<Token<'a>> { | |
354 | while let Some((pos, c)) = iterator.next() { | |
355 | if c == '\\' { | |
356 | // we skip next character | |
357 | iterator.next(); | |
358 | continue | |
359 | } | |
360 | if let Ok(c) = ReservedChar::try_from(c) { | |
361 | if c == start { | |
362 | let ret = Some(Token::String(&source[*start_pos..pos + 1])); | |
363 | *start_pos = pos; | |
364 | return ret; | |
365 | } | |
366 | } | |
367 | } | |
368 | None | |
369 | } | |
370 | ||
371 | fn fill_other<'a>(source: &'a str, v: &mut Vec<Token<'a>>, start: usize, pos: usize, | |
372 | is_in_block: isize, is_in_media: bool, is_in_attribute_selector: bool) { | |
373 | if start < pos { | |
374 | if is_in_attribute_selector == false && | |
375 | ((is_in_block == 0 && is_in_media == false) || | |
376 | (is_in_media == true && is_in_block == 1)) { | |
377 | let mut is_pseudo_class = false; | |
378 | let mut add = 0; | |
379 | if let Some(&Token::Char(ReservedChar::Colon)) = v.last() { | |
380 | is_pseudo_class = true; | |
381 | add = 1; | |
382 | } | |
383 | if let Ok(s) = SelectorElement::try_from(&source[start - add..pos]) { | |
384 | if is_pseudo_class { | |
385 | v.pop(); | |
386 | } | |
387 | v.push(Token::SelectorElement(s)); | |
388 | } else { | |
389 | let s = &source[start..pos]; | |
390 | if !s.starts_with(':') && !s.starts_with('.') && !s.starts_with('#') && | |
391 | !s.starts_with('@') { | |
392 | v.push(Token::Other(s)); | |
393 | } | |
394 | } | |
395 | } else { | |
396 | v.push(Token::Other(&source[start..pos])); | |
397 | } | |
398 | } | |
399 | } | |
400 | ||
401 | pub fn tokenize<'a>(source: &'a str) -> Result<Tokens<'a>, &'static str> { | |
402 | let mut v = Vec::with_capacity(1000); | |
403 | let mut iterator = source.char_indices().peekable(); | |
404 | let mut start = 0; | |
405 | let mut is_in_block: isize = 0; | |
406 | let mut is_in_media = false; | |
407 | let mut is_in_attribute_selector = false; | |
408 | ||
409 | loop { | |
410 | let (mut pos, c) = match iterator.next() { | |
411 | Some(x) => x, | |
412 | None => { | |
413 | fill_other(source, &mut v, start, source.len(), is_in_block, is_in_media, | |
414 | is_in_attribute_selector); | |
415 | break | |
416 | } | |
417 | }; | |
418 | if let Ok(c) = ReservedChar::try_from(c) { | |
419 | fill_other(source, &mut v, start, pos, is_in_block, is_in_media, | |
420 | is_in_attribute_selector); | |
421 | is_in_media = is_in_media || v.last() | |
422 | .unwrap_or(&Token::Char(ReservedChar::Space)) | |
423 | .is_media("media"); | |
424 | if c == ReservedChar::Quote || c == ReservedChar::DoubleQuote { | |
425 | if let Some(s) = get_string(source, &mut iterator, &mut pos, c) { | |
426 | v.push(s); | |
427 | } | |
428 | } else if c == ReservedChar::Star && | |
429 | *v.last().unwrap_or(&Token::Char(ReservedChar::Space)) == ReservedChar::Slash { | |
430 | v.pop(); | |
431 | if let Some(s) = get_comment(source, &mut iterator, &mut pos) { | |
432 | v.push(s); | |
433 | } | |
434 | } else if c == ReservedChar::Slash && | |
435 | *v.last().unwrap_or(&Token::Char(ReservedChar::Space)) == ReservedChar::Slash { | |
436 | v.pop(); | |
437 | if let Some(s) = get_line_comment(source, &mut iterator, &mut pos) { | |
438 | v.push(s); | |
439 | } | |
440 | } else if c == ReservedChar::OpenBracket { | |
441 | if is_in_attribute_selector { | |
442 | return Err("Already in attribute selector"); | |
443 | } | |
444 | is_in_attribute_selector = true; | |
445 | v.push(Token::Char(c)); | |
446 | } else if c == ReservedChar::CloseBracket { | |
447 | if !is_in_attribute_selector { | |
448 | return Err("Unexpected ']'"); | |
449 | } | |
450 | is_in_attribute_selector = false; | |
451 | v.push(Token::Char(c)); | |
452 | } else if c == ReservedChar::OpenCurlyBrace { | |
453 | is_in_block += 1; | |
454 | v.push(Token::Char(c)); | |
455 | } else if c == ReservedChar::CloseCurlyBrace { | |
456 | is_in_block -= 1; | |
457 | if is_in_block < 0 { | |
458 | return Err("Too much '}'"); | |
459 | } else if is_in_block == 0 { | |
460 | is_in_media = false; | |
461 | } | |
462 | v.push(Token::Char(c)); | |
463 | } else if c == ReservedChar::EqualSign { | |
464 | match match v.last() | |
465 | .unwrap_or(&Token::Char(ReservedChar::Space)) | |
466 | .get_char() | |
467 | .unwrap_or(ReservedChar::Space) { | |
468 | ReservedChar::Tilde => Some(SelectorOperator::OneAttributeEquals), | |
469 | ReservedChar::Pipe => Some(SelectorOperator::EqualsOrStartsWithFollowedByDash), | |
470 | ReservedChar::Dollar => Some(SelectorOperator::EndsWith), | |
471 | ReservedChar::Circumflex => Some(SelectorOperator::FirstStartsWith), | |
472 | ReservedChar::Star => Some(SelectorOperator::Contains), | |
473 | _ => None, | |
474 | } { | |
475 | Some(r) => { | |
476 | v.pop(); | |
477 | v.push(Token::SelectorOperator(r)); | |
478 | } | |
479 | None => v.push(Token::Char(c)), | |
480 | } | |
481 | } else if !c.is_useless() { | |
482 | v.push(Token::Char(c)); | |
483 | } else if !v.last().unwrap_or(&Token::Char(ReservedChar::Space)).is_useless() && | |
484 | !v.last().unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace)).is_char() { | |
485 | v.push(Token::Char(ReservedChar::Space)); | |
486 | } | |
487 | start = pos + 1; | |
488 | } | |
489 | } | |
490 | Ok(Tokens(clean_tokens(v))) | |
491 | } | |
492 | ||
493 | fn clean_tokens<'a>(mut v: Vec<Token<'a>>) -> Vec<Token<'a>> { | |
494 | let mut i = 0; | |
495 | ||
496 | while i < v.len() { | |
497 | if v[i].is_useless() { | |
498 | if (i > 0 && (v[i - 1].is_char() || | |
499 | v[i - 1].is_a_media() || | |
500 | v[i - 1].is_a_license())) || | |
501 | (i < v.len() - 1 && v[i + 1].is_char()) { | |
502 | v.remove(i); | |
503 | continue | |
504 | } | |
505 | } else if v[i].is_comment() { | |
506 | v.remove(i); | |
507 | continue | |
508 | } | |
509 | i += 1; | |
510 | } | |
511 | v | |
512 | } | |
513 | ||
514 | #[derive(Debug, PartialEq, Eq, Clone)] | |
515 | pub struct Tokens<'a>(pub Vec<Token<'a>>); | |
516 | ||
517 | impl<'a> fmt::Display for Tokens<'a> { | |
518 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
519 | for token in self.0.iter() { | |
520 | write!(f, "{}", token)?; | |
521 | } | |
522 | Ok(()) | |
523 | } | |
524 | } | |
525 | ||
526 | #[test] | |
527 | fn css_basic() { | |
528 | let s = r#" | |
529 | /*! just some license */ | |
530 | .foo > #bar p:hover { | |
531 | color: blue; | |
532 | background: "blue"; | |
533 | } | |
534 | ||
535 | // a comment! | |
536 | @media screen and (max-width: 640px) { | |
537 | .block:hover { | |
538 | display: block; | |
539 | } | |
540 | }"#; | |
541 | let expected = vec![Token::License(" just some license "), | |
542 | Token::SelectorElement(SelectorElement::Class("foo")), | |
543 | Token::Char(ReservedChar::SuperiorThan), | |
544 | Token::SelectorElement(SelectorElement::Id("bar")), | |
545 | Token::Char(ReservedChar::Space), | |
546 | Token::SelectorElement(SelectorElement::Tag("p")), | |
547 | Token::SelectorElement(SelectorElement::PseudoClass("hover")), | |
548 | Token::Char(ReservedChar::OpenCurlyBrace), | |
549 | Token::Other("color"), | |
550 | Token::Char(ReservedChar::Colon), | |
551 | Token::Other("blue"), | |
552 | Token::Char(ReservedChar::SemiColon), | |
553 | Token::Other("background"), | |
554 | Token::Char(ReservedChar::Colon), | |
555 | Token::String("\"blue\""), | |
556 | Token::Char(ReservedChar::SemiColon), | |
557 | Token::Char(ReservedChar::CloseCurlyBrace), | |
558 | Token::SelectorElement(SelectorElement::Media("media")), | |
559 | Token::Other("screen"), | |
560 | Token::Char(ReservedChar::Space), | |
561 | Token::Other("and"), | |
562 | Token::Char(ReservedChar::OpenParenthese), | |
563 | Token::Other("max-width"), | |
564 | Token::Char(ReservedChar::Colon), | |
565 | Token::Other("640px"), | |
566 | Token::Char(ReservedChar::CloseParenthese), | |
567 | Token::Char(ReservedChar::OpenCurlyBrace), | |
568 | Token::SelectorElement(SelectorElement::Class("block")), | |
569 | Token::SelectorElement(SelectorElement::PseudoClass("hover")), | |
570 | Token::Char(ReservedChar::OpenCurlyBrace), | |
571 | Token::Other("display"), | |
572 | Token::Char(ReservedChar::Colon), | |
573 | Token::Other("block"), | |
574 | Token::Char(ReservedChar::SemiColon), | |
575 | Token::Char(ReservedChar::CloseCurlyBrace), | |
576 | Token::Char(ReservedChar::CloseCurlyBrace)]; | |
577 | assert_eq!(tokenize(s), Ok(Tokens(expected))); | |
578 | } | |
579 | ||
580 | #[test] | |
581 | fn elem_selector() { | |
582 | let s = r#" | |
583 | /** just some license */ | |
584 | a[href*="example"] { | |
585 | background: yellow; | |
586 | } | |
587 | a[href$=".org"] { | |
588 | font-style: italic; | |
589 | } | |
590 | span[lang|="zh"] { | |
591 | color: red; | |
592 | } | |
593 | a[href^="/"] { | |
594 | background-color: gold; | |
595 | } | |
596 | div[value~="test"] { | |
597 | border-width: 1px; | |
598 | } | |
599 | span[lang="pt"] { | |
600 | font-size: 12em; // I love big fonts | |
601 | } | |
602 | "#; | |
603 | let expected = vec![Token::License(" just some license "), | |
604 | Token::SelectorElement(SelectorElement::Tag("a")), | |
605 | Token::Char(ReservedChar::OpenBracket), | |
606 | Token::Other("href"), | |
607 | Token::SelectorOperator(SelectorOperator::Contains), | |
608 | Token::String("\"example\""), | |
609 | Token::Char(ReservedChar::CloseBracket), | |
610 | Token::Char(ReservedChar::OpenCurlyBrace), | |
611 | Token::Other("background"), | |
612 | Token::Char(ReservedChar::Colon), | |
613 | Token::Other("yellow"), | |
614 | Token::Char(ReservedChar::SemiColon), | |
615 | Token::Char(ReservedChar::CloseCurlyBrace), | |
616 | ||
617 | Token::SelectorElement(SelectorElement::Tag("a")), | |
618 | Token::Char(ReservedChar::OpenBracket), | |
619 | Token::Other("href"), | |
620 | Token::SelectorOperator(SelectorOperator::EndsWith), | |
621 | Token::String("\".org\""), | |
622 | Token::Char(ReservedChar::CloseBracket), | |
623 | Token::Char(ReservedChar::OpenCurlyBrace), | |
624 | Token::Other("font-style"), | |
625 | Token::Char(ReservedChar::Colon), | |
626 | Token::Other("italic"), | |
627 | Token::Char(ReservedChar::SemiColon), | |
628 | Token::Char(ReservedChar::CloseCurlyBrace), | |
629 | ||
630 | Token::SelectorElement(SelectorElement::Tag("span")), | |
631 | Token::Char(ReservedChar::OpenBracket), | |
632 | Token::Other("lang"), | |
633 | Token::SelectorOperator(SelectorOperator::EqualsOrStartsWithFollowedByDash), | |
634 | Token::String("\"zh\""), | |
635 | Token::Char(ReservedChar::CloseBracket), | |
636 | Token::Char(ReservedChar::OpenCurlyBrace), | |
637 | Token::Other("color"), | |
638 | Token::Char(ReservedChar::Colon), | |
639 | Token::Other("red"), | |
640 | Token::Char(ReservedChar::SemiColon), | |
641 | Token::Char(ReservedChar::CloseCurlyBrace), | |
642 | ||
643 | Token::SelectorElement(SelectorElement::Tag("a")), | |
644 | Token::Char(ReservedChar::OpenBracket), | |
645 | Token::Other("href"), | |
646 | Token::SelectorOperator(SelectorOperator::FirstStartsWith), | |
647 | Token::String("\"/\""), | |
648 | Token::Char(ReservedChar::CloseBracket), | |
649 | Token::Char(ReservedChar::OpenCurlyBrace), | |
650 | Token::Other("background-color"), | |
651 | Token::Char(ReservedChar::Colon), | |
652 | Token::Other("gold"), | |
653 | Token::Char(ReservedChar::SemiColon), | |
654 | Token::Char(ReservedChar::CloseCurlyBrace), | |
655 | ||
656 | Token::SelectorElement(SelectorElement::Tag("div")), | |
657 | Token::Char(ReservedChar::OpenBracket), | |
658 | Token::Other("value"), | |
659 | Token::SelectorOperator(SelectorOperator::OneAttributeEquals), | |
660 | Token::String("\"test\""), | |
661 | Token::Char(ReservedChar::CloseBracket), | |
662 | Token::Char(ReservedChar::OpenCurlyBrace), | |
663 | Token::Other("border-width"), | |
664 | Token::Char(ReservedChar::Colon), | |
665 | Token::Other("1px"), | |
666 | Token::Char(ReservedChar::SemiColon), | |
667 | Token::Char(ReservedChar::CloseCurlyBrace), | |
668 | ||
669 | Token::SelectorElement(SelectorElement::Tag("span")), | |
670 | Token::Char(ReservedChar::OpenBracket), | |
671 | Token::Other("lang"), | |
672 | Token::Char(ReservedChar::EqualSign), | |
673 | Token::String("\"pt\""), | |
674 | Token::Char(ReservedChar::CloseBracket), | |
675 | Token::Char(ReservedChar::OpenCurlyBrace), | |
676 | Token::Other("font-size"), | |
677 | Token::Char(ReservedChar::Colon), | |
678 | Token::Other("12em"), | |
679 | Token::Char(ReservedChar::SemiColon), | |
680 | Token::Char(ReservedChar::CloseCurlyBrace)]; | |
681 | assert_eq!(tokenize(s), Ok(Tokens(expected))); | |
682 | } | |
683 | ||
684 | #[test] | |
685 | fn check_media() { | |
686 | let s = "@media (max-width: 700px) { color: red; }"; | |
687 | ||
688 | let expected = vec![Token::SelectorElement(SelectorElement::Media("media")), | |
689 | Token::Char(ReservedChar::OpenParenthese), | |
690 | Token::Other("max-width"), | |
691 | Token::Char(ReservedChar::Colon), | |
692 | Token::Other("700px"), | |
693 | Token::Char(ReservedChar::CloseParenthese), | |
694 | Token::Char(ReservedChar::OpenCurlyBrace), | |
695 | Token::SelectorElement(SelectorElement::Tag("color")), | |
696 | Token::Char(ReservedChar::Colon), | |
697 | Token::Other("red"), | |
698 | Token::Char(ReservedChar::SemiColon), | |
699 | Token::Char(ReservedChar::CloseCurlyBrace)]; | |
700 | assert_eq!(tokenize(s), Ok(Tokens(expected))); | |
701 | } |