Skip to main content

azul_css/
parser2.rs

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