Skip to main content

hpx_browser/css_parser/
token.rs

1use std::borrow::Cow;
2
3use crate::css_parser::source::SourceLocation;
4
5/// A CSS token with its source location.
6#[derive(Debug, Clone, PartialEq)]
7pub struct Token<'a> {
8    pub kind: TokenKind<'a>,
9    pub loc: SourceLocation,
10}
11
12/// All CSS token types per CSS Syntax Level 3 ยง4.
13#[derive(Debug, Clone, PartialEq)]
14pub enum TokenKind<'a> {
15    Ident(&'a str),
16    Function(&'a str),
17    AtKeyword(&'a str),
18    Hash {
19        value: &'a str,
20        is_id: bool,
21    },
22    String(&'a str),
23    BadString,
24    Url(&'a str),
25    BadUrl,
26    Number {
27        value: f64,
28        int_value: Option<i64>,
29        has_sign: bool,
30    },
31    Percentage {
32        value: f64,
33        int_value: Option<i64>,
34    },
35    Dimension {
36        value: f64,
37        int_value: Option<i64>,
38        unit: &'a str,
39    },
40    Whitespace,
41    Delim(char),
42    Colon,
43    Semicolon,
44    Comma,
45    OpenSquare,
46    CloseSquare,
47    OpenParen,
48    CloseParen,
49    OpenCurly,
50    CloseCurly,
51    Cdo,
52    Cdc,
53    Eof,
54}
55
56impl<'a> TokenKind<'a> {
57    pub fn is_whitespace(&self) -> bool {
58        matches!(self, TokenKind::Whitespace)
59    }
60
61    pub fn is_ident(&self) -> bool {
62        matches!(self, TokenKind::Ident(_))
63    }
64}
65
66/// Resolve CSS escape sequences in a raw string slice.
67pub fn resolve_escapes(raw: &str) -> Cow<'_, str> {
68    if !raw.contains('\\') {
69        return Cow::Borrowed(raw);
70    }
71
72    let mut result = String::with_capacity(raw.len());
73    let mut chars = raw.chars();
74
75    while let Some(ch) = chars.next() {
76        if ch == '\\' {
77            match chars.next() {
78                None => {
79                    result.push(ch);
80                }
81                Some('\n') => {}
82                Some(next) if next.is_ascii_hexdigit() => {
83                    let mut hex = String::with_capacity(6);
84                    hex.push(next);
85                    for _ in 0..5 {
86                        match chars.clone().next() {
87                            Some(h) if h.is_ascii_hexdigit() => {
88                                hex.push(h);
89                                chars.next();
90                            }
91                            _ => break,
92                        }
93                    }
94                    if let Some(&ws) = chars.as_str().as_bytes().first() {
95                        if ws == b' ' || ws == b'\t' || ws == b'\n' {
96                            chars.next();
97                        }
98                    }
99                    if let Ok(code) = u32::from_str_radix(&hex, 16) {
100                        if let Some(c) = char::from_u32(code) {
101                            if c == '\0' {
102                                result.push('\u{FFFD}');
103                            } else {
104                                result.push(c);
105                            }
106                        } else {
107                            result.push('\u{FFFD}');
108                        }
109                    } else {
110                        result.push('\u{FFFD}');
111                    }
112                }
113                Some(next) => {
114                    result.push(next);
115                }
116            }
117        } else {
118            result.push(ch);
119        }
120    }
121
122    Cow::Owned(result)
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn resolve_no_escapes() {
131        let result = resolve_escapes("hello");
132        assert!(matches!(result, Cow::Borrowed(_)));
133        assert_eq!(result, "hello");
134    }
135
136    #[test]
137    fn resolve_hex_escape() {
138        assert_eq!(resolve_escapes("\\26 "), "&");
139        assert_eq!(resolve_escapes("\\000026 "), "&");
140    }
141
142    #[test]
143    fn resolve_simple_escape() {
144        assert_eq!(resolve_escapes("\\("), "(");
145        assert_eq!(resolve_escapes("hello\\.world"), "hello.world");
146    }
147
148    #[test]
149    fn resolve_null_escape() {
150        assert_eq!(resolve_escapes("\\0 "), "\u{FFFD}");
151    }
152
153    #[test]
154    fn resolve_newline_continuation() {
155        assert_eq!(resolve_escapes("hel\\\nlo"), "hello");
156    }
157}