1#![allow(dead_code)]
9
10use alloc::{string::String, vec::Vec};
11
12pub type FixedPoint = i32;
14
15pub const fn px_to_fp(px: i32) -> FixedPoint {
17 px * 64
18}
19
20pub const fn fp_to_px(fp: FixedPoint) -> i32 {
22 fp / 64
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum CssToken {
28 Ident(String),
29 Hash(String),
30 StringToken(String),
31 Number(i32),
32 Percentage(i32),
33 Dimension(i32, String),
34 Whitespace,
35 Colon,
36 Semicolon,
37 Comma,
38 OpenBrace,
39 CloseBrace,
40 OpenParen,
41 CloseParen,
42 OpenBracket,
43 CloseBracket,
44 Delim(char),
45 AtKeyword(String),
46 Function(String),
47 Url(String),
48 Eof,
49}
50
51pub struct CssTokenizer {
53 input: Vec<u8>,
54 pos: usize,
55}
56
57impl CssTokenizer {
58 pub fn new(input: &str) -> Self {
59 Self {
60 input: input.as_bytes().to_vec(),
61 pos: 0,
62 }
63 }
64
65 fn peek(&self) -> Option<u8> {
66 self.input.get(self.pos).copied()
67 }
68
69 fn advance(&mut self) -> Option<u8> {
70 let ch = self.input.get(self.pos).copied();
71 if ch.is_some() {
72 self.pos += 1;
73 }
74 ch
75 }
76
77 fn skip_whitespace(&mut self) -> bool {
78 let start = self.pos;
79 while let Some(ch) = self.peek() {
80 if ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r' || ch == b'\x0C' {
81 self.pos += 1;
82 } else {
83 break;
84 }
85 }
86 self.pos > start
87 }
88
89 fn skip_comment(&mut self) -> bool {
90 if self.pos + 1 < self.input.len()
91 && self.input[self.pos] == b'/'
92 && self.input[self.pos + 1] == b'*'
93 {
94 self.pos += 2;
95 while self.pos + 1 < self.input.len() {
96 if self.input[self.pos] == b'*' && self.input[self.pos + 1] == b'/' {
97 self.pos += 2;
98 return true;
99 }
100 self.pos += 1;
101 }
102 self.pos = self.input.len();
103 return true;
104 }
105 false
106 }
107
108 fn read_ident(&mut self) -> String {
109 let mut s = String::new();
110 while let Some(ch) = self.peek() {
111 if ch.is_ascii_alphanumeric() || ch == b'-' || ch == b'_' {
112 s.push(ch as char);
113 self.pos += 1;
114 } else {
115 break;
116 }
117 }
118 s
119 }
120
121 fn read_string(&mut self, quote: u8) -> String {
122 let mut s = String::new();
123 while let Some(ch) = self.advance() {
124 if ch == quote {
125 break;
126 }
127 if ch == b'\\' {
128 if let Some(escaped) = self.advance() {
129 s.push(escaped as char);
130 }
131 } else {
132 s.push(ch as char);
133 }
134 }
135 s
136 }
137
138 fn read_number(&mut self) -> i32 {
139 let mut s = String::new();
140 let mut negative = false;
141
142 if self.peek() == Some(b'-') {
143 negative = true;
144 self.pos += 1;
145 } else if self.peek() == Some(b'+') {
146 self.pos += 1;
147 }
148
149 while let Some(ch) = self.peek() {
150 if ch.is_ascii_digit() {
151 s.push(ch as char);
152 self.pos += 1;
153 } else {
154 break;
155 }
156 }
157
158 if self.peek() == Some(b'.') {
160 self.pos += 1;
161 while let Some(ch) = self.peek() {
162 if ch.is_ascii_digit() {
163 self.pos += 1;
164 } else {
165 break;
166 }
167 }
168 }
169
170 let val = s.parse::<i32>().unwrap_or(0);
171 if negative {
172 -val
173 } else {
174 val
175 }
176 }
177
178 pub fn next_token(&mut self) -> CssToken {
179 while self.skip_comment() {}
181
182 if self.skip_whitespace() {
183 return CssToken::Whitespace;
184 }
185
186 match self.peek() {
187 None => CssToken::Eof,
188 Some(b'{') => {
189 self.pos += 1;
190 CssToken::OpenBrace
191 }
192 Some(b'}') => {
193 self.pos += 1;
194 CssToken::CloseBrace
195 }
196 Some(b'(') => {
197 self.pos += 1;
198 CssToken::OpenParen
199 }
200 Some(b')') => {
201 self.pos += 1;
202 CssToken::CloseParen
203 }
204 Some(b'[') => {
205 self.pos += 1;
206 CssToken::OpenBracket
207 }
208 Some(b']') => {
209 self.pos += 1;
210 CssToken::CloseBracket
211 }
212 Some(b':') => {
213 self.pos += 1;
214 CssToken::Colon
215 }
216 Some(b';') => {
217 self.pos += 1;
218 CssToken::Semicolon
219 }
220 Some(b',') => {
221 self.pos += 1;
222 CssToken::Comma
223 }
224 Some(b'#') => {
225 self.pos += 1;
226 let name = self.read_ident();
227 CssToken::Hash(name)
228 }
229 Some(b'.') => {
230 self.pos += 1;
231 if self.peek().is_some_and(|c| c.is_ascii_digit()) {
233 while self.peek().is_some_and(|c| c.is_ascii_digit()) {
235 self.pos += 1;
236 }
237 CssToken::Number(0)
238 } else {
239 CssToken::Delim('.')
240 }
241 }
242 Some(b'@') => {
243 self.pos += 1;
244 let name = self.read_ident();
245 CssToken::AtKeyword(name)
246 }
247 Some(b'"') => {
248 self.pos += 1;
249 let s = self.read_string(b'"');
250 CssToken::StringToken(s)
251 }
252 Some(b'\'') => {
253 self.pos += 1;
254 let s = self.read_string(b'\'');
255 CssToken::StringToken(s)
256 }
257 Some(ch) if ch.is_ascii_digit() || ch == b'-' || ch == b'+' => {
258 if ch == b'-' || ch == b'+' {
260 let next = self.input.get(self.pos + 1).copied();
262 if next.is_some_and(|c| c.is_ascii_digit()) {
263 let num = self.read_number();
264 if self.peek() == Some(b'%') {
266 self.pos += 1;
267 CssToken::Percentage(num)
268 } else if self.peek().is_some_and(|c| c.is_ascii_alphabetic()) {
269 let unit = self.read_ident();
270 CssToken::Dimension(num, unit)
271 } else {
272 CssToken::Number(num)
273 }
274 } else if ch == b'-' {
275 let ident = self.read_ident();
277 if self.peek() == Some(b'(') {
278 self.pos += 1;
279 CssToken::Function(ident)
280 } else {
281 CssToken::Ident(ident)
282 }
283 } else {
284 self.pos += 1;
285 CssToken::Delim(ch as char)
286 }
287 } else {
288 let num = self.read_number();
289 if self.peek() == Some(b'%') {
290 self.pos += 1;
291 CssToken::Percentage(num)
292 } else if self.peek().is_some_and(|c| c.is_ascii_alphabetic()) {
293 let unit = self.read_ident();
294 CssToken::Dimension(num, unit)
295 } else {
296 CssToken::Number(num)
297 }
298 }
299 }
300 Some(ch) if ch.is_ascii_alphabetic() || ch == b'_' => {
301 let ident = self.read_ident();
302 if self.peek() == Some(b'(') {
303 self.pos += 1;
304 if ident == "url" {
305 while self.skip_whitespace() {}
307 let url = if self.peek() == Some(b'"') || self.peek() == Some(b'\'') {
308 let q = self.advance().unwrap();
309 let s = self.read_string(q);
310 while self.skip_whitespace() {}
311 if self.peek() == Some(b')') {
312 self.pos += 1;
313 }
314 s
315 } else {
316 let mut s = String::new();
317 while let Some(c) = self.peek() {
318 if c == b')' {
319 self.pos += 1;
320 break;
321 }
322 s.push(c as char);
323 self.pos += 1;
324 }
325 s
326 };
327 CssToken::Url(url)
328 } else {
329 CssToken::Function(ident)
330 }
331 } else {
332 CssToken::Ident(ident)
333 }
334 }
335 Some(b'*') => {
336 self.pos += 1;
337 CssToken::Delim('*')
338 }
339 Some(b'>') => {
340 self.pos += 1;
341 CssToken::Delim('>')
342 }
343 Some(b'~') => {
344 self.pos += 1;
345 CssToken::Delim('~')
346 }
347 Some(b'!') => {
348 self.pos += 1;
349 CssToken::Delim('!')
350 }
351 Some(ch) => {
352 self.pos += 1;
353 CssToken::Delim(ch as char)
354 }
355 }
356 }
357
358 pub fn tokenize_all(&mut self) -> Vec<CssToken> {
359 let mut tokens = Vec::new();
360 loop {
361 let token = self.next_token();
362 if token == CssToken::Eof {
363 tokens.push(CssToken::Eof);
364 break;
365 }
366 tokens.push(token);
367 }
368 tokens
369 }
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Default)]
374pub struct SimpleSelector {
375 pub tag_name: Option<String>,
376 pub id: Option<String>,
377 pub classes: Vec<String>,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq)]
382pub enum Selector {
383 Simple(SimpleSelector),
384 Descendant(Vec<Selector>),
385 Child(Vec<Selector>),
386 Class(String),
387 Id(String),
388 Tag(String),
389 Universal,
390 Compound(Vec<Selector>),
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
395pub struct Specificity(pub u32, pub u32, pub u32);
396
397impl Selector {
398 pub fn specificity(&self) -> Specificity {
399 match self {
400 Selector::Id(_) => Specificity(1, 0, 0),
401 Selector::Class(_) => Specificity(0, 1, 0),
402 Selector::Tag(_) => Specificity(0, 0, 1),
403 Selector::Universal => Specificity(0, 0, 0),
404 Selector::Simple(s) => {
405 let ids = if s.id.is_some() { 1 } else { 0 };
406 let classes = s.classes.len() as u32;
407 let tags = if s.tag_name.is_some() { 1 } else { 0 };
408 Specificity(ids, classes, tags)
409 }
410 Selector::Descendant(parts) | Selector::Child(parts) | Selector::Compound(parts) => {
411 let mut spec = Specificity(0, 0, 0);
412 for p in parts {
413 let s = p.specificity();
414 spec.0 += s.0;
415 spec.1 += s.1;
416 spec.2 += s.2;
417 }
418 spec
419 }
420 }
421 }
422}
423
424#[derive(Debug, Clone, PartialEq, Eq)]
426pub enum Unit {
427 Px,
428 Em,
429 Rem,
430 Percent,
431 Vh,
432 Vw,
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Default)]
437pub enum CssValue {
438 Keyword(String),
439 Length(FixedPoint, Unit),
440 Color(u32),
441 Percentage(FixedPoint),
442 Number(i32),
443 Auto,
444 #[default]
445 None,
446 Inherit,
447 Initial,
448}
449
450#[derive(Debug, Clone)]
452pub struct Declaration {
453 pub property: String,
454 pub value: CssValue,
455 pub important: bool,
456}
457
458#[derive(Debug, Clone)]
460pub struct CssRule {
461 pub selectors: Vec<Selector>,
462 pub declarations: Vec<Declaration>,
463}
464
465#[derive(Debug, Clone, Default)]
467pub struct Stylesheet {
468 pub rules: Vec<CssRule>,
469}
470
471pub struct CssParser {
473 tokens: Vec<CssToken>,
474 pos: usize,
475}
476
477impl CssParser {
478 pub fn new(css: &str) -> Self {
479 let mut tokenizer = CssTokenizer::new(css);
480 let tokens = tokenizer.tokenize_all();
481 Self { tokens, pos: 0 }
482 }
483
484 fn peek(&self) -> &CssToken {
485 self.tokens.get(self.pos).unwrap_or(&CssToken::Eof)
486 }
487
488 fn advance(&mut self) -> CssToken {
489 let token = self.tokens.get(self.pos).cloned().unwrap_or(CssToken::Eof);
490 if self.pos < self.tokens.len() {
491 self.pos += 1;
492 }
493 token
494 }
495
496 fn skip_whitespace(&mut self) {
497 while *self.peek() == CssToken::Whitespace {
498 self.pos += 1;
499 }
500 }
501
502 pub fn parse(css: &str) -> Stylesheet {
504 let mut parser = CssParser::new(css);
505 parser.parse_stylesheet()
506 }
507
508 fn parse_stylesheet(&mut self) -> Stylesheet {
509 let mut rules = Vec::new();
510 loop {
511 self.skip_whitespace();
512 if *self.peek() == CssToken::Eof {
513 break;
514 }
515 if let CssToken::AtKeyword(_) = self.peek() {
517 self.skip_at_rule();
518 continue;
519 }
520 if let Some(rule) = self.parse_rule() {
521 rules.push(rule);
522 }
523 }
524 Stylesheet { rules }
525 }
526
527 fn skip_at_rule(&mut self) {
528 let mut depth = 0;
529 loop {
530 match self.advance() {
531 CssToken::OpenBrace => depth += 1,
532 CssToken::CloseBrace => {
533 if depth <= 1 {
534 break;
535 }
536 depth -= 1;
537 }
538 CssToken::Semicolon if depth == 0 => break,
539 CssToken::Eof => break,
540 _ => {}
541 }
542 }
543 }
544
545 fn parse_rule(&mut self) -> Option<CssRule> {
546 let selectors = self.parse_selectors();
547 if selectors.is_empty() {
548 let mut depth = 0;
550 loop {
551 match self.advance() {
552 CssToken::OpenBrace => depth += 1,
553 CssToken::CloseBrace if depth <= 1 => break,
554 CssToken::CloseBrace => depth -= 1,
555 CssToken::Eof => break,
556 _ => {}
557 }
558 }
559 return None;
560 }
561
562 self.skip_whitespace();
563 if *self.peek() == CssToken::OpenBrace {
564 self.advance();
565 }
566
567 let declarations = self.parse_declarations();
568
569 self.skip_whitespace();
570 if *self.peek() == CssToken::CloseBrace {
571 self.advance();
572 }
573
574 Some(CssRule {
575 selectors,
576 declarations,
577 })
578 }
579
580 fn parse_selectors(&mut self) -> Vec<Selector> {
581 let mut selectors = Vec::new();
582 if let Some(sel) = self.parse_selector() {
583 selectors.push(sel);
584 }
585 while *self.peek() == CssToken::Comma {
586 self.advance();
587 self.skip_whitespace();
588 if let Some(sel) = self.parse_selector() {
589 selectors.push(sel);
590 }
591 }
592 selectors
593 }
594
595 fn parse_selector(&mut self) -> Option<Selector> {
596 self.skip_whitespace();
597 let mut parts: Vec<Selector> = Vec::new();
598 let mut combinators: Vec<char> = Vec::new();
599
600 if let Some(simple) = self.parse_simple_selector() {
601 parts.push(simple);
602 } else {
603 return None;
604 }
605
606 loop {
607 let had_ws = *self.peek() == CssToken::Whitespace;
608 self.skip_whitespace();
609
610 match self.peek() {
611 CssToken::Delim('>') => {
612 self.advance();
613 self.skip_whitespace();
614 combinators.push('>');
615 if let Some(s) = self.parse_simple_selector() {
616 parts.push(s);
617 }
618 }
619 CssToken::OpenBrace | CssToken::Comma | CssToken::Eof => break,
620 _ if had_ws => {
621 if let Some(s) = self.parse_simple_selector() {
623 combinators.push(' ');
624 parts.push(s);
625 } else {
626 break;
627 }
628 }
629 _ => break,
630 }
631 }
632
633 if parts.len() == 1 {
634 Some(parts.remove(0))
635 } else if combinators.iter().all(|&c| c == '>') {
636 Some(Selector::Child(parts))
637 } else {
638 Some(Selector::Descendant(parts))
639 }
640 }
641
642 fn parse_simple_selector(&mut self) -> Option<Selector> {
643 let mut selector = SimpleSelector::default();
644 let mut matched = false;
645
646 loop {
647 match self.peek().clone() {
648 CssToken::Ident(name) => {
649 let name = name.clone();
650 self.advance();
651 selector.tag_name = Some(name);
652 matched = true;
653 }
654 CssToken::Hash(name) => {
655 let name = name.clone();
656 self.advance();
657 selector.id = Some(name);
658 matched = true;
659 }
660 CssToken::Delim('.') => {
661 self.advance();
662 if let CssToken::Ident(name) = self.peek().clone() {
663 let name = name.clone();
664 self.advance();
665 selector.classes.push(name);
666 matched = true;
667 }
668 }
669 CssToken::Delim('*') => {
670 self.advance();
671 if !matched
672 && selector.tag_name.is_none()
673 && selector.id.is_none()
674 && selector.classes.is_empty()
675 {
676 return Some(Selector::Universal);
677 }
678 matched = true;
679 }
680 _ => break,
681 }
682 }
683
684 if !matched {
685 return None;
686 }
687
688 if selector.id.is_none() && selector.classes.is_empty() {
690 if let Some(ref tag) = selector.tag_name {
691 return Some(Selector::Tag(tag.clone()));
692 }
693 }
694 if selector.tag_name.is_none() && selector.classes.is_empty() {
695 if let Some(ref id) = selector.id {
696 return Some(Selector::Id(id.clone()));
697 }
698 }
699 if selector.tag_name.is_none() && selector.id.is_none() && selector.classes.len() == 1 {
700 return Some(Selector::Class(selector.classes[0].clone()));
701 }
702
703 Some(Selector::Simple(selector))
704 }
705
706 fn parse_declarations(&mut self) -> Vec<Declaration> {
707 let mut declarations = Vec::new();
708 loop {
709 self.skip_whitespace();
710 match self.peek() {
711 CssToken::CloseBrace | CssToken::Eof => break,
712 _ => {
713 if let Some(decl) = self.parse_declaration() {
714 declarations.push(decl);
715 } else {
716 loop {
718 match self.peek() {
719 CssToken::Semicolon => {
720 self.advance();
721 break;
722 }
723 CssToken::CloseBrace | CssToken::Eof => break,
724 _ => {
725 self.advance();
726 }
727 }
728 }
729 }
730 }
731 }
732 }
733 declarations
734 }
735
736 fn parse_declaration(&mut self) -> Option<Declaration> {
737 self.skip_whitespace();
738 let property = match self.peek().clone() {
739 CssToken::Ident(name) => {
740 let name = name.clone();
741 self.advance();
742 name
743 }
744 _ => return None,
745 };
746
747 self.skip_whitespace();
748 if *self.peek() != CssToken::Colon {
749 return None;
750 }
751 self.advance();
752 self.skip_whitespace();
753
754 let value = self.parse_value(&property);
755
756 let mut important = false;
758 self.skip_whitespace();
759 if *self.peek() == CssToken::Delim('!') {
760 self.advance();
761 self.skip_whitespace();
762 if let CssToken::Ident(ref s) = *self.peek() {
763 if s == "important" {
764 important = true;
765 self.advance();
766 }
767 }
768 }
769
770 self.skip_whitespace();
771 if *self.peek() == CssToken::Semicolon {
772 self.advance();
773 }
774
775 Some(Declaration {
776 property,
777 value,
778 important,
779 })
780 }
781
782 fn parse_value(&mut self, _property: &str) -> CssValue {
783 self.skip_whitespace();
784 match self.peek().clone() {
785 CssToken::Ident(name) => {
786 let name = name.clone();
787 self.advance();
788 match name.as_str() {
789 "auto" => CssValue::Auto,
790 "none" => CssValue::None,
791 "inherit" => CssValue::Inherit,
792 "initial" => CssValue::Initial,
793 _ => CssValue::Keyword(name),
794 }
795 }
796 CssToken::Number(n) => {
797 self.advance();
798 CssValue::Number(n)
799 }
800 CssToken::Dimension(n, ref unit) => {
801 let unit = unit.clone();
802 self.advance();
803 let u = match unit.as_str() {
804 "px" => Unit::Px,
805 "em" => Unit::Em,
806 "rem" => Unit::Rem,
807 "vh" => Unit::Vh,
808 "vw" => Unit::Vw,
809 _ => Unit::Px,
810 };
811 CssValue::Length(px_to_fp(n), u)
812 }
813 CssToken::Percentage(n) => {
814 self.advance();
815 CssValue::Percentage(px_to_fp(n))
816 }
817 CssToken::Hash(ref color) => {
818 let color = color.clone();
819 self.advance();
820 CssValue::Color(parse_hex_color(&color))
821 }
822 CssToken::Function(ref name) => {
823 let name = name.clone();
824 self.advance();
825 if name == "rgb" || name == "rgba" {
826 self.parse_rgb_function()
827 } else {
828 let mut depth = 1;
830 while depth > 0 {
831 match self.advance() {
832 CssToken::OpenParen => depth += 1,
833 CssToken::CloseParen => depth -= 1,
834 CssToken::Eof => break,
835 _ => {}
836 }
837 }
838 CssValue::None
839 }
840 }
841 CssToken::StringToken(s) => {
842 let s = s.clone();
843 self.advance();
844 CssValue::Keyword(s)
845 }
846 _ => {
847 self.advance();
848 CssValue::None
849 }
850 }
851 }
852
853 fn parse_rgb_function(&mut self) -> CssValue {
854 let mut values = Vec::new();
855 loop {
856 self.skip_whitespace();
857 match self.peek().clone() {
858 CssToken::Number(n) => {
859 self.advance();
860 values.push(n);
861 }
862 CssToken::Percentage(n) => {
863 self.advance();
864 values.push(fp_to_px(n) * 255 / 100);
865 }
866 CssToken::CloseParen => {
867 self.advance();
868 break;
869 }
870 CssToken::Comma | CssToken::Whitespace | CssToken::Delim('/') => {
871 self.advance();
872 }
873 CssToken::Eof => break,
874 _ => {
875 self.advance();
876 }
877 }
878 }
879
880 let r = values.first().copied().unwrap_or(0).clamp(0, 255) as u32;
881 let g = values.get(1).copied().unwrap_or(0).clamp(0, 255) as u32;
882 let b = values.get(2).copied().unwrap_or(0).clamp(0, 255) as u32;
883 let a = if values.len() >= 4 {
884 values[3].clamp(0, 255) as u32
885 } else {
886 255
887 };
888
889 CssValue::Color((a << 24) | (r << 16) | (g << 8) | b)
890 }
891}
892
893pub fn parse_hex_color(hex: &str) -> u32 {
895 let hex = hex.trim_start_matches('#');
896 match hex.len() {
897 3 => {
898 let r = u8_from_hex_char(hex.as_bytes()[0]);
899 let g = u8_from_hex_char(hex.as_bytes()[1]);
900 let b = u8_from_hex_char(hex.as_bytes()[2]);
901 let r = (r << 4) | r;
902 let g = (g << 4) | g;
903 let b = (b << 4) | b;
904 0xFF000000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
905 }
906 6 => {
907 let val = u32::from_str_radix(hex, 16).unwrap_or(0);
908 0xFF000000 | val
909 }
910 8 => u32::from_str_radix(hex, 16).unwrap_or(0xFF000000),
911 _ => 0xFF000000,
912 }
913}
914
915fn u8_from_hex_char(c: u8) -> u8 {
916 match c {
917 b'0'..=b'9' => c - b'0',
918 b'a'..=b'f' => c - b'a' + 10,
919 b'A'..=b'F' => c - b'A' + 10,
920 _ => 0,
921 }
922}
923
924pub fn named_color(name: &str) -> Option<u32> {
926 match name {
927 "black" => Some(0xFF000000),
928 "white" => Some(0xFFFFFFFF),
929 "red" => Some(0xFFFF0000),
930 "green" => Some(0xFF008000),
931 "blue" => Some(0xFF0000FF),
932 "yellow" => Some(0xFFFFFF00),
933 "cyan" | "aqua" => Some(0xFF00FFFF),
934 "magenta" | "fuchsia" => Some(0xFFFF00FF),
935 "gray" | "grey" => Some(0xFF808080),
936 "silver" => Some(0xFFC0C0C0),
937 "maroon" => Some(0xFF800000),
938 "olive" => Some(0xFF808000),
939 "lime" => Some(0xFF00FF00),
940 "navy" => Some(0xFF000080),
941 "purple" => Some(0xFF800080),
942 "teal" => Some(0xFF008080),
943 "orange" => Some(0xFFFFA500),
944 "transparent" => Some(0x00000000),
945 _ => None,
946 }
947}
948
949#[cfg(test)]
950mod tests {
951 #[allow(unused_imports)]
952 use alloc::vec;
953
954 use super::*;
955
956 #[test]
957 fn test_fixed_point_conversion() {
958 assert_eq!(px_to_fp(16), 1024);
959 assert_eq!(fp_to_px(1024), 16);
960 assert_eq!(fp_to_px(px_to_fp(42)), 42);
961 }
962
963 #[test]
964 fn test_tokenize_simple() {
965 let mut t = CssTokenizer::new("body { color: red; }");
966 let tokens = t.tokenize_all();
967 assert!(tokens.len() > 3);
968 }
969
970 #[test]
971 fn test_tokenize_hash() {
972 let mut t = CssTokenizer::new("#main");
973 let token = t.next_token();
974 assert_eq!(token, CssToken::Hash("main".into()));
975 }
976
977 #[test]
978 fn test_tokenize_dimension() {
979 let mut t = CssTokenizer::new("16px");
980 let token = t.next_token();
981 assert_eq!(token, CssToken::Dimension(16, "px".into()));
982 }
983
984 #[test]
985 fn test_tokenize_percentage() {
986 let mut t = CssTokenizer::new("50%");
987 let token = t.next_token();
988 assert_eq!(token, CssToken::Percentage(50));
989 }
990
991 #[test]
992 fn test_tokenize_string() {
993 let mut t = CssTokenizer::new("\"hello\"");
994 let token = t.next_token();
995 assert_eq!(token, CssToken::StringToken("hello".into()));
996 }
997
998 #[test]
999 fn test_tokenize_comment() {
1000 let mut t = CssTokenizer::new("/* comment */ body");
1001 let token = t.next_token();
1002 assert_eq!(token, CssToken::Whitespace);
1004 }
1005
1006 #[test]
1007 fn test_parse_simple_rule() {
1008 let ss = CssParser::parse("p { color: red; }");
1009 assert_eq!(ss.rules.len(), 1);
1010 assert_eq!(ss.rules[0].selectors.len(), 1);
1011 assert_eq!(ss.rules[0].declarations.len(), 1);
1012 }
1013
1014 #[test]
1015 fn test_parse_tag_selector() {
1016 let ss = CssParser::parse("div { margin: 0; }");
1017 assert_eq!(ss.rules[0].selectors[0], Selector::Tag("div".into()));
1018 }
1019
1020 #[test]
1021 fn test_parse_class_selector() {
1022 let ss = CssParser::parse(".main { padding: 10px; }");
1023 assert_eq!(ss.rules[0].selectors[0], Selector::Class("main".into()));
1024 }
1025
1026 #[test]
1027 fn test_parse_id_selector() {
1028 let ss = CssParser::parse("#header { height: 100px; }");
1029 assert_eq!(ss.rules[0].selectors[0], Selector::Id("header".into()));
1030 }
1031
1032 #[test]
1033 fn test_parse_universal_selector() {
1034 let ss = CssParser::parse("* { margin: 0; }");
1035 assert_eq!(ss.rules[0].selectors[0], Selector::Universal);
1036 }
1037
1038 #[test]
1039 fn test_parse_descendant_selector() {
1040 let ss = CssParser::parse("div p { color: blue; }");
1041 assert_eq!(ss.rules.len(), 1);
1042 if let Selector::Descendant(parts) = &ss.rules[0].selectors[0] {
1043 assert_eq!(parts.len(), 2);
1044 } else {
1045 panic!("Expected descendant selector");
1046 }
1047 }
1048
1049 #[test]
1050 fn test_parse_child_selector() {
1051 let ss = CssParser::parse("div > p { color: red; }");
1052 assert_eq!(ss.rules.len(), 1);
1053 if let Selector::Child(parts) = &ss.rules[0].selectors[0] {
1054 assert_eq!(parts.len(), 2);
1055 } else {
1056 panic!("Expected child selector");
1057 }
1058 }
1059
1060 #[test]
1061 fn test_parse_multiple_selectors() {
1062 let ss = CssParser::parse("h1, h2, h3 { font-weight: bold; }");
1063 assert_eq!(ss.rules[0].selectors.len(), 3);
1064 }
1065
1066 #[test]
1067 fn test_parse_hex_color_6() {
1068 assert_eq!(parse_hex_color("ff0000"), 0xFFFF0000);
1069 }
1070
1071 #[test]
1072 fn test_parse_hex_color_3() {
1073 assert_eq!(parse_hex_color("f00"), 0xFFFF0000);
1074 }
1075
1076 #[test]
1077 fn test_parse_color_declaration() {
1078 let ss = CssParser::parse("p { color: #ff0000; }");
1079 let decl = &ss.rules[0].declarations[0];
1080 assert_eq!(decl.property, "color");
1081 assert_eq!(decl.value, CssValue::Color(0xFFFF0000));
1082 }
1083
1084 #[test]
1085 fn test_parse_length_declaration() {
1086 let ss = CssParser::parse("div { width: 100px; }");
1087 let decl = &ss.rules[0].declarations[0];
1088 assert_eq!(decl.property, "width");
1089 assert_eq!(decl.value, CssValue::Length(px_to_fp(100), Unit::Px));
1090 }
1091
1092 #[test]
1093 fn test_parse_keyword_value() {
1094 let ss = CssParser::parse("div { display: block; }");
1095 let decl = &ss.rules[0].declarations[0];
1096 assert_eq!(decl.value, CssValue::Keyword("block".into()));
1097 }
1098
1099 #[test]
1100 fn test_parse_auto_value() {
1101 let ss = CssParser::parse("div { margin: auto; }");
1102 let decl = &ss.rules[0].declarations[0];
1103 assert_eq!(decl.value, CssValue::Auto);
1104 }
1105
1106 #[test]
1107 fn test_parse_none_value() {
1108 let ss = CssParser::parse("div { display: none; }");
1109 let decl = &ss.rules[0].declarations[0];
1110 assert_eq!(decl.value, CssValue::None);
1111 }
1112
1113 #[test]
1114 fn test_parse_inherit() {
1115 let ss = CssParser::parse("div { color: inherit; }");
1116 let decl = &ss.rules[0].declarations[0];
1117 assert_eq!(decl.value, CssValue::Inherit);
1118 }
1119
1120 #[test]
1121 fn test_parse_multiple_declarations() {
1122 let ss = CssParser::parse("p { color: red; font-size: 16px; margin: 0; }");
1123 assert_eq!(ss.rules[0].declarations.len(), 3);
1124 }
1125
1126 #[test]
1127 fn test_specificity_id() {
1128 let sel = Selector::Id("main".into());
1129 assert_eq!(sel.specificity(), Specificity(1, 0, 0));
1130 }
1131
1132 #[test]
1133 fn test_specificity_class() {
1134 let sel = Selector::Class("box".into());
1135 assert_eq!(sel.specificity(), Specificity(0, 1, 0));
1136 }
1137
1138 #[test]
1139 fn test_specificity_tag() {
1140 let sel = Selector::Tag("div".into());
1141 assert_eq!(sel.specificity(), Specificity(0, 0, 1));
1142 }
1143
1144 #[test]
1145 fn test_named_colors() {
1146 assert_eq!(named_color("red"), Some(0xFFFF0000));
1147 assert_eq!(named_color("blue"), Some(0xFF0000FF));
1148 assert_eq!(named_color("unknown"), None);
1149 }
1150
1151 #[test]
1152 fn test_parse_empty() {
1153 let ss = CssParser::parse("");
1154 assert!(ss.rules.is_empty());
1155 }
1156}