⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/browser/
css_parser.rs

1//! CSS Parser
2//!
3//! Tokenizes and parses CSS stylesheets into structured rule sets.
4//! Supports selectors (tag, class, id, descendant, child, universal,
5//! compound), declarations with typed values, and specificity calculation.
6//! All numeric values use 26.6 fixed-point (i32).
7
8#![allow(dead_code)]
9
10use alloc::{string::String, vec::Vec};
11
12/// 26.6 fixed-point type: multiply pixel values by 64
13pub type FixedPoint = i32;
14
15/// Convert pixels to 26.6 fixed-point
16pub const fn px_to_fp(px: i32) -> FixedPoint {
17    px * 64
18}
19
20/// Convert 26.6 fixed-point to pixels
21pub const fn fp_to_px(fp: FixedPoint) -> i32 {
22    fp / 64
23}
24
25/// CSS token types
26#[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
51/// CSS tokenizer
52pub 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        // Skip decimal part (we use integer math)
159        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        // Skip comments
180        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                // Check if it's a number after dot
232                if self.peek().is_some_and(|c| c.is_ascii_digit()) {
233                    // Skip decimal digits
234                    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                // Check if it's actually a number (or negative number)
259                if ch == b'-' || ch == b'+' {
260                    // Peek ahead to see if it's a number
261                    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                        // Check for unit or %
265                        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                        // It's an ident starting with -
276                        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                        // Read URL content
306                        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/// A simple CSS selector
373#[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/// Selector types
381#[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/// Specificity: (id_count, class_count, tag_count)
394#[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/// CSS measurement unit
425#[derive(Debug, Clone, PartialEq, Eq)]
426pub enum Unit {
427    Px,
428    Em,
429    Rem,
430    Percent,
431    Vh,
432    Vw,
433}
434
435/// CSS property value
436#[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/// A CSS declaration (property: value)
451#[derive(Debug, Clone)]
452pub struct Declaration {
453    pub property: String,
454    pub value: CssValue,
455    pub important: bool,
456}
457
458/// A CSS rule (selectors + declarations)
459#[derive(Debug, Clone)]
460pub struct CssRule {
461    pub selectors: Vec<Selector>,
462    pub declarations: Vec<Declaration>,
463}
464
465/// A parsed CSS stylesheet
466#[derive(Debug, Clone, Default)]
467pub struct Stylesheet {
468    pub rules: Vec<CssRule>,
469}
470
471/// CSS parser
472pub 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    /// Parse a complete stylesheet
503    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            // Skip @-rules
516            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            // Skip to closing brace
549            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                    // Descendant combinator (space)
622                    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        // Simplify: if only tag, id, or class, use specific variant
689        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                        // Skip to next semicolon or close brace
717                        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        // Check for !important
757        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                    // Skip function content
829                    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
893/// Parse a hex color string to ARGB u32
894pub 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
924/// Named CSS colors
925pub 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        // Should skip comment and return next token
1003        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}