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, OsCondition,
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        "dragging" => Ok(CssPathPseudoSelector::Dragging),
345        "drag-over" => Ok(CssPathPseudoSelector::DragOver),
346        "nth-child" => {
347            let value = value.ok_or(CssPseudoSelectorParseError::EmptyNthChild)?;
348            let parsed = parse_nth_child_selector(value)?;
349            Ok(CssPathPseudoSelector::NthChild(parsed))
350        }
351        "lang" => {
352            let lang_value = value.ok_or(CssPseudoSelectorParseError::UnknownSelector(
353                selector, value,
354            ))?;
355            // Remove quotes if present
356            let lang_value = lang_value
357                .trim()
358                .trim_start_matches('"')
359                .trim_end_matches('"')
360                .trim_start_matches('\'')
361                .trim_end_matches('\'')
362                .trim();
363            Ok(CssPathPseudoSelector::Lang(AzString::from(
364                lang_value.to_string(),
365            )))
366        }
367        _ => Err(CssPseudoSelectorParseError::UnknownSelector(
368            selector, value,
369        )),
370    }
371}
372
373/// Parses the inner value of the `:nth-child` selector, including numbers and patterns.
374///
375/// I.e.: `"2n+3"` -> `Pattern { repeat: 2, offset: 3 }`
376fn parse_nth_child_selector<'a>(
377    value: &'a str,
378) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'a>> {
379    let value = value.trim();
380
381    if value.is_empty() {
382        return Err(CssPseudoSelectorParseError::EmptyNthChild);
383    }
384
385    if let Ok(number) = value.parse::<u32>() {
386        return Ok(CssNthChildSelector::Number(number));
387    }
388
389    // If the value is not a number
390    match value.as_ref() {
391        "even" => Ok(CssNthChildSelector::Even),
392        "odd" => Ok(CssNthChildSelector::Odd),
393        other => parse_nth_child_pattern(value),
394    }
395}
396
397/// Parses the pattern between the braces of a "nth-child" (such as "2n+3").
398fn parse_nth_child_pattern<'a>(
399    value: &'a str,
400) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'a>> {
401    use crate::css::CssNthChildPattern;
402
403    let value = value.trim();
404
405    if value.is_empty() {
406        return Err(CssPseudoSelectorParseError::EmptyNthChild);
407    }
408
409    // TODO: Test for "+"
410    let repeat = value
411        .split("n")
412        .next()
413        .ok_or(CssPseudoSelectorParseError::InvalidNthChildPattern(value))?
414        .trim()
415        .parse::<u32>()?;
416
417    // In a "2n+3" form, the first .next() yields the "2n", the second .next() yields the "3"
418    let mut offset_iterator = value.split("+");
419
420    // has to succeed, since the string is verified to not be empty
421    offset_iterator.next().unwrap();
422
423    let offset = match offset_iterator.next() {
424        Some(offset_string) => {
425            let offset_string = offset_string.trim();
426            if offset_string.is_empty() {
427                return Err(CssPseudoSelectorParseError::InvalidNthChildPattern(value));
428            } else {
429                offset_string.parse::<u32>()?
430            }
431        }
432        None => 0,
433    };
434
435    Ok(CssNthChildSelector::Pattern(CssNthChildPattern {
436        pattern_repeat: repeat,
437        offset,
438    }))
439}
440
441#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
442pub struct ErrorLocation {
443    pub original_pos: usize,
444}
445
446impl ErrorLocation {
447    /// Given an error location, returns the (line, column)
448    pub fn get_line_column_from_error(&self, css_string: &str) -> (usize, usize) {
449        let error_location = self.original_pos.saturating_sub(1);
450        let (mut line_number, mut total_characters) = (0, 0);
451
452        for line in css_string[0..error_location].lines() {
453            line_number += 1;
454            total_characters += line.chars().count();
455        }
456
457        // Rust doesn't count "\n" as a character, so we have to add the line number count on top
458        let total_characters = total_characters + line_number;
459        let column_pos = error_location - total_characters.saturating_sub(2);
460
461        (line_number, column_pos)
462    }
463}
464
465impl<'a> fmt::Display for CssParseError<'a> {
466    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
467        let start_location = self.location.0.get_line_column_from_error(self.css_string);
468        let end_location = self.location.1.get_line_column_from_error(self.css_string);
469        write!(
470            f,
471            "    start: line {}:{}\r\n    end: line {}:{}\r\n    text: \"{}\"\r\n    reason: {}",
472            start_location.0,
473            start_location.1,
474            end_location.0,
475            end_location.1,
476            self.get_error_string(),
477            self.error,
478        )
479    }
480}
481
482pub fn new_from_str<'a>(css_string: &'a str) -> (Css, Vec<CssParseWarnMsg<'a>>) {
483    let mut tokenizer = Tokenizer::new(css_string);
484    let (stylesheet, warnings) = match new_from_str_inner(css_string, &mut tokenizer) {
485        Ok((stylesheet, warnings)) => (stylesheet, warnings),
486        Err(error) => {
487            let warning = CssParseWarnMsg {
488                warning: CssParseWarnMsgInner::ParseError(error.error),
489                location: error.location,
490            };
491            (Stylesheet::default(), vec![warning])
492        }
493    };
494
495    (
496        Css {
497            stylesheets: vec![stylesheet].into(),
498        },
499        warnings,
500    )
501}
502
503/// Returns the location of where the parser is currently in the document
504fn get_error_location(tokenizer: &Tokenizer) -> ErrorLocation {
505    ErrorLocation {
506        original_pos: tokenizer.pos(),
507    }
508}
509
510#[derive(Debug, Clone, PartialEq)]
511pub enum CssPathParseError<'a> {
512    EmptyPath,
513    /// Invalid item encountered in string (for example a "{", "}")
514    InvalidTokenEncountered(&'a str),
515    UnexpectedEndOfStream(&'a str),
516    SyntaxError(CssSyntaxError),
517    /// The path has to be either `*`, `div`, `p` or something like that
518    NodeTypeTag(NodeTypeTagParseError<'a>),
519    /// Error while parsing a pseudo selector (like `:aldkfja`)
520    PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
521}
522
523impl_from! { NodeTypeTagParseError<'a>, CssPathParseError::NodeTypeTag }
524impl_from! { CssPseudoSelectorParseError<'a>, CssPathParseError::PseudoSelectorParseError }
525
526impl<'a> From<CssSyntaxError> for CssPathParseError<'a> {
527    fn from(e: CssSyntaxError) -> Self {
528        CssPathParseError::SyntaxError(e)
529    }
530}
531
532#[derive(Debug, Clone, PartialEq)]
533pub enum CssPathParseErrorOwned {
534    EmptyPath,
535    InvalidTokenEncountered(String),
536    UnexpectedEndOfStream(String),
537    SyntaxError(CssSyntaxError),
538    NodeTypeTag(NodeTypeTagParseErrorOwned),
539    PseudoSelectorParseError(CssPseudoSelectorParseErrorOwned),
540}
541
542impl<'a> CssPathParseError<'a> {
543    pub fn to_contained(&self) -> CssPathParseErrorOwned {
544        match self {
545            CssPathParseError::EmptyPath => CssPathParseErrorOwned::EmptyPath,
546            CssPathParseError::InvalidTokenEncountered(s) => {
547                CssPathParseErrorOwned::InvalidTokenEncountered(s.to_string())
548            }
549            CssPathParseError::UnexpectedEndOfStream(s) => {
550                CssPathParseErrorOwned::UnexpectedEndOfStream(s.to_string())
551            }
552            CssPathParseError::SyntaxError(e) => CssPathParseErrorOwned::SyntaxError(e.clone()),
553            CssPathParseError::NodeTypeTag(e) => {
554                CssPathParseErrorOwned::NodeTypeTag(e.to_contained())
555            }
556            CssPathParseError::PseudoSelectorParseError(e) => {
557                CssPathParseErrorOwned::PseudoSelectorParseError(e.to_contained())
558            }
559        }
560    }
561}
562
563impl CssPathParseErrorOwned {
564    pub fn to_shared<'a>(&'a self) -> CssPathParseError<'a> {
565        match self {
566            CssPathParseErrorOwned::EmptyPath => CssPathParseError::EmptyPath,
567            CssPathParseErrorOwned::InvalidTokenEncountered(s) => {
568                CssPathParseError::InvalidTokenEncountered(s)
569            }
570            CssPathParseErrorOwned::UnexpectedEndOfStream(s) => {
571                CssPathParseError::UnexpectedEndOfStream(s)
572            }
573            CssPathParseErrorOwned::SyntaxError(e) => CssPathParseError::SyntaxError(e.clone()),
574            CssPathParseErrorOwned::NodeTypeTag(e) => CssPathParseError::NodeTypeTag(e.to_shared()),
575            CssPathParseErrorOwned::PseudoSelectorParseError(e) => {
576                CssPathParseError::PseudoSelectorParseError(e.to_shared())
577            }
578        }
579    }
580}
581
582/// Parses a CSS path from a string (only the path,.no commas allowed)
583///
584/// ```rust
585/// # extern crate azul_css;
586/// # use azul_css::parser2::parse_css_path;
587/// # use azul_css::css::{
588/// #     CssPathSelector::*, CssPathPseudoSelector::*, CssPath,
589/// #     NodeTypeTag::*, CssNthChildSelector::*
590/// # };
591///
592/// assert_eq!(
593///     parse_css_path("* div #my_id > .class:nth-child(2)"),
594///     Ok(CssPath {
595///         selectors: vec![
596///             Global,
597///             Type(Div),
598///             Children,
599///             Id("my_id".to_string().into()),
600///             DirectChildren,
601///             Class("class".to_string().into()),
602///             PseudoSelector(NthChild(Number(2))),
603///         ]
604///         .into()
605///     })
606/// );
607/// ```
608pub fn parse_css_path<'a>(input: &'a str) -> Result<CssPath, CssPathParseError<'a>> {
609    use azul_simplecss::{Combinator, Token};
610
611    let input = input.trim();
612    if input.is_empty() {
613        return Err(CssPathParseError::EmptyPath);
614    }
615
616    let mut tokenizer = Tokenizer::new(input);
617    let mut selectors = Vec::new();
618
619    loop {
620        let token = tokenizer.parse_next()?;
621        match token {
622            Token::UniversalSelector => {
623                selectors.push(CssPathSelector::Global);
624            }
625            Token::TypeSelector(div_type) => {
626                if let Ok(nt) = NodeTypeTag::from_str(div_type) {
627                    selectors.push(CssPathSelector::Type(nt));
628                }
629            }
630            Token::IdSelector(id) => {
631                selectors.push(CssPathSelector::Id(id.to_string().into()));
632            }
633            Token::ClassSelector(class) => {
634                selectors.push(CssPathSelector::Class(class.to_string().into()));
635            }
636            Token::Combinator(Combinator::GreaterThan) => {
637                selectors.push(CssPathSelector::DirectChildren);
638            }
639            Token::Combinator(Combinator::Space) => {
640                selectors.push(CssPathSelector::Children);
641            }
642            Token::Combinator(Combinator::Plus) => {
643                selectors.push(CssPathSelector::AdjacentSibling);
644            }
645            Token::Combinator(Combinator::Tilde) => {
646                selectors.push(CssPathSelector::GeneralSibling);
647            }
648            Token::PseudoClass { selector, value } => {
649                selectors.push(CssPathSelector::PseudoSelector(pseudo_selector_from_str(
650                    selector, value,
651                )?));
652            }
653            Token::EndOfStream => {
654                break;
655            }
656            _ => {
657                return Err(CssPathParseError::InvalidTokenEncountered(input));
658            }
659        }
660    }
661
662    if !selectors.is_empty() {
663        Ok(CssPath {
664            selectors: selectors.into(),
665        })
666    } else {
667        Err(CssPathParseError::EmptyPath)
668    }
669}
670
671#[derive(Debug, Clone, PartialEq)]
672pub struct UnparsedCssRuleBlock<'a> {
673    /// The css path (full selector) of the style ruleset
674    pub path: CssPath,
675    /// `"justify-content" => "center"`
676    pub declarations: BTreeMap<&'a str, (&'a str, (ErrorLocation, ErrorLocation))>,
677    /// Conditions from enclosing @-rules (@media, @lang, etc.)
678    pub conditions: Vec<DynamicSelector>,
679}
680
681/// Owned version of UnparsedCssRuleBlock, with BTreeMap of Strings.
682#[derive(Debug, Clone, PartialEq)]
683pub struct UnparsedCssRuleBlockOwned {
684    pub path: CssPath,
685    pub declarations: BTreeMap<String, (String, (ErrorLocation, ErrorLocation))>,
686    pub conditions: Vec<DynamicSelector>,
687}
688
689impl<'a> UnparsedCssRuleBlock<'a> {
690    pub fn to_contained(&self) -> UnparsedCssRuleBlockOwned {
691        UnparsedCssRuleBlockOwned {
692            path: self.path.clone(),
693            declarations: self
694                .declarations
695                .iter()
696                .map(|(k, (v, loc))| (k.to_string(), (v.to_string(), loc.clone())))
697                .collect(),
698            conditions: self.conditions.clone(),
699        }
700    }
701}
702
703impl UnparsedCssRuleBlockOwned {
704    pub fn to_shared<'a>(&'a self) -> UnparsedCssRuleBlock<'a> {
705        UnparsedCssRuleBlock {
706            path: self.path.clone(),
707            declarations: self
708                .declarations
709                .iter()
710                .map(|(k, (v, loc))| (k.as_str(), (v.as_str(), loc.clone())))
711                .collect(),
712            conditions: self.conditions.clone(),
713        }
714    }
715}
716
717#[derive(Debug, Clone, PartialEq)]
718pub struct CssParseWarnMsg<'a> {
719    pub warning: CssParseWarnMsgInner<'a>,
720    pub location: (ErrorLocation, ErrorLocation),
721}
722
723/// Owned version of CssParseWarnMsg, where warning is the owned type.
724#[derive(Debug, Clone, PartialEq)]
725pub struct CssParseWarnMsgOwned {
726    pub warning: CssParseWarnMsgInnerOwned,
727    pub location: (ErrorLocation, ErrorLocation),
728}
729
730impl<'a> CssParseWarnMsg<'a> {
731    pub fn to_contained(&self) -> CssParseWarnMsgOwned {
732        CssParseWarnMsgOwned {
733            warning: self.warning.to_contained(),
734            location: self.location.clone(),
735        }
736    }
737}
738
739impl CssParseWarnMsgOwned {
740    pub fn to_shared<'a>(&'a self) -> CssParseWarnMsg<'a> {
741        CssParseWarnMsg {
742            warning: self.warning.to_shared(),
743            location: self.location.clone(),
744        }
745    }
746}
747
748#[derive(Debug, Clone, PartialEq)]
749pub enum CssParseWarnMsgInner<'a> {
750    /// Key "blah" isn't (yet) supported, so the parser didn't attempt to parse the value at all
751    UnsupportedKeyValuePair { key: &'a str, value: &'a str },
752    /// A CSS parse error that was encountered but recovered from
753    ParseError(CssParseErrorInner<'a>),
754    /// A rule was skipped due to an error
755    SkippedRule {
756        selector: Option<&'a str>,
757        error: CssParseErrorInner<'a>,
758    },
759    /// A declaration was skipped due to an error
760    SkippedDeclaration {
761        key: &'a str,
762        value: &'a str,
763        error: CssParseErrorInner<'a>,
764    },
765    /// Malformed block structure (mismatched braces, etc.)
766    MalformedStructure { message: &'a str },
767}
768
769#[derive(Debug, Clone, PartialEq)]
770pub enum CssParseWarnMsgInnerOwned {
771    UnsupportedKeyValuePair {
772        key: String,
773        value: String,
774    },
775    ParseError(CssParseErrorInnerOwned),
776    SkippedRule {
777        selector: Option<String>,
778        error: CssParseErrorInnerOwned,
779    },
780    SkippedDeclaration {
781        key: String,
782        value: String,
783        error: CssParseErrorInnerOwned,
784    },
785    MalformedStructure {
786        message: String,
787    },
788}
789
790impl<'a> CssParseWarnMsgInner<'a> {
791    pub fn to_contained(&self) -> CssParseWarnMsgInnerOwned {
792        match self {
793            Self::UnsupportedKeyValuePair { key, value } => {
794                CssParseWarnMsgInnerOwned::UnsupportedKeyValuePair {
795                    key: key.to_string(),
796                    value: value.to_string(),
797                }
798            }
799            Self::ParseError(e) => CssParseWarnMsgInnerOwned::ParseError(e.to_contained()),
800            Self::SkippedRule { selector, error } => CssParseWarnMsgInnerOwned::SkippedRule {
801                selector: selector.map(|s| s.to_string()),
802                error: error.to_contained(),
803            },
804            Self::SkippedDeclaration { key, value, error } => {
805                CssParseWarnMsgInnerOwned::SkippedDeclaration {
806                    key: key.to_string(),
807                    value: value.to_string(),
808                    error: error.to_contained(),
809                }
810            }
811            Self::MalformedStructure { message } => CssParseWarnMsgInnerOwned::MalformedStructure {
812                message: message.to_string(),
813            },
814        }
815    }
816}
817
818impl CssParseWarnMsgInnerOwned {
819    pub fn to_shared<'a>(&'a self) -> CssParseWarnMsgInner<'a> {
820        match self {
821            Self::UnsupportedKeyValuePair { key, value } => {
822                CssParseWarnMsgInner::UnsupportedKeyValuePair { key, value }
823            }
824            Self::ParseError(e) => CssParseWarnMsgInner::ParseError(e.to_shared()),
825            Self::SkippedRule { selector, error } => CssParseWarnMsgInner::SkippedRule {
826                selector: selector.as_deref(),
827                error: error.to_shared(),
828            },
829            Self::SkippedDeclaration { key, value, error } => {
830                CssParseWarnMsgInner::SkippedDeclaration {
831                    key,
832                    value,
833                    error: error.to_shared(),
834                }
835            }
836            Self::MalformedStructure { message } => {
837                CssParseWarnMsgInner::MalformedStructure { message }
838            }
839        }
840    }
841}
842
843impl_display! { CssParseWarnMsgInner<'a>, {
844    UnsupportedKeyValuePair { key, value } => format!("Unsupported CSS property: \"{}: {}\"", key, value),
845    ParseError(e) => format!("Parse error (recoverable): {}", e),
846    SkippedRule { selector, error } => {
847        let sel = selector.unwrap_or("unknown");
848        format!("Skipped rule for selector '{}': {}", sel, error)
849    },
850    SkippedDeclaration { key, value, error } => format!("Skipped declaration '{}:{}': {}", key, value, error),
851    MalformedStructure { message } => format!("Malformed CSS structure: {}", message),
852}}
853
854/// Parses @media conditions from the content following "@media"
855/// Returns a list of DynamicSelectors for the conditions
856fn parse_media_conditions(content: &str) -> Vec<DynamicSelector> {
857    let mut conditions = Vec::new();
858    let content = content.trim();
859
860    // Handle simple media types: "screen", "print", "all"
861    if content.eq_ignore_ascii_case("screen") {
862        conditions.push(DynamicSelector::Media(MediaType::Screen));
863        return conditions;
864    }
865    if content.eq_ignore_ascii_case("print") {
866        conditions.push(DynamicSelector::Media(MediaType::Print));
867        return conditions;
868    }
869    if content.eq_ignore_ascii_case("all") {
870        conditions.push(DynamicSelector::Media(MediaType::All));
871        return conditions;
872    }
873
874    // Parse more complex media queries like "(min-width: 800px)" or "screen and (max-width: 600px)"
875    // Split by "and" for compound queries
876    for part in content.split(" and ") {
877        let part = part.trim();
878
879        // Skip media type keywords in compound queries
880        if part.eq_ignore_ascii_case("screen")
881            || part.eq_ignore_ascii_case("print")
882            || part.eq_ignore_ascii_case("all")
883        {
884            if part.eq_ignore_ascii_case("screen") {
885                conditions.push(DynamicSelector::Media(MediaType::Screen));
886            } else if part.eq_ignore_ascii_case("print") {
887                conditions.push(DynamicSelector::Media(MediaType::Print));
888            }
889            continue;
890        }
891
892        // Parse parenthesized conditions like "(min-width: 800px)"
893        if let Some(inner) = part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
894            if let Some(selector) = parse_media_feature(inner) {
895                conditions.push(selector);
896            }
897        }
898    }
899
900    conditions
901}
902
903/// Parses a single media feature like "min-width: 800px"
904fn parse_media_feature(feature: &str) -> Option<DynamicSelector> {
905    let parts: Vec<&str> = feature.splitn(2, ':').collect();
906    if parts.len() != 2 {
907        // Handle features without values like "orientation: portrait"
908        return None;
909    }
910
911    let key = parts[0].trim();
912    let value = parts[1].trim();
913
914    match key.to_lowercase().as_str() {
915        "min-width" => {
916            if let Some(px) = parse_px_value(value) {
917                return Some(DynamicSelector::ViewportWidth(MinMaxRange::new(
918                    Some(px),
919                    None,
920                )));
921            }
922        }
923        "max-width" => {
924            if let Some(px) = parse_px_value(value) {
925                return Some(DynamicSelector::ViewportWidth(MinMaxRange::new(
926                    None,
927                    Some(px),
928                )));
929            }
930        }
931        "min-height" => {
932            if let Some(px) = parse_px_value(value) {
933                return Some(DynamicSelector::ViewportHeight(MinMaxRange::new(
934                    Some(px),
935                    None,
936                )));
937            }
938        }
939        "max-height" => {
940            if let Some(px) = parse_px_value(value) {
941                return Some(DynamicSelector::ViewportHeight(MinMaxRange::new(
942                    None,
943                    Some(px),
944                )));
945            }
946        }
947        "orientation" => {
948            if value.eq_ignore_ascii_case("portrait") {
949                return Some(DynamicSelector::Orientation(OrientationType::Portrait));
950            } else if value.eq_ignore_ascii_case("landscape") {
951                return Some(DynamicSelector::Orientation(OrientationType::Landscape));
952            }
953        }
954        _ => {}
955    }
956
957    None
958}
959
960/// Parses a pixel value like "800px" and returns the numeric value
961fn parse_px_value(value: &str) -> Option<f32> {
962    let value = value.trim();
963    if let Some(num_str) = value.strip_suffix("px") {
964        num_str.trim().parse::<f32>().ok()
965    } else {
966        // Try parsing as a bare number
967        value.parse::<f32>().ok()
968    }
969}
970
971/// Parses @lang condition from the content following "@lang"
972/// Format: @lang("de-DE") or @lang(de-DE)
973fn parse_lang_condition(content: &str) -> Option<DynamicSelector> {
974    let content = content.trim();
975
976    // Remove parentheses and quotes
977    let lang = content
978        .strip_prefix('(')
979        .and_then(|s| s.strip_suffix(')'))
980        .unwrap_or(content)
981        .trim();
982
983    let lang = lang
984        .strip_prefix('"')
985        .and_then(|s| s.strip_suffix('"'))
986        .or_else(|| lang.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
987        .unwrap_or(lang)
988        .trim();
989
990    if lang.is_empty() {
991        return None;
992    }
993
994    // Use Prefix matching by default (e.g., "de" matches "de-DE", "de-AT")
995    Some(DynamicSelector::Language(LanguageCondition::Prefix(
996        AzString::from(lang.to_string()),
997    )))
998}
999
1000/// Parses @os condition from the content following "@os"
1001/// Format: @os(linux) or @os("windows") or @os(macos)
1002fn parse_os_condition(content: &str) -> Option<DynamicSelector> {
1003    let content = content.trim();
1004
1005    // Remove parentheses and quotes
1006    let os = content
1007        .strip_prefix('(')
1008        .and_then(|s| s.strip_suffix(')'))
1009        .unwrap_or(content)
1010        .trim();
1011
1012    let os = os
1013        .strip_prefix('"')
1014        .and_then(|s| s.strip_suffix('"'))
1015        .or_else(|| os.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1016        .unwrap_or(os)
1017        .trim();
1018
1019    if os.is_empty() {
1020        return None;
1021    }
1022
1023    // Parse OS name (case-insensitive)
1024    let condition = match os.to_lowercase().as_str() {
1025        "linux" => OsCondition::Linux,
1026        "windows" | "win" => OsCondition::Windows,
1027        "macos" | "mac" | "osx" => OsCondition::MacOS,
1028        "ios" => OsCondition::IOS,
1029        "android" => OsCondition::Android,
1030        "apple" => OsCondition::Apple, // macOS + iOS
1031        "web" | "wasm" => OsCondition::Web,
1032        "*" | "any" | "all" => OsCondition::Any,
1033        _ => return None, // Unknown OS
1034    };
1035
1036    Some(DynamicSelector::Os(condition))
1037}
1038
1039/// Parses a CSS string (single-threaded) and returns the parsed rules in blocks
1040///
1041/// May return "warning" messages, i.e. messages that just serve as a warning,
1042/// instead of being actual errors. These warnings may be ignored by the caller,
1043/// but can be useful for debugging.
1044fn new_from_str_inner<'a>(
1045    css_string: &'a str,
1046    tokenizer: &mut Tokenizer<'a>,
1047) -> Result<(Stylesheet, Vec<CssParseWarnMsg<'a>>), CssParseError<'a>> {
1048    use azul_simplecss::{Combinator, Token};
1049
1050    let mut css_blocks = Vec::new();
1051    let mut warnings = Vec::new();
1052
1053    let mut block_nesting = 0_usize;
1054    let mut last_path: Vec<CssPathSelector> = Vec::new();
1055    let mut last_error_location = ErrorLocation { original_pos: 0 };
1056
1057    // Stack for tracking @-rule conditions (e.g., @media, @lang, @os)
1058    // Each entry contains the conditions and the nesting level where they were introduced
1059    let mut at_rule_stack: Vec<(Vec<DynamicSelector>, usize)> = Vec::new();
1060    // Pending @-rule that needs to be combined with AtStr tokens
1061    let mut pending_at_rule: Option<&str> = None;
1062    // Collect multiple AtStr tokens (e.g., "screen", "(min-width: 800px)" for compound media queries)
1063    let mut pending_at_str_parts: Vec<String> = Vec::new();
1064
1065    // Stack for nested selectors
1066    // Each entry: (parent_paths, declarations, nesting_level)
1067    // parent_paths: all accumulated paths at this level (for comma-separated selectors)
1068    // declarations: current declarations at this level
1069    struct NestingLevel<'a> {
1070        paths: Vec<Vec<CssPathSelector>>,
1071        declarations: BTreeMap<&'a str, (&'a str, (ErrorLocation, ErrorLocation))>,
1072        nesting_level: usize,
1073    }
1074    let mut nesting_stack: Vec<NestingLevel<'a>> = Vec::new();
1075    // Current accumulated paths before BlockStart
1076    let mut current_paths: Vec<Vec<CssPathSelector>> = Vec::new();
1077    // Current declarations at current level
1078    let mut current_declarations: BTreeMap<&str, (&str, (ErrorLocation, ErrorLocation))> = BTreeMap::new();
1079
1080    // Safety: limit maximum iterations to prevent infinite loops
1081    // A reasonable limit is 10x the input length (each char could produce at most a few tokens)
1082    let max_iterations = css_string.len().saturating_mul(10).max(1000);
1083    let mut iterations = 0_usize;
1084    let mut last_position = 0_usize;
1085    let mut stuck_count = 0_usize;
1086
1087    loop {
1088        // Safety check 1: Maximum iterations
1089        iterations += 1;
1090        if iterations > max_iterations {
1091            warnings.push(CssParseWarnMsg {
1092                warning: CssParseWarnMsgInner::MalformedStructure {
1093                    message: "Parser iteration limit exceeded - possible infinite loop",
1094                },
1095                location: (last_error_location, get_error_location(tokenizer)),
1096            });
1097            break;
1098        }
1099
1100        // Safety check 2: Detect if parser is stuck (position not advancing)
1101        let current_position = tokenizer.pos();
1102        if current_position == last_position {
1103            stuck_count += 1;
1104            if stuck_count > 10 {
1105                warnings.push(CssParseWarnMsg {
1106                    warning: CssParseWarnMsgInner::MalformedStructure {
1107                        message: "Parser stuck - position not advancing",
1108                    },
1109                    location: (last_error_location, get_error_location(tokenizer)),
1110                });
1111                break;
1112            }
1113        } else {
1114            stuck_count = 0;
1115            last_position = current_position;
1116        }
1117
1118        let token = match tokenizer.parse_next() {
1119            Ok(token) => token,
1120            Err(e) => {
1121                let error_location = get_error_location(tokenizer);
1122                warnings.push(CssParseWarnMsg {
1123                    warning: CssParseWarnMsgInner::ParseError(e.into()),
1124                    location: (last_error_location, error_location),
1125                });
1126                // On error, break to avoid infinite loop - the tokenizer may be stuck
1127                break;
1128            }
1129        };
1130
1131        macro_rules! warn_and_continue {
1132            ($warning:expr) => {{
1133                warnings.push(CssParseWarnMsg {
1134                    warning: $warning,
1135                    location: (last_error_location, get_error_location(tokenizer)),
1136                });
1137                continue;
1138            }};
1139        }
1140
1141        // Helper: get parent paths from nesting stack (if any)
1142        fn get_parent_paths(nesting_stack: &[NestingLevel<'_>]) -> Vec<Vec<CssPathSelector>> {
1143            if let Some(parent) = nesting_stack.last() {
1144                parent.paths.clone()
1145            } else {
1146                Vec::new()
1147            }
1148        }
1149
1150        // Helper: combine parent path with child selector for nesting
1151        // For .button { :hover { } } -> .button:hover
1152        // For .outer { .inner { } } -> .outer .inner (with Children combinator)
1153        fn combine_paths(
1154            parent_paths: &[Vec<CssPathSelector>],
1155            child_path: &[CssPathSelector],
1156            is_pseudo_only: bool,
1157        ) -> Vec<Vec<CssPathSelector>> {
1158            if parent_paths.is_empty() {
1159                vec![child_path.to_vec()]
1160            } else {
1161                parent_paths
1162                    .iter()
1163                    .map(|parent| {
1164                        let mut combined = parent.clone();
1165                        if !is_pseudo_only && !child_path.is_empty() {
1166                            // Add implicit descendant combinator for non-pseudo selectors
1167                            combined.push(CssPathSelector::Children);
1168                        }
1169                        combined.extend(child_path.iter().cloned());
1170                        combined
1171                    })
1172                    .collect()
1173            }
1174        }
1175
1176        match token {
1177            Token::AtRule(rule_name) => {
1178                // Store the @-rule name to combine with the following AtStr tokens
1179                pending_at_rule = Some(rule_name);
1180                pending_at_str_parts.clear();
1181            }
1182            Token::AtStr(content) => {
1183                // Collect AtStr tokens until we see BlockStart
1184                if pending_at_rule.is_some() {
1185                    // Skip "and" keyword, it's just a separator
1186                    if !content.eq_ignore_ascii_case("and") {
1187                        pending_at_str_parts.push(content.to_string());
1188                    }
1189                }
1190            }
1191            Token::BlockStart => {
1192                // Process pending @-rule with all collected AtStr parts
1193                if let Some(rule_name) = pending_at_rule.take() {
1194                    let combined_content = pending_at_str_parts.join(" and ");
1195                    pending_at_str_parts.clear();
1196                    
1197                    let conditions = match rule_name.to_lowercase().as_str() {
1198                        "media" => parse_media_conditions(&combined_content),
1199                        "lang" => parse_lang_condition(&combined_content).into_iter().collect(),
1200                        "os" => parse_os_condition(&combined_content).into_iter().collect(),
1201                        _ => {
1202                            // Unknown @-rule, ignore
1203                            Vec::new()
1204                        }
1205                    };
1206
1207                    if !conditions.is_empty() {
1208                        // Push conditions to stack, will be applied to nested rules
1209                        at_rule_stack.push((conditions, block_nesting + 1));
1210                    }
1211                }
1212
1213                block_nesting += 1;
1214
1215                // If we have a selector, push current state onto nesting stack
1216                if !current_paths.is_empty() || !last_path.is_empty() {
1217                    // Finalize current_paths with last_path
1218                    if !last_path.is_empty() {
1219                        current_paths.push(last_path.clone());
1220                        last_path.clear();
1221                    }
1222
1223                    // Get parent paths and combine with current paths
1224                    let parent_paths = get_parent_paths(&nesting_stack);
1225                    let combined_paths: Vec<Vec<CssPathSelector>> = if parent_paths.is_empty() {
1226                        current_paths.clone()
1227                    } else {
1228                        // Combine each parent path with each current path
1229                        let mut result = Vec::new();
1230                        for parent in &parent_paths {
1231                            for child in &current_paths {
1232                                // Check if child starts with pseudo-selector
1233                                let is_pseudo_only = child.first().map(|s| matches!(s, CssPathSelector::PseudoSelector(_))).unwrap_or(false);
1234                                let mut combined = parent.clone();
1235                                if !is_pseudo_only && !child.is_empty() {
1236                                    combined.push(CssPathSelector::Children);
1237                                }
1238                                combined.extend(child.iter().cloned());
1239                                result.push(combined);
1240                            }
1241                        }
1242                        result
1243                    };
1244
1245                    // Push to nesting stack
1246                    nesting_stack.push(NestingLevel {
1247                        paths: combined_paths,
1248                        declarations: std::mem::take(&mut current_declarations),
1249                        nesting_level: block_nesting,
1250                    });
1251                    current_paths.clear();
1252                }
1253            }
1254            Token::Comma => {
1255                // Comma separates selectors
1256                if !last_path.is_empty() {
1257                    current_paths.push(last_path.clone());
1258                    last_path.clear();
1259                }
1260            }
1261            Token::BlockEnd => {
1262                if block_nesting == 0 {
1263                    warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1264                        message: "Block end without matching block start"
1265                    });
1266                }
1267
1268                // Collect all conditions from the current @-rule stack
1269                let current_conditions: Vec<DynamicSelector> = at_rule_stack
1270                    .iter()
1271                    .flat_map(|(conds, _)| conds.iter().cloned())
1272                    .collect();
1273
1274                // Pop @-rule conditions that are at this nesting level
1275                while let Some((_, level)) = at_rule_stack.last() {
1276                    if *level >= block_nesting {
1277                        at_rule_stack.pop();
1278                    } else {
1279                        break;
1280                    }
1281                }
1282
1283                block_nesting = block_nesting.saturating_sub(1);
1284
1285                // Pop from nesting stack if we have one
1286                if let Some(level) = nesting_stack.pop() {
1287                    // Emit CSS blocks for all paths at this level
1288                    if !level.paths.is_empty() && !current_declarations.is_empty() {
1289                        css_blocks.extend(level.paths.iter().map(|path| UnparsedCssRuleBlock {
1290                            path: CssPath {
1291                                selectors: path.clone().into(),
1292                            },
1293                            declarations: current_declarations.clone(),
1294                            conditions: current_conditions.clone(),
1295                        }));
1296                    }
1297                    // Restore parent declarations
1298                    current_declarations = level.declarations;
1299                }
1300
1301                last_path.clear();
1302                current_paths.clear();
1303            }
1304            Token::UniversalSelector => {
1305                last_path.push(CssPathSelector::Global);
1306            }
1307            Token::TypeSelector(div_type) => {
1308                match NodeTypeTag::from_str(div_type) {
1309                    Ok(nt) => last_path.push(CssPathSelector::Type(nt)),
1310                    Err(e) => {
1311                        warn_and_continue!(CssParseWarnMsgInner::SkippedRule {
1312                            selector: Some(div_type),
1313                            error: e.into(),
1314                        });
1315                    }
1316                }
1317            }
1318            Token::IdSelector(id) => {
1319                last_path.push(CssPathSelector::Id(id.to_string().into()));
1320            }
1321            Token::ClassSelector(class) => {
1322                last_path.push(CssPathSelector::Class(class.to_string().into()));
1323            }
1324            Token::Combinator(Combinator::GreaterThan) => {
1325                last_path.push(CssPathSelector::DirectChildren);
1326            }
1327            Token::Combinator(Combinator::Space) => {
1328                last_path.push(CssPathSelector::Children);
1329            }
1330            Token::Combinator(Combinator::Plus) => {
1331                last_path.push(CssPathSelector::AdjacentSibling);
1332            }
1333            Token::Combinator(Combinator::Tilde) => {
1334                last_path.push(CssPathSelector::GeneralSibling);
1335            }
1336            Token::PseudoClass { selector, value } | Token::DoublePseudoClass { selector, value } => {
1337                match pseudo_selector_from_str(selector, value) {
1338                    Ok(ps) => last_path.push(CssPathSelector::PseudoSelector(ps)),
1339                    Err(e) => {
1340                        warn_and_continue!(CssParseWarnMsgInner::SkippedRule {
1341                            selector: Some(selector),
1342                            error: e.into(),
1343                        });
1344                    }
1345                }
1346            }
1347            Token::AttributeSelector(attr) => {
1348                // Parse attribute selector - for now just store as-is
1349                // TODO: properly parse attribute selectors
1350                last_path.push(CssPathSelector::Class(format!("[{}]", attr).into()));
1351            }
1352            Token::Declaration(key, val) => {
1353                current_declarations.insert(
1354                    key,
1355                    (val, (last_error_location, get_error_location(tokenizer))),
1356                );
1357            }
1358            Token::EndOfStream => {
1359                if block_nesting != 0 {
1360                    warnings.push(CssParseWarnMsg {
1361                        warning: CssParseWarnMsgInner::MalformedStructure {
1362                            message: "Unclosed blocks at end of file",
1363                        },
1364                        location: (last_error_location, get_error_location(tokenizer)),
1365                    });
1366                }
1367                break;
1368            }
1369            _ => { /* Ignore unsupported tokens */ }
1370        }
1371
1372        last_error_location = get_error_location(tokenizer);
1373    }
1374
1375    // Process the collected CSS blocks and convert warnings
1376    let (stylesheet, mut block_warnings) = css_blocks_to_stylesheet(css_blocks, css_string);
1377    warnings.append(&mut block_warnings);
1378
1379    Ok((stylesheet, warnings))
1380}
1381
1382fn css_blocks_to_stylesheet<'a>(
1383    css_blocks: Vec<UnparsedCssRuleBlock<'a>>,
1384    css_string: &'a str,
1385) -> (Stylesheet, Vec<CssParseWarnMsg<'a>>) {
1386    let css_key_map = crate::props::property::get_css_key_map();
1387    let mut warnings = Vec::new();
1388    let mut parsed_css_blocks = Vec::new();
1389
1390    for unparsed_css_block in css_blocks {
1391        let mut declarations = Vec::<CssDeclaration>::new();
1392
1393        for (unparsed_css_key, (unparsed_css_value, location)) in &unparsed_css_block.declarations {
1394            match parse_declaration_resilient(
1395                unparsed_css_key,
1396                unparsed_css_value,
1397                *location,
1398                &css_key_map,
1399            ) {
1400                Ok(decls) => declarations.extend(decls),
1401                Err(e) => {
1402                    warnings.push(CssParseWarnMsg {
1403                        warning: CssParseWarnMsgInner::SkippedDeclaration {
1404                            key: unparsed_css_key,
1405                            value: unparsed_css_value,
1406                            error: e,
1407                        },
1408                        location: *location,
1409                    });
1410                }
1411            }
1412        }
1413
1414        parsed_css_blocks.push(CssRuleBlock {
1415            path: unparsed_css_block.path.into(),
1416            declarations: declarations.into(),
1417            conditions: unparsed_css_block.conditions.into(),
1418        });
1419    }
1420
1421    (
1422        Stylesheet {
1423            rules: parsed_css_blocks.into(),
1424        },
1425        warnings,
1426    )
1427}
1428
1429fn parse_declaration_resilient<'a>(
1430    unparsed_css_key: &'a str,
1431    unparsed_css_value: &'a str,
1432    location: (ErrorLocation, ErrorLocation),
1433    css_key_map: &CssKeyMap,
1434) -> Result<Vec<CssDeclaration>, CssParseErrorInner<'a>> {
1435    let mut declarations = Vec::new();
1436
1437    if let Some(combined_key) = CombinedCssPropertyType::from_str(unparsed_css_key, css_key_map) {
1438        if let Some(css_var) = check_if_value_is_css_var(unparsed_css_value) {
1439            return Err(CssParseErrorInner::VarOnShorthandProperty {
1440                key: combined_key,
1441                value: unparsed_css_value,
1442            });
1443        }
1444
1445        // Attempt to parse combined properties, continue with what succeeds
1446        match parse_combined_css_property(combined_key, unparsed_css_value) {
1447            Ok(parsed_props) => {
1448                declarations.extend(parsed_props.into_iter().map(CssDeclaration::Static));
1449            }
1450            Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
1451        }
1452    } else if let Some(normal_key) = CssPropertyType::from_str(unparsed_css_key, css_key_map) {
1453        if let Some(css_var) = check_if_value_is_css_var(unparsed_css_value) {
1454            let (css_var_id, css_var_default) = css_var?;
1455            match parse_css_property(normal_key, css_var_default) {
1456                Ok(parsed_default) => {
1457                    declarations.push(CssDeclaration::Dynamic(DynamicCssProperty {
1458                        dynamic_id: css_var_id.to_string().into(),
1459                        default_value: parsed_default,
1460                    }));
1461                }
1462                Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
1463            }
1464        } else {
1465            match parse_css_property(normal_key, unparsed_css_value) {
1466                Ok(parsed_value) => {
1467                    declarations.push(CssDeclaration::Static(parsed_value));
1468                }
1469                Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
1470            }
1471        }
1472    } else {
1473        return Err(CssParseErrorInner::UnknownPropertyKey(
1474            unparsed_css_key,
1475            unparsed_css_value,
1476        ));
1477    }
1478
1479    Ok(declarations)
1480}
1481
1482fn unparsed_css_blocks_to_stylesheet<'a>(
1483    css_blocks: Vec<UnparsedCssRuleBlock<'a>>,
1484    css_string: &'a str,
1485) -> Result<(Stylesheet, Vec<CssParseWarnMsg<'a>>), CssParseError<'a>> {
1486    // Actually parse the properties
1487    let css_key_map = crate::props::property::get_css_key_map();
1488
1489    let mut warnings = Vec::new();
1490
1491    let parsed_css_blocks = css_blocks
1492        .into_iter()
1493        .map(|unparsed_css_block| {
1494            let mut declarations = Vec::<CssDeclaration>::new();
1495
1496            for (unparsed_css_key, (unparsed_css_value, location)) in
1497                unparsed_css_block.declarations
1498            {
1499                parse_css_declaration(
1500                    unparsed_css_key,
1501                    unparsed_css_value,
1502                    location,
1503                    &css_key_map,
1504                    &mut warnings,
1505                    &mut declarations,
1506                )
1507                .map_err(|e| CssParseError {
1508                    css_string,
1509                    error: e.into(),
1510                    location,
1511                })?;
1512            }
1513
1514            Ok(CssRuleBlock {
1515                path: unparsed_css_block.path.into(),
1516                declarations: declarations.into(),
1517                conditions: unparsed_css_block.conditions.into(),
1518            })
1519        })
1520        .collect::<Result<Vec<CssRuleBlock>, CssParseError>>()?;
1521
1522    Ok((parsed_css_blocks.into(), warnings))
1523}
1524
1525pub fn parse_css_declaration<'a>(
1526    unparsed_css_key: &'a str,
1527    unparsed_css_value: &'a str,
1528    location: (ErrorLocation, ErrorLocation),
1529    css_key_map: &CssKeyMap,
1530    warnings: &mut Vec<CssParseWarnMsg<'a>>,
1531    declarations: &mut Vec<CssDeclaration>,
1532) -> Result<(), CssParseErrorInner<'a>> {
1533    match parse_declaration_resilient(unparsed_css_key, unparsed_css_value, location, css_key_map) {
1534        Ok(mut decls) => {
1535            declarations.append(&mut decls);
1536            Ok(())
1537        }
1538        Err(e) => {
1539            if let CssParseErrorInner::UnknownPropertyKey(key, val) = &e {
1540                warnings.push(CssParseWarnMsg {
1541                    warning: CssParseWarnMsgInner::UnsupportedKeyValuePair { key, value: val },
1542                    location,
1543                });
1544                Ok(()) // Continue processing despite unknown property
1545            } else {
1546                Err(e) // Propagate other errors
1547            }
1548        }
1549    }
1550}
1551
1552fn check_if_value_is_css_var<'a>(
1553    unparsed_css_value: &'a str,
1554) -> Option<Result<(&'a str, &'a str), CssParseErrorInner<'a>>> {
1555    const DEFAULT_VARIABLE_DEFAULT: &str = "none";
1556
1557    let (_, brace_contents) = parse_parentheses(unparsed_css_value, &["var"]).ok()?;
1558
1559    // value is a CSS variable, i.e. var(--main-bg-color)
1560    Some(match parse_css_variable_brace_contents(brace_contents) {
1561        Some((variable_id, default_value)) => Ok((
1562            variable_id,
1563            default_value.unwrap_or(DEFAULT_VARIABLE_DEFAULT),
1564        )),
1565        None => Err(DynamicCssParseError::InvalidBraceContents(brace_contents).into()),
1566    })
1567}
1568
1569/// Parses the brace contents of a css var, i.e.:
1570///
1571/// ```no_run,ignore
1572/// "--main-bg-col, blue" => (Some("main-bg-col"), Some("blue"))
1573/// "--main-bg-col"       => (Some("main-bg-col"), None)
1574/// ```
1575fn parse_css_variable_brace_contents<'a>(input: &'a str) -> Option<(&'a str, Option<&'a str>)> {
1576    let input = input.trim();
1577
1578    let mut split_comma_iter = input.splitn(2, ",");
1579    let var_name = split_comma_iter.next()?;
1580    let var_name = var_name.trim();
1581
1582    if !var_name.starts_with("--") {
1583        return None; // no proper CSS variable name
1584    }
1585
1586    Some((&var_name[2..], split_comma_iter.next()))
1587}