Skip to main content

azul_css/
parser2.rs

1//! High-level types and functions related to CSS parsing
2use alloc::{collections::BTreeMap, string::ToString, vec::Vec};
3use core::{fmt, num::ParseIntError};
4
5pub use azul_simplecss::Error as CssSyntaxError;
6use azul_simplecss::Tokenizer;
7
8pub use crate::props::property::CssParsingError;
9use crate::{
10    corety::AzString,
11    css::{
12        Css, CssDeclaration, CssNthChildSelector, CssPath, CssPathPseudoSelector, CssPathSelector,
13        CssRuleBlock, DynamicCssProperty, NodeTypeTag, NodeTypeTagParseError,
14        NodeTypeTagParseErrorOwned, Stylesheet,
15    },
16    dynamic_selector::{
17        DynamicSelector, DynamicSelectorVec, LanguageCondition, MediaType, MinMaxRange,
18        OrientationType,
19    },
20    props::{
21        basic::parse::parse_parentheses,
22        property::{
23            parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
24            CssParsingErrorOwned, CssPropertyType,
25        },
26    },
27};
28
29/// Error that can happen during the parsing of a CSS value
30#[derive(Debug, Clone, PartialEq)]
31pub struct CssParseError<'a> {
32    pub css_string: &'a str,
33    pub error: CssParseErrorInner<'a>,
34    pub location: (ErrorLocation, ErrorLocation),
35}
36
37/// Owned version of CssParseError, without references.
38#[derive(Debug, Clone, PartialEq)]
39pub struct CssParseErrorOwned {
40    pub css_string: String,
41    pub error: CssParseErrorInnerOwned,
42    pub location: (ErrorLocation, ErrorLocation),
43}
44
45impl<'a> CssParseError<'a> {
46    pub fn to_contained(&self) -> CssParseErrorOwned {
47        CssParseErrorOwned {
48            css_string: self.css_string.to_string(),
49            error: self.error.to_contained(),
50            location: self.location.clone(),
51        }
52    }
53}
54
55impl CssParseErrorOwned {
56    pub fn to_shared<'a>(&'a self) -> CssParseError<'a> {
57        CssParseError {
58            css_string: &self.css_string,
59            error: self.error.to_shared(),
60            location: self.location.clone(),
61        }
62    }
63}
64
65impl<'a> CssParseError<'a> {
66    /// Returns the string between the (start, end) location
67    pub fn get_error_string(&self) -> &'a str {
68        let (start, end) = (self.location.0.original_pos, self.location.1.original_pos);
69        let s = &self.css_string[start..end];
70        s.trim()
71    }
72}
73
74#[derive(Debug, Clone, PartialEq)]
75pub enum CssParseErrorInner<'a> {
76    /// A hard error in the CSS syntax
77    ParseError(CssSyntaxError),
78    /// Braces are not balanced properly
79    UnclosedBlock,
80    /// Invalid syntax, such as `#div { #div: "my-value" }`
81    MalformedCss,
82    /// Error parsing dynamic CSS property, such as
83    /// `#div { width: {{ my_id }} /* no default case */ }`
84    DynamicCssParseError(DynamicCssParseError<'a>),
85    /// Error while parsing a pseudo selector (like `:aldkfja`)
86    PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
87    /// The path has to be either `*`, `div`, `p` or something like that
88    NodeTypeTag(NodeTypeTagParseError<'a>),
89    /// A certain property has an unknown key, for example: `alsdfkj: 500px` = `unknown CSS key
90    /// "alsdfkj: 500px"`
91    UnknownPropertyKey(&'a str, &'a str),
92    /// `var()` can't be used on properties that expand to multiple values, since they would be
93    /// ambigouus and degrade performance - for example `margin: var(--blah)` would be ambigouus
94    /// because it's not clear when setting the variable, whether all sides should be set,
95    /// instead, you have to use `margin-top: var(--blah)`, `margin-bottom: var(--baz)` in order
96    /// to work around this limitation.
97    VarOnShorthandProperty {
98        key: CombinedCssPropertyType,
99        value: &'a str,
100    },
101}
102
103#[derive(Debug, Clone, PartialEq)]
104pub enum CssParseErrorInnerOwned {
105    ParseError(CssSyntaxError),
106    UnclosedBlock,
107    MalformedCss,
108    DynamicCssParseError(DynamicCssParseErrorOwned),
109    PseudoSelectorParseError(CssPseudoSelectorParseErrorOwned),
110    NodeTypeTag(NodeTypeTagParseErrorOwned),
111    UnknownPropertyKey(String, String),
112    VarOnShorthandProperty {
113        key: CombinedCssPropertyType,
114        value: String,
115    },
116}
117
118impl<'a> CssParseErrorInner<'a> {
119    pub fn to_contained(&self) -> CssParseErrorInnerOwned {
120        match self {
121            CssParseErrorInner::ParseError(e) => CssParseErrorInnerOwned::ParseError(e.clone()),
122            CssParseErrorInner::UnclosedBlock => CssParseErrorInnerOwned::UnclosedBlock,
123            CssParseErrorInner::MalformedCss => CssParseErrorInnerOwned::MalformedCss,
124            CssParseErrorInner::DynamicCssParseError(e) => {
125                CssParseErrorInnerOwned::DynamicCssParseError(e.to_contained())
126            }
127            CssParseErrorInner::PseudoSelectorParseError(e) => {
128                CssParseErrorInnerOwned::PseudoSelectorParseError(e.to_contained())
129            }
130            CssParseErrorInner::NodeTypeTag(e) => {
131                CssParseErrorInnerOwned::NodeTypeTag(e.to_contained())
132            }
133            CssParseErrorInner::UnknownPropertyKey(a, b) => {
134                CssParseErrorInnerOwned::UnknownPropertyKey(a.to_string(), b.to_string())
135            }
136            CssParseErrorInner::VarOnShorthandProperty { key, value } => {
137                CssParseErrorInnerOwned::VarOnShorthandProperty {
138                    key: key.clone(),
139                    value: value.to_string(),
140                }
141            }
142        }
143    }
144}
145
146impl CssParseErrorInnerOwned {
147    pub fn to_shared<'a>(&'a self) -> CssParseErrorInner<'a> {
148        match self {
149            CssParseErrorInnerOwned::ParseError(e) => CssParseErrorInner::ParseError(e.clone()),
150            CssParseErrorInnerOwned::UnclosedBlock => CssParseErrorInner::UnclosedBlock,
151            CssParseErrorInnerOwned::MalformedCss => CssParseErrorInner::MalformedCss,
152            CssParseErrorInnerOwned::DynamicCssParseError(e) => {
153                CssParseErrorInner::DynamicCssParseError(e.to_shared())
154            }
155            CssParseErrorInnerOwned::PseudoSelectorParseError(e) => {
156                CssParseErrorInner::PseudoSelectorParseError(e.to_shared())
157            }
158            CssParseErrorInnerOwned::NodeTypeTag(e) => {
159                CssParseErrorInner::NodeTypeTag(e.to_shared())
160            }
161            CssParseErrorInnerOwned::UnknownPropertyKey(a, b) => {
162                CssParseErrorInner::UnknownPropertyKey(a, b)
163            }
164            CssParseErrorInnerOwned::VarOnShorthandProperty { key, value } => {
165                CssParseErrorInner::VarOnShorthandProperty {
166                    key: key.clone(),
167                    value,
168                }
169            }
170        }
171    }
172}
173
174impl_display! { CssParseErrorInner<'a>, {
175    ParseError(e) => format!("Parse Error: {:?}", e),
176    UnclosedBlock => "Unclosed block",
177    MalformedCss => "Malformed Css",
178    DynamicCssParseError(e) => format!("{}", e),
179    PseudoSelectorParseError(e) => format!("Failed to parse pseudo-selector: {}", e),
180    NodeTypeTag(e) => format!("Failed to parse CSS selector path: {}", e),
181    UnknownPropertyKey(k, v) => format!("Unknown CSS key: \"{}: {}\"", k, v),
182    VarOnShorthandProperty { key, value } => format!(
183        "Error while parsing: \"{}: {};\": var() cannot be used on shorthand properties - use `{}-top` or `{}-x` as the key instead: ",
184        key, value, key, key
185    ),
186}}
187
188impl<'a> From<CssSyntaxError> for CssParseErrorInner<'a> {
189    fn from(e: CssSyntaxError) -> Self {
190        CssParseErrorInner::ParseError(e)
191    }
192}
193
194impl_from! { DynamicCssParseError<'a>, CssParseErrorInner::DynamicCssParseError }
195impl_from! { NodeTypeTagParseError<'a>, CssParseErrorInner::NodeTypeTag }
196impl_from! { CssPseudoSelectorParseError<'a>, CssParseErrorInner::PseudoSelectorParseError }
197
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum CssPseudoSelectorParseError<'a> {
200    EmptyNthChild,
201    UnknownSelector(&'a str, Option<&'a str>),
202    InvalidNthChildPattern(&'a str),
203    InvalidNthChild(ParseIntError),
204}
205
206impl<'a> From<ParseIntError> for CssPseudoSelectorParseError<'a> {
207    fn from(e: ParseIntError) -> Self {
208        CssPseudoSelectorParseError::InvalidNthChild(e)
209    }
210}
211
212impl_display! { CssPseudoSelectorParseError<'a>, {
213    EmptyNthChild => format!("\
214        Empty :nth-child() selector - nth-child() must at least take a number, \
215        a pattern (such as \"2n+3\") or the values \"even\" or \"odd\"."
216    ),
217    UnknownSelector(selector, value) => {
218        let format_str = match value {
219            Some(v) => format!("{}({})", selector, v),
220            None => format!("{}", selector),
221        };
222        format!("Invalid or unknown CSS pseudo-selector: ':{}'", format_str)
223    },
224    InvalidNthChildPattern(selector) => format!(
225        "Invalid pseudo-selector :{} - value has to be a \
226        number, \"even\" or \"odd\" or a pattern such as \"2n+3\"", selector
227    ),
228    InvalidNthChild(e) => format!("Invalid :nth-child pseudo-selector: ':{}'", e),
229}}
230
231#[derive(Debug, Clone, PartialEq)]
232pub enum CssPseudoSelectorParseErrorOwned {
233    EmptyNthChild,
234    UnknownSelector(String, Option<String>),
235    InvalidNthChildPattern(String),
236    InvalidNthChild(ParseIntError),
237}
238
239impl<'a> CssPseudoSelectorParseError<'a> {
240    pub fn to_contained(&self) -> CssPseudoSelectorParseErrorOwned {
241        match self {
242            CssPseudoSelectorParseError::EmptyNthChild => {
243                CssPseudoSelectorParseErrorOwned::EmptyNthChild
244            }
245            CssPseudoSelectorParseError::UnknownSelector(a, b) => {
246                CssPseudoSelectorParseErrorOwned::UnknownSelector(
247                    a.to_string(),
248                    b.map(|s| s.to_string()),
249                )
250            }
251            CssPseudoSelectorParseError::InvalidNthChildPattern(s) => {
252                CssPseudoSelectorParseErrorOwned::InvalidNthChildPattern(s.to_string())
253            }
254            CssPseudoSelectorParseError::InvalidNthChild(e) => {
255                CssPseudoSelectorParseErrorOwned::InvalidNthChild(e.clone())
256            }
257        }
258    }
259}
260
261impl CssPseudoSelectorParseErrorOwned {
262    pub fn to_shared<'a>(&'a self) -> CssPseudoSelectorParseError<'a> {
263        match self {
264            CssPseudoSelectorParseErrorOwned::EmptyNthChild => {
265                CssPseudoSelectorParseError::EmptyNthChild
266            }
267            CssPseudoSelectorParseErrorOwned::UnknownSelector(a, b) => {
268                CssPseudoSelectorParseError::UnknownSelector(a, b.as_deref())
269            }
270            CssPseudoSelectorParseErrorOwned::InvalidNthChildPattern(s) => {
271                CssPseudoSelectorParseError::InvalidNthChildPattern(s)
272            }
273            CssPseudoSelectorParseErrorOwned::InvalidNthChild(e) => {
274                CssPseudoSelectorParseError::InvalidNthChild(e.clone())
275            }
276        }
277    }
278}
279
280/// Error that can happen during `css_parser::parse_key_value_pair`
281#[derive(Debug, Clone, PartialEq)]
282pub enum DynamicCssParseError<'a> {
283    /// The brace contents aren't valid, i.e. `var(asdlfkjasf)`
284    InvalidBraceContents(&'a str),
285    /// Unexpected value when parsing the string
286    UnexpectedValue(CssParsingError<'a>),
287}
288
289impl_display! { DynamicCssParseError<'a>, {
290    InvalidBraceContents(e) => format!("Invalid contents of var() function: var({})", e),
291    UnexpectedValue(e) => format!("{}", e),
292}}
293
294impl<'a> From<CssParsingError<'a>> for DynamicCssParseError<'a> {
295    fn from(e: CssParsingError<'a>) -> Self {
296        DynamicCssParseError::UnexpectedValue(e)
297    }
298}
299
300#[derive(Debug, Clone, PartialEq)]
301pub enum DynamicCssParseErrorOwned {
302    InvalidBraceContents(String),
303    UnexpectedValue(CssParsingErrorOwned),
304}
305
306impl<'a> DynamicCssParseError<'a> {
307    pub fn to_contained(&self) -> DynamicCssParseErrorOwned {
308        match self {
309            DynamicCssParseError::InvalidBraceContents(s) => {
310                DynamicCssParseErrorOwned::InvalidBraceContents(s.to_string())
311            }
312            DynamicCssParseError::UnexpectedValue(e) => {
313                DynamicCssParseErrorOwned::UnexpectedValue(e.to_contained())
314            }
315        }
316    }
317}
318
319impl DynamicCssParseErrorOwned {
320    pub fn to_shared<'a>(&'a self) -> DynamicCssParseError<'a> {
321        match self {
322            DynamicCssParseErrorOwned::InvalidBraceContents(s) => {
323                DynamicCssParseError::InvalidBraceContents(s)
324            }
325            DynamicCssParseErrorOwned::UnexpectedValue(e) => {
326                DynamicCssParseError::UnexpectedValue(e.to_shared())
327            }
328        }
329    }
330}
331
332/// "selector" contains the actual selector such as "nth-child" while "value" contains
333/// an optional value - for example "nth-child(3)" would be: selector: "nth-child", value: "3".
334pub fn pseudo_selector_from_str<'a>(
335    selector: &'a str,
336    value: Option<&'a str>,
337) -> Result<CssPathPseudoSelector, CssPseudoSelectorParseError<'a>> {
338    match selector {
339        "first" => Ok(CssPathPseudoSelector::First),
340        "last" => Ok(CssPathPseudoSelector::Last),
341        "hover" => Ok(CssPathPseudoSelector::Hover),
342        "active" => Ok(CssPathPseudoSelector::Active),
343        "focus" => Ok(CssPathPseudoSelector::Focus),
344        "nth-child" => {
345            let value = value.ok_or(CssPseudoSelectorParseError::EmptyNthChild)?;
346            let parsed = parse_nth_child_selector(value)?;
347            Ok(CssPathPseudoSelector::NthChild(parsed))
348        }
349        "lang" => {
350            let lang_value = value.ok_or(CssPseudoSelectorParseError::UnknownSelector(
351                selector, value,
352            ))?;
353            // Remove quotes if present
354            let lang_value = lang_value
355                .trim()
356                .trim_start_matches('"')
357                .trim_end_matches('"')
358                .trim_start_matches('\'')
359                .trim_end_matches('\'')
360                .trim();
361            Ok(CssPathPseudoSelector::Lang(AzString::from(
362                lang_value.to_string(),
363            )))
364        }
365        _ => Err(CssPseudoSelectorParseError::UnknownSelector(
366            selector, value,
367        )),
368    }
369}
370
371/// Parses the inner value of the `:nth-child` selector, including numbers and patterns.
372///
373/// I.e.: `"2n+3"` -> `Pattern { repeat: 2, offset: 3 }`
374fn parse_nth_child_selector<'a>(
375    value: &'a str,
376) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'a>> {
377    let value = value.trim();
378
379    if value.is_empty() {
380        return Err(CssPseudoSelectorParseError::EmptyNthChild);
381    }
382
383    if let Ok(number) = value.parse::<u32>() {
384        return Ok(CssNthChildSelector::Number(number));
385    }
386
387    // If the value is not a number
388    match value.as_ref() {
389        "even" => Ok(CssNthChildSelector::Even),
390        "odd" => Ok(CssNthChildSelector::Odd),
391        other => parse_nth_child_pattern(value),
392    }
393}
394
395/// Parses the pattern between the braces of a "nth-child" (such as "2n+3").
396fn parse_nth_child_pattern<'a>(
397    value: &'a str,
398) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'a>> {
399    use crate::css::CssNthChildPattern;
400
401    let value = value.trim();
402
403    if value.is_empty() {
404        return Err(CssPseudoSelectorParseError::EmptyNthChild);
405    }
406
407    // TODO: Test for "+"
408    let repeat = value
409        .split("n")
410        .next()
411        .ok_or(CssPseudoSelectorParseError::InvalidNthChildPattern(value))?
412        .trim()
413        .parse::<u32>()?;
414
415    // In a "2n+3" form, the first .next() yields the "2n", the second .next() yields the "3"
416    let mut offset_iterator = value.split("+");
417
418    // has to succeed, since the string is verified to not be empty
419    offset_iterator.next().unwrap();
420
421    let offset = match offset_iterator.next() {
422        Some(offset_string) => {
423            let offset_string = offset_string.trim();
424            if offset_string.is_empty() {
425                return Err(CssPseudoSelectorParseError::InvalidNthChildPattern(value));
426            } else {
427                offset_string.parse::<u32>()?
428            }
429        }
430        None => 0,
431    };
432
433    Ok(CssNthChildSelector::Pattern(CssNthChildPattern {
434        pattern_repeat: repeat,
435        offset,
436    }))
437}
438
439#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
440pub struct ErrorLocation {
441    pub original_pos: usize,
442}
443
444impl ErrorLocation {
445    /// Given an error location, returns the (line, column)
446    pub fn get_line_column_from_error(&self, css_string: &str) -> (usize, usize) {
447        let error_location = self.original_pos.saturating_sub(1);
448        let (mut line_number, mut total_characters) = (0, 0);
449
450        for line in css_string[0..error_location].lines() {
451            line_number += 1;
452            total_characters += line.chars().count();
453        }
454
455        // Rust doesn't count "\n" as a character, so we have to add the line number count on top
456        let total_characters = total_characters + line_number;
457        let column_pos = error_location - total_characters.saturating_sub(2);
458
459        (line_number, column_pos)
460    }
461}
462
463impl<'a> fmt::Display for CssParseError<'a> {
464    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
465        let start_location = self.location.0.get_line_column_from_error(self.css_string);
466        let end_location = self.location.1.get_line_column_from_error(self.css_string);
467        write!(
468            f,
469            "    start: line {}:{}\r\n    end: line {}:{}\r\n    text: \"{}\"\r\n    reason: {}",
470            start_location.0,
471            start_location.1,
472            end_location.0,
473            end_location.1,
474            self.get_error_string(),
475            self.error,
476        )
477    }
478}
479
480pub fn new_from_str<'a>(css_string: &'a str) -> (Css, Vec<CssParseWarnMsg<'a>>) {
481    let mut tokenizer = Tokenizer::new(css_string);
482    let (stylesheet, warnings) = match new_from_str_inner(css_string, &mut tokenizer) {
483        Ok((stylesheet, warnings)) => (stylesheet, warnings),
484        Err(error) => {
485            let warning = CssParseWarnMsg {
486                warning: CssParseWarnMsgInner::ParseError(error.error),
487                location: error.location,
488            };
489            (Stylesheet::default(), vec![warning])
490        }
491    };
492
493    (
494        Css {
495            stylesheets: vec![stylesheet].into(),
496        },
497        warnings,
498    )
499}
500
501/// Returns the location of where the parser is currently in the document
502fn get_error_location(tokenizer: &Tokenizer) -> ErrorLocation {
503    ErrorLocation {
504        original_pos: tokenizer.pos(),
505    }
506}
507
508#[derive(Debug, Clone, PartialEq)]
509pub enum CssPathParseError<'a> {
510    EmptyPath,
511    /// Invalid item encountered in string (for example a "{", "}")
512    InvalidTokenEncountered(&'a str),
513    UnexpectedEndOfStream(&'a str),
514    SyntaxError(CssSyntaxError),
515    /// The path has to be either `*`, `div`, `p` or something like that
516    NodeTypeTag(NodeTypeTagParseError<'a>),
517    /// Error while parsing a pseudo selector (like `:aldkfja`)
518    PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
519}
520
521impl_from! { NodeTypeTagParseError<'a>, CssPathParseError::NodeTypeTag }
522impl_from! { CssPseudoSelectorParseError<'a>, CssPathParseError::PseudoSelectorParseError }
523
524impl<'a> From<CssSyntaxError> for CssPathParseError<'a> {
525    fn from(e: CssSyntaxError) -> Self {
526        CssPathParseError::SyntaxError(e)
527    }
528}
529
530#[derive(Debug, Clone, PartialEq)]
531pub enum CssPathParseErrorOwned {
532    EmptyPath,
533    InvalidTokenEncountered(String),
534    UnexpectedEndOfStream(String),
535    SyntaxError(CssSyntaxError),
536    NodeTypeTag(NodeTypeTagParseErrorOwned),
537    PseudoSelectorParseError(CssPseudoSelectorParseErrorOwned),
538}
539
540impl<'a> CssPathParseError<'a> {
541    pub fn to_contained(&self) -> CssPathParseErrorOwned {
542        match self {
543            CssPathParseError::EmptyPath => CssPathParseErrorOwned::EmptyPath,
544            CssPathParseError::InvalidTokenEncountered(s) => {
545                CssPathParseErrorOwned::InvalidTokenEncountered(s.to_string())
546            }
547            CssPathParseError::UnexpectedEndOfStream(s) => {
548                CssPathParseErrorOwned::UnexpectedEndOfStream(s.to_string())
549            }
550            CssPathParseError::SyntaxError(e) => CssPathParseErrorOwned::SyntaxError(e.clone()),
551            CssPathParseError::NodeTypeTag(e) => {
552                CssPathParseErrorOwned::NodeTypeTag(e.to_contained())
553            }
554            CssPathParseError::PseudoSelectorParseError(e) => {
555                CssPathParseErrorOwned::PseudoSelectorParseError(e.to_contained())
556            }
557        }
558    }
559}
560
561impl CssPathParseErrorOwned {
562    pub fn to_shared<'a>(&'a self) -> CssPathParseError<'a> {
563        match self {
564            CssPathParseErrorOwned::EmptyPath => CssPathParseError::EmptyPath,
565            CssPathParseErrorOwned::InvalidTokenEncountered(s) => {
566                CssPathParseError::InvalidTokenEncountered(s)
567            }
568            CssPathParseErrorOwned::UnexpectedEndOfStream(s) => {
569                CssPathParseError::UnexpectedEndOfStream(s)
570            }
571            CssPathParseErrorOwned::SyntaxError(e) => CssPathParseError::SyntaxError(e.clone()),
572            CssPathParseErrorOwned::NodeTypeTag(e) => CssPathParseError::NodeTypeTag(e.to_shared()),
573            CssPathParseErrorOwned::PseudoSelectorParseError(e) => {
574                CssPathParseError::PseudoSelectorParseError(e.to_shared())
575            }
576        }
577    }
578}
579
580/// Parses a CSS path from a string (only the path,.no commas allowed)
581///
582/// ```rust
583/// # extern crate azul_css;
584/// # use azul_css::parser2::parse_css_path;
585/// # use azul_css::css::{
586/// #     CssPathSelector::*, CssPathPseudoSelector::*, CssPath,
587/// #     NodeTypeTag::*, CssNthChildSelector::*
588/// # };
589///
590/// assert_eq!(
591///     parse_css_path("* div #my_id > .class:nth-child(2)"),
592///     Ok(CssPath {
593///         selectors: vec![
594///             Global,
595///             Type(Div),
596///             Children,
597///             Id("my_id".to_string().into()),
598///             DirectChildren,
599///             Class("class".to_string().into()),
600///             PseudoSelector(NthChild(Number(2))),
601///         ]
602///         .into()
603///     })
604/// );
605/// ```
606pub fn parse_css_path<'a>(input: &'a str) -> Result<CssPath, CssPathParseError<'a>> {
607    use azul_simplecss::{Combinator, Token};
608
609    let input = input.trim();
610    if input.is_empty() {
611        return Err(CssPathParseError::EmptyPath);
612    }
613
614    let mut tokenizer = Tokenizer::new(input);
615    let mut selectors = Vec::new();
616
617    loop {
618        let token = tokenizer.parse_next()?;
619        match token {
620            Token::UniversalSelector => {
621                selectors.push(CssPathSelector::Global);
622            }
623            Token::TypeSelector(div_type) => {
624                if let Ok(nt) = NodeTypeTag::from_str(div_type) {
625                    selectors.push(CssPathSelector::Type(nt));
626                }
627            }
628            Token::IdSelector(id) => {
629                selectors.push(CssPathSelector::Id(id.to_string().into()));
630            }
631            Token::ClassSelector(class) => {
632                selectors.push(CssPathSelector::Class(class.to_string().into()));
633            }
634            Token::Combinator(Combinator::GreaterThan) => {
635                selectors.push(CssPathSelector::DirectChildren);
636            }
637            Token::Combinator(Combinator::Space) => {
638                selectors.push(CssPathSelector::Children);
639            }
640            Token::Combinator(Combinator::Plus) => {
641                selectors.push(CssPathSelector::AdjacentSibling);
642            }
643            Token::Combinator(Combinator::Tilde) => {
644                selectors.push(CssPathSelector::GeneralSibling);
645            }
646            Token::PseudoClass { selector, value } => {
647                selectors.push(CssPathSelector::PseudoSelector(pseudo_selector_from_str(
648                    selector, value,
649                )?));
650            }
651            Token::EndOfStream => {
652                break;
653            }
654            _ => {
655                return Err(CssPathParseError::InvalidTokenEncountered(input));
656            }
657        }
658    }
659
660    if !selectors.is_empty() {
661        Ok(CssPath {
662            selectors: selectors.into(),
663        })
664    } else {
665        Err(CssPathParseError::EmptyPath)
666    }
667}
668
669#[derive(Debug, Clone, PartialEq)]
670pub struct UnparsedCssRuleBlock<'a> {
671    /// The css path (full selector) of the style ruleset
672    pub path: CssPath,
673    /// `"justify-content" => "center"`
674    pub declarations: BTreeMap<&'a str, (&'a str, (ErrorLocation, ErrorLocation))>,
675    /// Conditions from enclosing @-rules (@media, @lang, etc.)
676    pub conditions: Vec<DynamicSelector>,
677}
678
679/// Owned version of UnparsedCssRuleBlock, with BTreeMap of Strings.
680#[derive(Debug, Clone, PartialEq)]
681pub struct UnparsedCssRuleBlockOwned {
682    pub path: CssPath,
683    pub declarations: BTreeMap<String, (String, (ErrorLocation, ErrorLocation))>,
684    pub conditions: Vec<DynamicSelector>,
685}
686
687impl<'a> UnparsedCssRuleBlock<'a> {
688    pub fn to_contained(&self) -> UnparsedCssRuleBlockOwned {
689        UnparsedCssRuleBlockOwned {
690            path: self.path.clone(),
691            declarations: self
692                .declarations
693                .iter()
694                .map(|(k, (v, loc))| (k.to_string(), (v.to_string(), loc.clone())))
695                .collect(),
696            conditions: self.conditions.clone(),
697        }
698    }
699}
700
701impl UnparsedCssRuleBlockOwned {
702    pub fn to_shared<'a>(&'a self) -> UnparsedCssRuleBlock<'a> {
703        UnparsedCssRuleBlock {
704            path: self.path.clone(),
705            declarations: self
706                .declarations
707                .iter()
708                .map(|(k, (v, loc))| (k.as_str(), (v.as_str(), loc.clone())))
709                .collect(),
710            conditions: self.conditions.clone(),
711        }
712    }
713}
714
715#[derive(Debug, Clone, PartialEq)]
716pub struct CssParseWarnMsg<'a> {
717    pub warning: CssParseWarnMsgInner<'a>,
718    pub location: (ErrorLocation, ErrorLocation),
719}
720
721/// Owned version of CssParseWarnMsg, where warning is the owned type.
722#[derive(Debug, Clone, PartialEq)]
723pub struct CssParseWarnMsgOwned {
724    pub warning: CssParseWarnMsgInnerOwned,
725    pub location: (ErrorLocation, ErrorLocation),
726}
727
728impl<'a> CssParseWarnMsg<'a> {
729    pub fn to_contained(&self) -> CssParseWarnMsgOwned {
730        CssParseWarnMsgOwned {
731            warning: self.warning.to_contained(),
732            location: self.location.clone(),
733        }
734    }
735}
736
737impl CssParseWarnMsgOwned {
738    pub fn to_shared<'a>(&'a self) -> CssParseWarnMsg<'a> {
739        CssParseWarnMsg {
740            warning: self.warning.to_shared(),
741            location: self.location.clone(),
742        }
743    }
744}
745
746#[derive(Debug, Clone, PartialEq)]
747pub enum CssParseWarnMsgInner<'a> {
748    /// Key "blah" isn't (yet) supported, so the parser didn't attempt to parse the value at all
749    UnsupportedKeyValuePair { key: &'a str, value: &'a str },
750    /// A CSS parse error that was encountered but recovered from
751    ParseError(CssParseErrorInner<'a>),
752    /// A rule was skipped due to an error
753    SkippedRule {
754        selector: Option<&'a str>,
755        error: CssParseErrorInner<'a>,
756    },
757    /// A declaration was skipped due to an error
758    SkippedDeclaration {
759        key: &'a str,
760        value: &'a str,
761        error: CssParseErrorInner<'a>,
762    },
763    /// Malformed block structure (mismatched braces, etc.)
764    MalformedStructure { message: &'a str },
765}
766
767#[derive(Debug, Clone, PartialEq)]
768pub enum CssParseWarnMsgInnerOwned {
769    UnsupportedKeyValuePair {
770        key: String,
771        value: String,
772    },
773    ParseError(CssParseErrorInnerOwned),
774    SkippedRule {
775        selector: Option<String>,
776        error: CssParseErrorInnerOwned,
777    },
778    SkippedDeclaration {
779        key: String,
780        value: String,
781        error: CssParseErrorInnerOwned,
782    },
783    MalformedStructure {
784        message: String,
785    },
786}
787
788impl<'a> CssParseWarnMsgInner<'a> {
789    pub fn to_contained(&self) -> CssParseWarnMsgInnerOwned {
790        match self {
791            Self::UnsupportedKeyValuePair { key, value } => {
792                CssParseWarnMsgInnerOwned::UnsupportedKeyValuePair {
793                    key: key.to_string(),
794                    value: value.to_string(),
795                }
796            }
797            Self::ParseError(e) => CssParseWarnMsgInnerOwned::ParseError(e.to_contained()),
798            Self::SkippedRule { selector, error } => CssParseWarnMsgInnerOwned::SkippedRule {
799                selector: selector.map(|s| s.to_string()),
800                error: error.to_contained(),
801            },
802            Self::SkippedDeclaration { key, value, error } => {
803                CssParseWarnMsgInnerOwned::SkippedDeclaration {
804                    key: key.to_string(),
805                    value: value.to_string(),
806                    error: error.to_contained(),
807                }
808            }
809            Self::MalformedStructure { message } => CssParseWarnMsgInnerOwned::MalformedStructure {
810                message: message.to_string(),
811            },
812        }
813    }
814}
815
816impl CssParseWarnMsgInnerOwned {
817    pub fn to_shared<'a>(&'a self) -> CssParseWarnMsgInner<'a> {
818        match self {
819            Self::UnsupportedKeyValuePair { key, value } => {
820                CssParseWarnMsgInner::UnsupportedKeyValuePair { key, value }
821            }
822            Self::ParseError(e) => CssParseWarnMsgInner::ParseError(e.to_shared()),
823            Self::SkippedRule { selector, error } => CssParseWarnMsgInner::SkippedRule {
824                selector: selector.as_deref(),
825                error: error.to_shared(),
826            },
827            Self::SkippedDeclaration { key, value, error } => {
828                CssParseWarnMsgInner::SkippedDeclaration {
829                    key,
830                    value,
831                    error: error.to_shared(),
832                }
833            }
834            Self::MalformedStructure { message } => {
835                CssParseWarnMsgInner::MalformedStructure { message }
836            }
837        }
838    }
839}
840
841impl_display! { CssParseWarnMsgInner<'a>, {
842    UnsupportedKeyValuePair { key, value } => format!("Unsupported CSS property: \"{}: {}\"", key, value),
843    ParseError(e) => format!("Parse error (recoverable): {}", e),
844    SkippedRule { selector, error } => {
845        let sel = selector.unwrap_or("unknown");
846        format!("Skipped rule for selector '{}': {}", sel, error)
847    },
848    SkippedDeclaration { key, value, error } => format!("Skipped declaration '{}:{}': {}", key, value, error),
849    MalformedStructure { message } => format!("Malformed CSS structure: {}", message),
850}}
851
852/// Parses @media conditions from the content following "@media"
853/// Returns a list of DynamicSelectors for the conditions
854fn parse_media_conditions(content: &str) -> Vec<DynamicSelector> {
855    let mut conditions = Vec::new();
856    let content = content.trim();
857
858    // Handle simple media types: "screen", "print", "all"
859    if content.eq_ignore_ascii_case("screen") {
860        conditions.push(DynamicSelector::Media(MediaType::Screen));
861        return conditions;
862    }
863    if content.eq_ignore_ascii_case("print") {
864        conditions.push(DynamicSelector::Media(MediaType::Print));
865        return conditions;
866    }
867    if content.eq_ignore_ascii_case("all") {
868        conditions.push(DynamicSelector::Media(MediaType::All));
869        return conditions;
870    }
871
872    // Parse more complex media queries like "(min-width: 800px)" or "screen and (max-width: 600px)"
873    // Split by "and" for compound queries
874    for part in content.split(" and ") {
875        let part = part.trim();
876
877        // Skip media type keywords in compound queries
878        if part.eq_ignore_ascii_case("screen")
879            || part.eq_ignore_ascii_case("print")
880            || part.eq_ignore_ascii_case("all")
881        {
882            if part.eq_ignore_ascii_case("screen") {
883                conditions.push(DynamicSelector::Media(MediaType::Screen));
884            } else if part.eq_ignore_ascii_case("print") {
885                conditions.push(DynamicSelector::Media(MediaType::Print));
886            }
887            continue;
888        }
889
890        // Parse parenthesized conditions like "(min-width: 800px)"
891        if let Some(inner) = part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
892            if let Some(selector) = parse_media_feature(inner) {
893                conditions.push(selector);
894            }
895        }
896    }
897
898    conditions
899}
900
901/// Parses a single media feature like "min-width: 800px"
902fn parse_media_feature(feature: &str) -> Option<DynamicSelector> {
903    let parts: Vec<&str> = feature.splitn(2, ':').collect();
904    if parts.len() != 2 {
905        // Handle features without values like "orientation: portrait"
906        return None;
907    }
908
909    let key = parts[0].trim();
910    let value = parts[1].trim();
911
912    match key.to_lowercase().as_str() {
913        "min-width" => {
914            if let Some(px) = parse_px_value(value) {
915                return Some(DynamicSelector::ViewportWidth(MinMaxRange::new(
916                    Some(px),
917                    None,
918                )));
919            }
920        }
921        "max-width" => {
922            if let Some(px) = parse_px_value(value) {
923                return Some(DynamicSelector::ViewportWidth(MinMaxRange::new(
924                    None,
925                    Some(px),
926                )));
927            }
928        }
929        "min-height" => {
930            if let Some(px) = parse_px_value(value) {
931                return Some(DynamicSelector::ViewportHeight(MinMaxRange::new(
932                    Some(px),
933                    None,
934                )));
935            }
936        }
937        "max-height" => {
938            if let Some(px) = parse_px_value(value) {
939                return Some(DynamicSelector::ViewportHeight(MinMaxRange::new(
940                    None,
941                    Some(px),
942                )));
943            }
944        }
945        "orientation" => {
946            if value.eq_ignore_ascii_case("portrait") {
947                return Some(DynamicSelector::Orientation(OrientationType::Portrait));
948            } else if value.eq_ignore_ascii_case("landscape") {
949                return Some(DynamicSelector::Orientation(OrientationType::Landscape));
950            }
951        }
952        _ => {}
953    }
954
955    None
956}
957
958/// Parses a pixel value like "800px" and returns the numeric value
959fn parse_px_value(value: &str) -> Option<f32> {
960    let value = value.trim();
961    if let Some(num_str) = value.strip_suffix("px") {
962        num_str.trim().parse::<f32>().ok()
963    } else {
964        // Try parsing as a bare number
965        value.parse::<f32>().ok()
966    }
967}
968
969/// Parses @lang condition from the content following "@lang"
970/// Format: @lang("de-DE") or @lang(de-DE)
971fn parse_lang_condition(content: &str) -> Option<DynamicSelector> {
972    let content = content.trim();
973
974    // Remove parentheses and quotes
975    let lang = content
976        .strip_prefix('(')
977        .and_then(|s| s.strip_suffix(')'))
978        .unwrap_or(content)
979        .trim();
980
981    let lang = lang
982        .strip_prefix('"')
983        .and_then(|s| s.strip_suffix('"'))
984        .or_else(|| lang.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
985        .unwrap_or(lang)
986        .trim();
987
988    if lang.is_empty() {
989        return None;
990    }
991
992    // Use Prefix matching by default (e.g., "de" matches "de-DE", "de-AT")
993    Some(DynamicSelector::Language(LanguageCondition::Prefix(
994        AzString::from(lang.to_string()),
995    )))
996}
997
998/// Parses a CSS string (single-threaded) and returns the parsed rules in blocks
999///
1000/// May return "warning" messages, i.e. messages that just serve as a warning,
1001/// instead of being actual errors. These warnings may be ignored by the caller,
1002/// but can be useful for debugging.
1003fn new_from_str_inner<'a>(
1004    css_string: &'a str,
1005    tokenizer: &mut Tokenizer<'a>,
1006) -> Result<(Stylesheet, Vec<CssParseWarnMsg<'a>>), CssParseError<'a>> {
1007    use azul_simplecss::{Combinator, Token};
1008
1009    let mut css_blocks = Vec::new();
1010    let mut warnings = Vec::new();
1011
1012    let mut parser_in_block = false;
1013    let mut block_nesting = 0_usize;
1014    let mut current_paths = Vec::new();
1015    let mut current_rules = BTreeMap::<&str, (&str, (ErrorLocation, ErrorLocation))>::new();
1016    let mut last_path = Vec::new();
1017    let mut last_error_location = ErrorLocation { original_pos: 0 };
1018
1019    // Stack for tracking @-rule conditions (e.g., @media, @lang)
1020    // Each entry contains the conditions and the nesting level where they were introduced
1021    let mut at_rule_stack: Vec<(Vec<DynamicSelector>, usize)> = Vec::new();
1022    // Pending @-rule that needs to be combined with AtStr
1023    let mut pending_at_rule: Option<&str> = None;
1024
1025    // Safety: limit maximum iterations to prevent infinite loops
1026    // A reasonable limit is 10x the input length (each char could produce at most a few tokens)
1027    let max_iterations = css_string.len().saturating_mul(10).max(1000);
1028    let mut iterations = 0_usize;
1029    let mut last_position = 0_usize;
1030    let mut stuck_count = 0_usize;
1031
1032    loop {
1033        // Safety check 1: Maximum iterations
1034        iterations += 1;
1035        if iterations > max_iterations {
1036            warnings.push(CssParseWarnMsg {
1037                warning: CssParseWarnMsgInner::MalformedStructure {
1038                    message: "Parser iteration limit exceeded - possible infinite loop",
1039                },
1040                location: (last_error_location, get_error_location(tokenizer)),
1041            });
1042            break;
1043        }
1044
1045        // Safety check 2: Detect if parser is stuck (position not advancing)
1046        let current_position = tokenizer.pos();
1047        if current_position == last_position {
1048            stuck_count += 1;
1049            if stuck_count > 10 {
1050                warnings.push(CssParseWarnMsg {
1051                    warning: CssParseWarnMsgInner::MalformedStructure {
1052                        message: "Parser stuck - position not advancing",
1053                    },
1054                    location: (last_error_location, get_error_location(tokenizer)),
1055                });
1056                break;
1057            }
1058        } else {
1059            stuck_count = 0;
1060            last_position = current_position;
1061        }
1062
1063        let token = match tokenizer.parse_next() {
1064            Ok(token) => token,
1065            Err(e) => {
1066                let error_location = get_error_location(tokenizer);
1067                warnings.push(CssParseWarnMsg {
1068                    warning: CssParseWarnMsgInner::ParseError(e.into()),
1069                    location: (last_error_location, error_location),
1070                });
1071                // On error, break to avoid infinite loop - the tokenizer may be stuck
1072                break;
1073            }
1074        };
1075
1076        macro_rules! warn_and_continue {
1077            ($warning:expr) => {{
1078                warnings.push(CssParseWarnMsg {
1079                    warning: $warning,
1080                    location: (last_error_location, get_error_location(tokenizer)),
1081                });
1082                continue;
1083            }};
1084        }
1085
1086        match token {
1087            Token::AtRule(rule_name) => {
1088                // Store the @-rule name to combine with the following AtStr
1089                pending_at_rule = Some(rule_name);
1090            }
1091            Token::AtStr(content) => {
1092                // Combine with pending @-rule
1093                if let Some(rule_name) = pending_at_rule.take() {
1094                    let conditions = match rule_name.to_lowercase().as_str() {
1095                        "media" => parse_media_conditions(content),
1096                        "lang" => parse_lang_condition(content).into_iter().collect(),
1097                        _ => {
1098                            // Unknown @-rule, ignore
1099                            Vec::new()
1100                        }
1101                    };
1102
1103                    if !conditions.is_empty() {
1104                        // Push conditions to stack, will be applied to nested rules
1105                        at_rule_stack.push((conditions, block_nesting + 1));
1106                    }
1107                }
1108            }
1109            Token::BlockStart => {
1110                // Check if this is an @-rule block start (pending_at_rule means we saw @media/@lang)
1111                if pending_at_rule.is_some() {
1112                    // This is an @-rule block without AtStr content (e.g., "@media { ... }")
1113                    pending_at_rule = None;
1114                }
1115
1116                block_nesting += 1;
1117
1118                // Only set parser_in_block for rule blocks (with selectors), not @-rule blocks
1119                if !current_paths.is_empty() || !last_path.is_empty() {
1120                    if parser_in_block {
1121                        warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1122                            message: "Block start inside another block"
1123                        });
1124                    }
1125                    parser_in_block = true;
1126                    if !last_path.is_empty() {
1127                        current_paths.push(last_path.clone());
1128                        last_path.clear();
1129                    }
1130                }
1131            }
1132            Token::Comma => {
1133                if parser_in_block {
1134                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1135                        message: "Comma inside block"
1136                    });
1137                }
1138                if !last_path.is_empty() {
1139                    current_paths.push(last_path.clone());
1140                    last_path.clear();
1141                } else {
1142                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1143                        message: "Empty selector before comma"
1144                    });
1145                }
1146            }
1147            Token::BlockEnd => {
1148                if block_nesting == 0 {
1149                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1150                        message: "Block end without matching block start"
1151                    });
1152                }
1153
1154                // Collect all conditions from the current @-rule stack
1155                let current_conditions: Vec<DynamicSelector> = at_rule_stack
1156                    .iter()
1157                    .flat_map(|(conds, _)| conds.iter().cloned())
1158                    .collect();
1159
1160                // Pop @-rule conditions that are at this nesting level
1161                while let Some((_, level)) = at_rule_stack.last() {
1162                    if *level >= block_nesting {
1163                        at_rule_stack.pop();
1164                    } else {
1165                        break;
1166                    }
1167                }
1168
1169                block_nesting = block_nesting.saturating_sub(1);
1170
1171                // Only process as a rule block if we have selectors (not an @-rule block)
1172                if !current_paths.is_empty() {
1173                    parser_in_block = false;
1174                    css_blocks.extend(current_paths.drain(..).map(|path| UnparsedCssRuleBlock {
1175                        path: CssPath {
1176                            selectors: path.into(),
1177                        },
1178                        declarations: current_rules.clone(),
1179                        conditions: current_conditions.clone(),
1180                    }));
1181                    current_rules.clear();
1182                } else if parser_in_block {
1183                    // We were in a block but no selectors - this is an error
1184                    parser_in_block = false;
1185                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1186                        message: "Block with no selectors"
1187                    });
1188                }
1189                // If !parser_in_block and current_paths is empty, this is closing an @-rule block
1190
1191                last_path.clear();
1192            }
1193            Token::UniversalSelector => {
1194                if parser_in_block {
1195                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1196                        message: "Selector inside block"
1197                    });
1198                }
1199                last_path.push(CssPathSelector::Global);
1200            }
1201            Token::TypeSelector(div_type) => {
1202                if parser_in_block {
1203                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1204                        message: "Selector inside block"
1205                    });
1206                }
1207
1208                match NodeTypeTag::from_str(div_type) {
1209                    Ok(nt) => last_path.push(CssPathSelector::Type(nt)),
1210                    Err(e) => {
1211                        warn_and_continue!(CssParseWarnMsgInner::SkippedRule {
1212                            selector: Some(div_type),
1213                            error: e.into(),
1214                        });
1215                    }
1216                }
1217            }
1218            Token::IdSelector(id) => {
1219                if parser_in_block {
1220                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1221                        message: "Selector inside block"
1222                    });
1223                }
1224                last_path.push(CssPathSelector::Id(id.to_string().into()));
1225            }
1226            Token::ClassSelector(class) => {
1227                if parser_in_block {
1228                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1229                        message: "Selector inside block"
1230                    });
1231                }
1232                last_path.push(CssPathSelector::Class(class.to_string().into()));
1233            }
1234            Token::Combinator(Combinator::GreaterThan) => {
1235                if parser_in_block {
1236                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1237                        message: "Selector inside block"
1238                    });
1239                }
1240                last_path.push(CssPathSelector::DirectChildren);
1241            }
1242            Token::Combinator(Combinator::Space) => {
1243                if parser_in_block {
1244                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1245                        message: "Selector inside block"
1246                    });
1247                }
1248                last_path.push(CssPathSelector::Children);
1249            }
1250            Token::Combinator(Combinator::Plus) => {
1251                if parser_in_block {
1252                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1253                        message: "Selector inside block"
1254                    });
1255                }
1256                last_path.push(CssPathSelector::AdjacentSibling);
1257            }
1258            Token::Combinator(Combinator::Tilde) => {
1259                if parser_in_block {
1260                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1261                        message: "Selector inside block"
1262                    });
1263                }
1264                last_path.push(CssPathSelector::GeneralSibling);
1265            }
1266            Token::PseudoClass { selector, value } => {
1267                if parser_in_block {
1268                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1269                        message: "Selector inside block"
1270                    });
1271                }
1272
1273                match pseudo_selector_from_str(selector, value) {
1274                    Ok(ps) => last_path.push(CssPathSelector::PseudoSelector(ps)),
1275                    Err(e) => {
1276                        warn_and_continue!(CssParseWarnMsgInner::SkippedRule {
1277                            selector: Some(selector),
1278                            error: e.into(),
1279                        });
1280                    }
1281                }
1282            }
1283            Token::Declaration(key, val) => {
1284                if !parser_in_block {
1285                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1286                        message: "Declaration outside block"
1287                    });
1288                }
1289                current_rules.insert(
1290                    key,
1291                    (val, (last_error_location, get_error_location(tokenizer))),
1292                );
1293            }
1294            Token::DeclarationStr(content) => {
1295                // Inside @-rule blocks, selectors appear as DeclarationStr
1296                // Parse the content as a selector if we're in an @-rule block
1297                if !at_rule_stack.is_empty() && !parser_in_block {
1298                    // Try to parse as a selector
1299                    let content = content.trim();
1300                    if let Ok(nt) = NodeTypeTag::from_str(content) {
1301                        last_path.push(CssPathSelector::Type(nt));
1302                    } else if content.starts_with('.') {
1303                        last_path.push(CssPathSelector::Class(content[1..].to_string().into()));
1304                    } else if content.starts_with('#') {
1305                        last_path.push(CssPathSelector::Id(content[1..].to_string().into()));
1306                    } else if content == "*" {
1307                        last_path.push(CssPathSelector::Global);
1308                    }
1309                    // Push current path if we have one
1310                    if !last_path.is_empty() {
1311                        current_paths.push(last_path.clone());
1312                        last_path.clear();
1313                    }
1314                }
1315            }
1316            Token::EndOfStream => {
1317                if block_nesting != 0 {
1318                    warnings.push(CssParseWarnMsg {
1319                        warning: CssParseWarnMsgInner::MalformedStructure {
1320                            message: "Unclosed blocks at end of file",
1321                        },
1322                        location: (last_error_location, get_error_location(tokenizer)),
1323                    });
1324                }
1325                break;
1326            }
1327            _ => { /* Ignore unsupported tokens */ }
1328        }
1329
1330        last_error_location = get_error_location(tokenizer);
1331    }
1332
1333    // Process the collected CSS blocks and convert warnings
1334    let (stylesheet, mut block_warnings) = css_blocks_to_stylesheet(css_blocks, css_string);
1335    warnings.append(&mut block_warnings);
1336
1337    Ok((stylesheet, warnings))
1338}
1339
1340fn css_blocks_to_stylesheet<'a>(
1341    css_blocks: Vec<UnparsedCssRuleBlock<'a>>,
1342    css_string: &'a str,
1343) -> (Stylesheet, Vec<CssParseWarnMsg<'a>>) {
1344    let css_key_map = crate::props::property::get_css_key_map();
1345    let mut warnings = Vec::new();
1346    let mut parsed_css_blocks = Vec::new();
1347
1348    for unparsed_css_block in css_blocks {
1349        let mut declarations = Vec::<CssDeclaration>::new();
1350
1351        for (unparsed_css_key, (unparsed_css_value, location)) in &unparsed_css_block.declarations {
1352            match parse_declaration_resilient(
1353                unparsed_css_key,
1354                unparsed_css_value,
1355                *location,
1356                &css_key_map,
1357            ) {
1358                Ok(decls) => declarations.extend(decls),
1359                Err(e) => {
1360                    warnings.push(CssParseWarnMsg {
1361                        warning: CssParseWarnMsgInner::SkippedDeclaration {
1362                            key: unparsed_css_key,
1363                            value: unparsed_css_value,
1364                            error: e,
1365                        },
1366                        location: *location,
1367                    });
1368                }
1369            }
1370        }
1371
1372        parsed_css_blocks.push(CssRuleBlock {
1373            path: unparsed_css_block.path.into(),
1374            declarations: declarations.into(),
1375            conditions: unparsed_css_block.conditions.into(),
1376        });
1377    }
1378
1379    (
1380        Stylesheet {
1381            rules: parsed_css_blocks.into(),
1382        },
1383        warnings,
1384    )
1385}
1386
1387fn parse_declaration_resilient<'a>(
1388    unparsed_css_key: &'a str,
1389    unparsed_css_value: &'a str,
1390    location: (ErrorLocation, ErrorLocation),
1391    css_key_map: &CssKeyMap,
1392) -> Result<Vec<CssDeclaration>, CssParseErrorInner<'a>> {
1393    let mut declarations = Vec::new();
1394
1395    if let Some(combined_key) = CombinedCssPropertyType::from_str(unparsed_css_key, css_key_map) {
1396        if let Some(css_var) = check_if_value_is_css_var(unparsed_css_value) {
1397            return Err(CssParseErrorInner::VarOnShorthandProperty {
1398                key: combined_key,
1399                value: unparsed_css_value,
1400            });
1401        }
1402
1403        // Attempt to parse combined properties, continue with what succeeds
1404        match parse_combined_css_property(combined_key, unparsed_css_value) {
1405            Ok(parsed_props) => {
1406                declarations.extend(parsed_props.into_iter().map(CssDeclaration::Static));
1407            }
1408            Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
1409        }
1410    } else if let Some(normal_key) = CssPropertyType::from_str(unparsed_css_key, css_key_map) {
1411        if let Some(css_var) = check_if_value_is_css_var(unparsed_css_value) {
1412            let (css_var_id, css_var_default) = css_var?;
1413            match parse_css_property(normal_key, css_var_default) {
1414                Ok(parsed_default) => {
1415                    declarations.push(CssDeclaration::Dynamic(DynamicCssProperty {
1416                        dynamic_id: css_var_id.to_string().into(),
1417                        default_value: parsed_default,
1418                    }));
1419                }
1420                Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
1421            }
1422        } else {
1423            match parse_css_property(normal_key, unparsed_css_value) {
1424                Ok(parsed_value) => {
1425                    declarations.push(CssDeclaration::Static(parsed_value));
1426                }
1427                Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
1428            }
1429        }
1430    } else {
1431        return Err(CssParseErrorInner::UnknownPropertyKey(
1432            unparsed_css_key,
1433            unparsed_css_value,
1434        ));
1435    }
1436
1437    Ok(declarations)
1438}
1439
1440fn unparsed_css_blocks_to_stylesheet<'a>(
1441    css_blocks: Vec<UnparsedCssRuleBlock<'a>>,
1442    css_string: &'a str,
1443) -> Result<(Stylesheet, Vec<CssParseWarnMsg<'a>>), CssParseError<'a>> {
1444    // Actually parse the properties
1445    let css_key_map = crate::props::property::get_css_key_map();
1446
1447    let mut warnings = Vec::new();
1448
1449    let parsed_css_blocks = css_blocks
1450        .into_iter()
1451        .map(|unparsed_css_block| {
1452            let mut declarations = Vec::<CssDeclaration>::new();
1453
1454            for (unparsed_css_key, (unparsed_css_value, location)) in
1455                unparsed_css_block.declarations
1456            {
1457                parse_css_declaration(
1458                    unparsed_css_key,
1459                    unparsed_css_value,
1460                    location,
1461                    &css_key_map,
1462                    &mut warnings,
1463                    &mut declarations,
1464                )
1465                .map_err(|e| CssParseError {
1466                    css_string,
1467                    error: e.into(),
1468                    location,
1469                })?;
1470            }
1471
1472            Ok(CssRuleBlock {
1473                path: unparsed_css_block.path.into(),
1474                declarations: declarations.into(),
1475                conditions: unparsed_css_block.conditions.into(),
1476            })
1477        })
1478        .collect::<Result<Vec<CssRuleBlock>, CssParseError>>()?;
1479
1480    Ok((parsed_css_blocks.into(), warnings))
1481}
1482
1483pub fn parse_css_declaration<'a>(
1484    unparsed_css_key: &'a str,
1485    unparsed_css_value: &'a str,
1486    location: (ErrorLocation, ErrorLocation),
1487    css_key_map: &CssKeyMap,
1488    warnings: &mut Vec<CssParseWarnMsg<'a>>,
1489    declarations: &mut Vec<CssDeclaration>,
1490) -> Result<(), CssParseErrorInner<'a>> {
1491    match parse_declaration_resilient(unparsed_css_key, unparsed_css_value, location, css_key_map) {
1492        Ok(mut decls) => {
1493            declarations.append(&mut decls);
1494            Ok(())
1495        }
1496        Err(e) => {
1497            if let CssParseErrorInner::UnknownPropertyKey(key, val) = &e {
1498                warnings.push(CssParseWarnMsg {
1499                    warning: CssParseWarnMsgInner::UnsupportedKeyValuePair { key, value: val },
1500                    location,
1501                });
1502                Ok(()) // Continue processing despite unknown property
1503            } else {
1504                Err(e) // Propagate other errors
1505            }
1506        }
1507    }
1508}
1509
1510fn check_if_value_is_css_var<'a>(
1511    unparsed_css_value: &'a str,
1512) -> Option<Result<(&'a str, &'a str), CssParseErrorInner<'a>>> {
1513    const DEFAULT_VARIABLE_DEFAULT: &str = "none";
1514
1515    let (_, brace_contents) = parse_parentheses(unparsed_css_value, &["var"]).ok()?;
1516
1517    // value is a CSS variable, i.e. var(--main-bg-color)
1518    Some(match parse_css_variable_brace_contents(brace_contents) {
1519        Some((variable_id, default_value)) => Ok((
1520            variable_id,
1521            default_value.unwrap_or(DEFAULT_VARIABLE_DEFAULT),
1522        )),
1523        None => Err(DynamicCssParseError::InvalidBraceContents(brace_contents).into()),
1524    })
1525}
1526
1527/// Parses the brace contents of a css var, i.e.:
1528///
1529/// ```no_run,ignore
1530/// "--main-bg-col, blue" => (Some("main-bg-col"), Some("blue"))
1531/// "--main-bg-col"       => (Some("main-bg-col"), None)
1532/// ```
1533fn parse_css_variable_brace_contents<'a>(input: &'a str) -> Option<(&'a str, Option<&'a str>)> {
1534    let input = input.trim();
1535
1536    let mut split_comma_iter = input.splitn(2, ",");
1537    let var_name = split_comma_iter.next()?;
1538    let var_name = var_name.trim();
1539
1540    if !var_name.starts_with("--") {
1541        return None; // no proper CSS variable name
1542    }
1543
1544    Some((&var_name[2..], split_comma_iter.next()))
1545}