azul_css_parser/
css.rs

1//! High-level types and functions related to CSS parsing
2use std::{
3    num::ParseIntError,
4    fmt,
5    collections::HashMap,
6};
7pub use azul_simplecss::Error as CssSyntaxError;
8use azul_simplecss::Tokenizer;
9
10use crate::css_parser;
11pub use crate::css_parser::CssParsingError;
12use azul_css::{
13    Css, CssDeclaration, Stylesheet, DynamicCssProperty,
14    CssPropertyType, CssRuleBlock, CssPath, CssPathSelector,
15    CssNthChildSelector, CssPathPseudoSelector, CssNthChildSelector::*,
16    NodeTypePath, NodeTypePathParseError, CombinedCssPropertyType, CssKeyMap,
17};
18
19/// Error that can happen during the parsing of a CSS value
20#[derive(Debug, Clone, PartialEq)]
21pub struct CssParseError<'a> {
22    pub css_string: &'a str,
23    pub error: CssParseErrorInner<'a>,
24    pub location: (ErrorLocation, ErrorLocation),
25}
26
27impl<'a> CssParseError<'a> {
28    /// Returns the string between the (start, end) location
29    pub fn get_error_string(&self) -> &'a str {
30        let (start, end) = (self.location.0.original_pos, self.location.1.original_pos);
31        let s = &self.css_string[start..end];
32        s.trim()
33    }
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub enum CssParseErrorInner<'a> {
38    /// A hard error in the CSS syntax
39    ParseError(CssSyntaxError),
40    /// Braces are not balanced properly
41    UnclosedBlock,
42    /// Invalid syntax, such as `#div { #div: "my-value" }`
43    MalformedCss,
44    /// Error parsing dynamic CSS property, such as
45    /// `#div { width: {{ my_id }} /* no default case */ }`
46    DynamicCssParseError(DynamicCssParseError<'a>),
47    /// Error while parsing a pseudo selector (like `:aldkfja`)
48    PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
49    /// The path has to be either `*`, `div`, `p` or something like that
50    NodeTypePath(NodeTypePathParseError<'a>),
51    /// A certain property has an unknown key, for example: `alsdfkj: 500px` = `unknown CSS key "alsdfkj: 500px"`
52    UnknownPropertyKey(&'a str, &'a str),
53    /// `var()` can't be used on properties that expand to multiple values, since they would be ambigouus
54    /// and degrade performance - for example `margin: var(--blah)` would be ambigouus because it's not clear
55    /// when setting the variable, whether all sides should be set, instead, you have to use `margin-top: var(--blah)`,
56    /// `margin-bottom: var(--baz)` in order to work around this limitation.
57    VarOnShorthandProperty { key: CombinedCssPropertyType, value: &'a str },
58}
59
60impl_display!{ CssParseErrorInner<'a>, {
61    ParseError(e) => format!("Parse Error: {:?}", e),
62    UnclosedBlock => "Unclosed block",
63    MalformedCss => "Malformed Css",
64    DynamicCssParseError(e) => format!("{}", e),
65    PseudoSelectorParseError(e) => format!("Failed to parse pseudo-selector: {}", e),
66    NodeTypePath(e) => format!("Failed to parse CSS selector path: {}", e),
67    UnknownPropertyKey(k, v) => format!("Unknown CSS key: \"{}: {}\"", k, v),
68    VarOnShorthandProperty { key, value } => format!(
69        "Error while parsing: \"{}: {};\": var() cannot be used on shorthand properties - use `{}-top` or `{}-x` as the key instead: ",
70        key, value, key, key
71    ),
72}}
73
74impl<'a> From<CssSyntaxError> for CssParseErrorInner<'a> {
75    fn from(e: CssSyntaxError) -> Self {
76        CssParseErrorInner::ParseError(e)
77    }
78}
79
80impl_from! { DynamicCssParseError<'a>, CssParseErrorInner::DynamicCssParseError }
81impl_from! { NodeTypePathParseError<'a>, CssParseErrorInner::NodeTypePath }
82impl_from! { CssPseudoSelectorParseError<'a>, CssParseErrorInner::PseudoSelectorParseError }
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum CssPseudoSelectorParseError<'a> {
86    EmptyNthChild,
87    UnknownSelector(&'a str, Option<&'a str>),
88    InvalidNthChildPattern(&'a str),
89    InvalidNthChild(ParseIntError),
90}
91
92impl<'a> From<ParseIntError> for CssPseudoSelectorParseError<'a> {
93    fn from(e: ParseIntError) -> Self { CssPseudoSelectorParseError::InvalidNthChild(e) }
94}
95
96impl_display! { CssPseudoSelectorParseError<'a>, {
97    EmptyNthChild => format!("\
98        Empty :nth-child() selector - nth-child() must at least take a number, \
99        a pattern (such as \"2n+3\") or the values \"even\" or \"odd\"."
100    ),
101    UnknownSelector(selector, value) => {
102        let format_str = match value {
103            Some(v) => format!("{}({})", selector, v),
104            None => format!("{}", selector),
105        };
106        format!("Invalid or unknown CSS pseudo-selector: ':{}'", format_str)
107    },
108    InvalidNthChildPattern(selector) => format!(
109        "Invalid pseudo-selector :{} - value has to be a \
110        number, \"even\" or \"odd\" or a pattern such as \"2n+3\"", selector
111    ),
112    InvalidNthChild(e) => format!("Invalid :nth-child pseudo-selector: ':{}'", e),
113}}
114
115/// Error that can happen during `css_parser::parse_key_value_pair`
116#[derive(Debug, Clone, PartialEq)]
117pub enum DynamicCssParseError<'a> {
118    /// The brace contents aren't valid, i.e. `var(asdlfkjasf)`
119    InvalidBraceContents(&'a str),
120    /// Unexpected value when parsing the string
121    UnexpectedValue(CssParsingError<'a>),
122}
123
124impl_display!{ DynamicCssParseError<'a>, {
125    InvalidBraceContents(e) => format!("Invalid contents of var() function: var({})", e),
126    UnexpectedValue(e) => format!("{}", e),
127}}
128
129impl<'a> From<CssParsingError<'a>> for DynamicCssParseError<'a> {
130    fn from(e: CssParsingError<'a>) -> Self {
131        DynamicCssParseError::UnexpectedValue(e)
132    }
133}
134
135/// "selector" contains the actual selector such as "nth-child" while "value" contains
136/// an optional value - for example "nth-child(3)" would be: selector: "nth-child", value: "3".
137fn pseudo_selector_from_str<'a>(selector: &'a str, value: Option<&'a str>)
138-> Result<CssPathPseudoSelector, CssPseudoSelectorParseError<'a>>
139{
140    match selector {
141        "first" => Ok(CssPathPseudoSelector::First),
142        "last" => Ok(CssPathPseudoSelector::Last),
143        "hover" => Ok(CssPathPseudoSelector::Hover),
144        "active" => Ok(CssPathPseudoSelector::Active),
145        "focus" => Ok(CssPathPseudoSelector::Focus),
146        "nth-child" => {
147            let value = value.ok_or(CssPseudoSelectorParseError::EmptyNthChild)?;
148            let parsed = parse_nth_child_selector(value)?;
149            Ok(CssPathPseudoSelector::NthChild(parsed))
150        },
151        _ => {
152            Err(CssPseudoSelectorParseError::UnknownSelector(selector, value))
153        },
154    }
155}
156
157/// Parses the inner value of the `:nth-child` selector, including numbers and patterns.
158///
159/// I.e.: `"2n+3"` -> `Pattern { repeat: 2, offset: 3 }`
160fn parse_nth_child_selector<'a>(value: &'a str) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'a>> {
161
162    let value = value.trim();
163
164    if value.is_empty() {
165        return Err(CssPseudoSelectorParseError::EmptyNthChild);
166    }
167
168    if let Ok(number) = value.parse::<usize>() {
169        return Ok(Number(number));
170    }
171
172    // If the value is not a number
173    match value.as_ref() {
174        "even" => Ok(Even),
175        "odd" => Ok(Odd),
176        other => parse_nth_child_pattern(value),
177    }
178}
179
180/// Parses the pattern between the braces of a "nth-child" (such as "2n+3").
181fn parse_nth_child_pattern<'a>(value: &'a str) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'a>> {
182
183    let value = value.trim();
184
185    if value.is_empty() {
186        return Err(CssPseudoSelectorParseError::EmptyNthChild);
187    }
188
189    // TODO: Test for "+"
190    let repeat = value.split("n").next()
191        .ok_or(CssPseudoSelectorParseError::InvalidNthChildPattern(value))?
192        .trim()
193        .parse::<usize>()?;
194
195    // In a "2n+3" form, the first .next() yields the "2n", the second .next() yields the "3"
196    let mut offset_iterator = value.split("+");
197
198    // has to succeed, since the string is verified to not be empty
199    offset_iterator.next().unwrap();
200
201    let offset = match offset_iterator.next() {
202        Some(offset_string) => {
203            let offset_string = offset_string.trim();
204            if offset_string.is_empty() {
205                return Err(CssPseudoSelectorParseError::InvalidNthChildPattern(value));
206            } else {
207                offset_string.parse::<usize>()?
208            }
209        },
210        None => 0,
211    };
212
213    Ok(Pattern { repeat, offset })
214}
215
216#[test]
217fn test_css_pseudo_selector_parse() {
218
219    use self::CssPathPseudoSelector::*;
220    use self::CssPseudoSelectorParseError::*;
221
222    let ok_res = [
223        (("first", None), First),
224        (("last", None), Last),
225        (("hover", None), Hover),
226        (("active", None), Active),
227        (("focus", None), Focus),
228        (("nth-child", Some("4")), NthChild(Number(4))),
229        (("nth-child", Some("even")), NthChild(Even)),
230        (("nth-child", Some("odd")), NthChild(Odd)),
231        (("nth-child", Some("5n")), NthChild(Pattern { repeat: 5, offset: 0 })),
232        (("nth-child", Some("2n+3")), NthChild(Pattern { repeat: 2, offset: 3 })),
233    ];
234
235    let err = [
236        (("asdf", None), UnknownSelector("asdf", None)),
237        (("", None), UnknownSelector("", None)),
238        (("nth-child", Some("2n+")), InvalidNthChildPattern("2n+")),
239        // Can't test for ParseIntError because the fields are private.
240        // This is an example on why you shouldn't use std::error::Error!
241    ];
242
243    for ((selector, val), a) in &ok_res {
244        assert_eq!(pseudo_selector_from_str(selector, *val), Ok(*a));
245    }
246
247    for ((selector, val), e) in &err {
248        assert_eq!(pseudo_selector_from_str(selector, *val), Err(e.clone()));
249    }
250}
251
252#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
253pub struct ErrorLocation {
254    pub original_pos: usize,
255}
256
257impl ErrorLocation {
258    /// Given an error location, returns the (line, column)
259    pub fn get_line_column_from_error(&self, css_string: &str) -> (usize, usize) {
260
261        let error_location = self.original_pos.saturating_sub(1);
262        let (mut line_number, mut total_characters) = (0, 0);
263
264        for line in css_string[0..error_location].lines() {
265            line_number += 1;
266            total_characters += line.chars().count();
267        }
268
269        // Rust doesn't count "\n" as a character, so we have to add the line number count on top
270        let total_characters = total_characters + line_number;
271        let column_pos = error_location - total_characters.saturating_sub(2);
272
273        (line_number, column_pos)
274    }
275}
276
277impl<'a> fmt::Display for CssParseError<'a> {
278    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279        let start_location = self.location.0.get_line_column_from_error(self.css_string);
280        let end_location = self.location.1.get_line_column_from_error(self.css_string);
281        write!(f, "    start: line {}:{}\r\n    end: line {}:{}\r\n    text: \"{}\"\r\n    reason: {}",
282            start_location.0, start_location.1,
283            end_location.0, end_location.1,
284            self.get_error_string(),
285            self.error,
286        )
287    }
288}
289
290pub fn new_from_str<'a>(css_string: &'a str) -> Result<Css, CssParseError<'a>> {
291    let mut tokenizer = Tokenizer::new(css_string);
292    let (stylesheet, _warnings) = new_from_str_inner(css_string, &mut tokenizer)?;
293    Ok(Css { stylesheets: vec![stylesheet] })
294}
295
296/// Returns the location of where the parser is currently in the document
297fn get_error_location(tokenizer: &Tokenizer) -> ErrorLocation {
298    ErrorLocation {
299        original_pos: tokenizer.pos(),
300    }
301}
302
303#[derive(Debug, Clone, PartialEq)]
304pub enum CssPathParseError<'a> {
305    EmptyPath,
306    /// Invalid item encountered in string (for example a "{", "}")
307    InvalidTokenEncountered(&'a str),
308    UnexpectedEndOfStream(&'a str),
309    SyntaxError(CssSyntaxError),
310    /// The path has to be either `*`, `div`, `p` or something like that
311    NodeTypePath(NodeTypePathParseError<'a>),
312    /// Error while parsing a pseudo selector (like `:aldkfja`)
313    PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
314}
315
316impl_from! { NodeTypePathParseError<'a>, CssPathParseError::NodeTypePath }
317impl_from! { CssPseudoSelectorParseError<'a>, CssPathParseError::PseudoSelectorParseError }
318
319impl<'a> From<CssSyntaxError> for CssPathParseError<'a> {
320    fn from(e: CssSyntaxError) -> Self {
321        CssPathParseError::SyntaxError(e)
322    }
323}
324
325/// Parses a CSS path from a string (only the path,.no commas allowed)
326///
327/// ```rust
328/// # extern crate azul_css;
329/// # extern crate azul_css_parser;
330/// # use azul_css_parser::parse_css_path;
331/// # use azul_css::{
332/// #     CssPathSelector::*, CssPathPseudoSelector::*, CssPath,
333/// #     NodeTypePath::*, CssNthChildSelector::*
334/// # };
335///
336/// assert_eq!(
337///     parse_css_path("* div #my_id > .class:nth-child(2)"),
338///     Ok(CssPath { selectors: vec![
339///          Global,
340///          Type(Div),
341///          Children,
342///          Id("my_id".to_string()),
343///          DirectChildren,
344///          Class("class".to_string()),
345///          PseudoSelector(NthChild(Number(2))),
346///     ]})
347/// );
348/// ```
349pub fn parse_css_path<'a>(input: &'a str) -> Result<CssPath, CssPathParseError<'a>> {
350
351    use azul_simplecss::{Token, Combinator};
352
353    let input = input.trim();
354    if input.is_empty() {
355        return Err(CssPathParseError::EmptyPath);
356    }
357
358    let mut tokenizer = Tokenizer::new(input);
359    let mut selectors = Vec::new();
360
361    loop {
362        let token = tokenizer.parse_next()?;
363        match token {
364            Token::UniversalSelector => {
365                selectors.push(CssPathSelector::Global);
366            },
367            Token::TypeSelector(div_type) => {
368                selectors.push(CssPathSelector::Type(NodeTypePath::from_str(div_type)?));
369            },
370            Token::IdSelector(id) => {
371                selectors.push(CssPathSelector::Id(id.to_string()));
372            },
373            Token::ClassSelector(class) => {
374                selectors.push(CssPathSelector::Class(class.to_string()));
375            },
376            Token::Combinator(Combinator::GreaterThan) => {
377                selectors.push(CssPathSelector::DirectChildren);
378            },
379            Token::Combinator(Combinator::Space) => {
380                selectors.push(CssPathSelector::Children);
381            },
382            Token::PseudoClass { selector, value } => {
383                selectors.push(CssPathSelector::PseudoSelector(pseudo_selector_from_str(selector, value)?));
384            },
385            Token::EndOfStream => {
386                break;
387            }
388            _ => {
389                return Err(CssPathParseError::InvalidTokenEncountered(input));
390            }
391        }
392    }
393
394    if !selectors.is_empty() {
395        Ok(CssPath { selectors })
396    } else {
397        Err(CssPathParseError::EmptyPath)
398    }
399}
400
401#[derive(Debug, Clone, PartialEq)]
402pub struct UnparsedCssRuleBlock<'a> {
403    /// The css path (full selector) of the style ruleset
404    pub path: CssPath,
405    /// `"justify-content" => "center"`
406    pub declarations: HashMap<&'a str, (&'a str, (ErrorLocation, ErrorLocation))>,
407}
408
409#[derive(Debug, Clone, PartialEq)]
410pub struct CssParseWarnMsg<'a> {
411    warning: CssParseWarnMsgInner<'a>,
412    location: (ErrorLocation, ErrorLocation),
413}
414
415#[derive(Debug, Clone, PartialEq)]
416pub enum CssParseWarnMsgInner<'a> {
417    /// Key "blah" isn't (yet) supported, so the parser didn't attempt to parse the value at all
418    UnsupportedKeyValuePair { key: &'a str, value: &'a str },
419}
420
421/// Parses a CSS string (single-threaded) and returns the parsed rules in blocks
422///
423/// May return "warning" messages, i.e. messages that just serve as a warning,
424/// instead of being actual errors. These warnings may be ignored by the caller,
425/// but can be useful for debugging.
426fn new_from_str_inner<'a>(css_string: &'a str, tokenizer: &mut Tokenizer<'a>)
427-> Result<(Stylesheet, Vec<CssParseWarnMsg<'a>>), CssParseError<'a>> {
428
429    use azul_simplecss::{Token, Combinator};
430
431    let mut css_blocks = Vec::new();
432
433    // Used for error checking / checking for closed braces
434    let mut parser_in_block = false;
435    let mut block_nesting = 0_usize;
436
437    // Current css paths (i.e. `div#id, .class, p` are stored here -
438    // when the block is finished, all `current_rules` gets duplicated with
439    // one path corresponding to one set of rules each).
440    let mut current_paths = Vec::new();
441    // Current CSS declarations
442    let mut current_rules = HashMap::<&str, (&str, (ErrorLocation, ErrorLocation))>::new();
443    // Keep track of the current path during parsing
444    let mut last_path = Vec::new();
445
446    let mut last_error_location = ErrorLocation { original_pos: 0 };
447
448    loop {
449
450        let token = tokenizer.parse_next().map_err(|e| CssParseError {
451            css_string,
452            error: e.into(),
453            location: (last_error_location, get_error_location(tokenizer))
454        })?;
455
456        macro_rules! check_parser_is_outside_block {() => {
457            if parser_in_block {
458                return Err(CssParseError {
459                    css_string,
460                    error: CssParseErrorInner::MalformedCss,
461                    location: (last_error_location, get_error_location(tokenizer)),
462                });
463            }
464        }}
465
466        macro_rules! check_parser_is_inside_block {() => {
467            if !parser_in_block {
468                return Err(CssParseError {
469                    css_string,
470                    error: CssParseErrorInner::MalformedCss,
471                    location: (last_error_location, get_error_location(tokenizer)),
472                });
473            }
474        }}
475
476        match token {
477            Token::BlockStart => {
478                check_parser_is_outside_block!();
479                parser_in_block = true;
480                block_nesting += 1;
481                current_paths.push(last_path.clone());
482                last_path.clear();
483            },
484            Token::Comma => {
485                check_parser_is_outside_block!();
486                current_paths.push(last_path.clone());
487                last_path.clear();
488            },
489            Token::BlockEnd => {
490
491                block_nesting -= 1;
492                check_parser_is_inside_block!();
493                parser_in_block = false;
494
495                css_blocks.extend(current_paths.drain(..).map(|path| {
496                    UnparsedCssRuleBlock {
497                        path: CssPath { selectors: path },
498                        declarations: current_rules.clone(),
499                    }
500                }));
501
502                current_rules.clear();
503                last_path.clear(); // technically unnecessary, but just to be sure
504            },
505
506            // tokens that adjust the last_path
507            Token::UniversalSelector => {
508                check_parser_is_outside_block!();
509                last_path.push(CssPathSelector::Global);
510            },
511            Token::TypeSelector(div_type) => {
512                check_parser_is_outside_block!();
513                last_path.push(CssPathSelector::Type(NodeTypePath::from_str(div_type).map_err(|e| {
514                    CssParseError {
515                        css_string,
516                        error: e.into(),
517                        location: (last_error_location, get_error_location(tokenizer)),
518                    }
519                })?));
520            },
521            Token::IdSelector(id) => {
522                check_parser_is_outside_block!();
523                last_path.push(CssPathSelector::Id(id.to_string()));
524            },
525            Token::ClassSelector(class) => {
526                check_parser_is_outside_block!();
527                last_path.push(CssPathSelector::Class(class.to_string()));
528            },
529            Token::Combinator(Combinator::GreaterThan) => {
530                check_parser_is_outside_block!();
531                last_path.push(CssPathSelector::DirectChildren);
532            },
533            Token::Combinator(Combinator::Space) => {
534                check_parser_is_outside_block!();
535                last_path.push(CssPathSelector::Children);
536            },
537            Token::PseudoClass { selector, value } => {
538                check_parser_is_outside_block!();
539                last_path.push(CssPathSelector::PseudoSelector(pseudo_selector_from_str(selector, value).map_err(|e| {
540                    CssParseError {
541                        css_string,
542                        error: e.into(),
543                        location: (last_error_location, get_error_location(tokenizer)),
544                    }
545                })?));
546            },
547            Token::Declaration(key, val) => {
548                check_parser_is_inside_block!();
549                current_rules.insert(key, (val, (last_error_location, get_error_location(tokenizer))));
550            },
551            Token::EndOfStream => {
552
553                // uneven number of open / close braces
554                if block_nesting != 0 {
555                    return Err(CssParseError {
556                        css_string,
557                        error: CssParseErrorInner::UnclosedBlock,
558                        location: (last_error_location, get_error_location(tokenizer)),
559                    });
560                }
561
562                break;
563            },
564            _ => {
565                // attributes, lang-attributes and @keyframes are not supported
566            }
567        }
568
569        last_error_location = get_error_location(tokenizer);
570    }
571
572    unparsed_css_blocks_to_stylesheet(css_blocks, css_string)
573}
574
575fn unparsed_css_blocks_to_stylesheet<'a>(css_blocks: Vec<UnparsedCssRuleBlock<'a>>, css_string: &'a str)
576-> Result<(Stylesheet, Vec<CssParseWarnMsg<'a>>), CssParseError<'a>> {
577
578    // Actually parse the properties (TODO: this could be done in parallel and in a separate function)
579    let css_key_map = azul_css::get_css_key_map();
580
581    let mut warnings = Vec::new();
582
583    let parsed_css_blocks = css_blocks.into_iter().map(|unparsed_css_block| {
584
585        let mut declarations = Vec::<CssDeclaration>::new();
586
587        for (unparsed_css_key, (unparsed_css_value, location)) in unparsed_css_block.declarations {
588            parse_css_declaration(
589                unparsed_css_key,
590                unparsed_css_value,
591                location,
592                &css_key_map,
593                &mut warnings,
594                &mut declarations,
595            ).map_err(|e| CssParseError {
596                css_string,
597                error: e.into(),
598                location,
599            })?;
600        }
601
602        Ok(CssRuleBlock {
603            path: unparsed_css_block.path,
604            declarations,
605        })
606    }).collect::<Result<Vec<CssRuleBlock>, CssParseError>>()?;
607
608    Ok((parsed_css_blocks.into(), warnings))
609}
610
611fn parse_css_declaration<'a>(
612    unparsed_css_key: &'a str,
613    unparsed_css_value: &'a str,
614    location: (ErrorLocation, ErrorLocation),
615    css_key_map: &CssKeyMap,
616    warnings: &mut Vec<CssParseWarnMsg<'a>>,
617    declarations: &mut Vec<CssDeclaration>,
618) -> Result<(), CssParseErrorInner<'a>> {
619
620    use self::CssParseErrorInner::*;
621    use self::CssParseWarnMsgInner::*;
622
623    if let Some(combined_key) = CombinedCssPropertyType::from_str(unparsed_css_key, &css_key_map) {
624        if let Some(css_var) = check_if_value_is_css_var(unparsed_css_value) {
625            // margin: var(--my-variable);
626            return Err(VarOnShorthandProperty { key: combined_key, value: unparsed_css_value });
627        } else {
628            // margin: 10px;
629            let parsed_css_properties =
630                css_parser::parse_combined_css_property(combined_key, unparsed_css_value)
631                .map_err(|e| DynamicCssParseError(e.into()))?;
632
633            declarations.extend(parsed_css_properties.into_iter().map(|val| CssDeclaration::Static(val)));
634        }
635    } else if let Some(normal_key) = CssPropertyType::from_str(unparsed_css_key, css_key_map) {
636        if let Some(css_var) = check_if_value_is_css_var(unparsed_css_value) {
637            // margin-left: var(--my-variable);
638            let (css_var_id, css_var_default) = css_var?;
639            let parsed_default_value =
640                css_parser::parse_css_property(normal_key, css_var_default)
641                .map_err(|e| DynamicCssParseError(e.into()))?;
642
643            declarations.push(CssDeclaration::Dynamic(DynamicCssProperty {
644                dynamic_id: css_var_id.to_string(),
645                default_value: parsed_default_value,
646            }));
647        } else {
648            // margin-left: 10px;
649            let parsed_css_value =
650                css_parser::parse_css_property(normal_key, unparsed_css_value)
651                .map_err(|e| DynamicCssParseError(e.into()))?;
652
653            declarations.push(CssDeclaration::Static(parsed_css_value));
654        }
655    } else {
656        // asldfkjasdf: 10px;
657        warnings.push(CssParseWarnMsg {
658            warning: UnsupportedKeyValuePair { key: unparsed_css_key, value: unparsed_css_value },
659            location,
660        });
661    }
662
663    Ok(())
664}
665
666fn check_if_value_is_css_var<'a>(unparsed_css_value: &'a str) -> Option<Result<(&'a str, &'a str), CssParseErrorInner<'a>>> {
667
668    const DEFAULT_VARIABLE_DEFAULT: &str = "none";
669
670    let (_, brace_contents) = css_parser::parse_parentheses(unparsed_css_value, &["var"]).ok()?;
671
672    // value is a CSS variable, i.e. var(--main-bg-color)
673    Some(match parse_css_variable_brace_contents(brace_contents) {
674        Some((variable_id, default_value)) => Ok((variable_id, default_value.unwrap_or(DEFAULT_VARIABLE_DEFAULT))),
675        None => Err(DynamicCssParseError::InvalidBraceContents(brace_contents).into()),
676    })
677}
678
679/// Parses the brace contents of a css var, i.e.:
680///
681/// ```no_run,ignore
682/// "--main-bg-col, blue" => (Some("main-bg-col"), Some("blue"))
683/// "--main-bg-col"       => (Some("main-bg-col"), None)
684/// ```
685fn parse_css_variable_brace_contents<'a>(input: &'a str) -> Option<(&'a str, Option<&'a str>)> {
686
687    let input = input.trim();
688
689    let mut split_comma_iter = input.splitn(2, ",");
690    let var_name = split_comma_iter.next()?;
691    let var_name = var_name.trim();
692
693    if !var_name.starts_with("--") {
694        return None; // no proper CSS variable name
695    }
696
697    Some((&var_name[2..], split_comma_iter.next()))
698}
699
700#[test]
701fn test_css_parse_1() {
702
703    use azul_css::*;
704
705    let parsed_css = new_from_str("
706        div#my_id .my_class:first {
707            background-color: red;
708        }
709    ").unwrap();
710
711
712    let expected_css_rules = vec![CssRuleBlock {
713        path: CssPath {
714            selectors: vec![
715                CssPathSelector::Type(NodeTypePath::Div),
716                CssPathSelector::Id(String::from("my_id")),
717                CssPathSelector::Children,
718                // NOTE: This is technically wrong, the space between "#my_id"
719                // and ".my_class" is important, but gets ignored for now
720                CssPathSelector::Class(String::from("my_class")),
721                CssPathSelector::PseudoSelector(CssPathPseudoSelector::First),
722            ],
723        },
724        declarations: vec![CssDeclaration::Static(CssProperty::BackgroundContent(
725            CssPropertyValue::Exact(StyleBackgroundContent::Color(ColorU {
726                r: 255,
727                g: 0,
728                b: 0,
729                a: 255,
730            })),
731        ))],
732    }];
733
734    assert_eq!(
735        parsed_css,
736        Css {
737            stylesheets: vec![expected_css_rules.into()]
738        }
739    );
740}
741
742#[test]
743fn test_css_simple_selector_parse() {
744    use self::CssPathSelector::*;
745    use azul_css::NodeTypePath;
746    let css = "div#id.my_class > p .new { }";
747    let parsed = vec![
748        Type(NodeTypePath::Div),
749        Id("id".into()),
750        Class("my_class".into()),
751        DirectChildren,
752        Type(NodeTypePath::P),
753        Children,
754        Class("new".into())
755    ];
756    assert_eq!(new_from_str(css).unwrap(), Css {
757        stylesheets: vec![Stylesheet {
758            rules: vec![CssRuleBlock {
759                path: CssPath { selectors: parsed },
760                declarations: Vec::new(),
761            }],
762        }],
763    });
764}
765
766#[cfg(test)]
767mod stylesheet_parse {
768
769    use azul_css::*;
770    use super::*;
771
772    fn test_css(css: &str, expected: Vec<CssRuleBlock>) {
773        let css = new_from_str(css).unwrap();
774        assert_eq!(css, Css { stylesheets: vec![expected.into()] });
775    }
776
777    // Tests that an element with a single class always gets the CSS element applied properly
778    #[test]
779    fn test_apply_css_pure_class() {
780        let red = CssProperty::BackgroundContent(CssPropertyValue::Exact(
781            StyleBackgroundContent::Color(ColorU {
782                r: 255,
783                g: 0,
784                b: 0,
785                a: 255,
786            }),
787        ));
788        let blue = CssProperty::BackgroundContent(CssPropertyValue::Exact(
789            StyleBackgroundContent::Color(ColorU {
790                r: 0,
791                g: 0,
792                b: 255,
793                a: 255,
794            }),
795        ));
796        let black = CssProperty::BackgroundContent(CssPropertyValue::Exact(
797            StyleBackgroundContent::Color(ColorU {
798                r: 0,
799                g: 0,
800                b: 0,
801                a: 255,
802            }),
803        ));
804
805        // Simple example
806        {
807            let css_1 = ".my_class { background-color: red; }";
808            let expected_rules = vec![
809                CssRuleBlock {
810                    path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into())] },
811                    declarations: vec![
812                        CssDeclaration::Static(red.clone())
813                    ],
814                },
815            ];
816            test_css(css_1, expected_rules);
817        }
818
819        // Slightly more complex example
820        {
821            let css_2 = "#my_id { background-color: red; } .my_class { background-color: blue; }";
822            let expected_rules = vec![
823                CssRuleBlock {
824                    path: CssPath { selectors: vec![CssPathSelector::Id("my_id".into())] },
825                    declarations: vec![CssDeclaration::Static(red.clone())]
826                },
827                CssRuleBlock {
828                    path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into())] },
829                    declarations: vec![CssDeclaration::Static(blue.clone())]
830                },
831            ];
832            test_css(css_2, expected_rules);
833        }
834
835        // Even more complex example
836        {
837            let css_3 = "* { background-color: black; } .my_class#my_id { background-color: red; } .my_class { background-color: blue; }";
838            let expected_rules = vec![
839                CssRuleBlock {
840                    path: CssPath { selectors: vec![CssPathSelector::Global] },
841                    declarations: vec![CssDeclaration::Static(black.clone())]
842                },
843                CssRuleBlock {
844                    path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into()), CssPathSelector::Id("my_id".into())] },
845                    declarations: vec![CssDeclaration::Static(red.clone())]
846                },
847                CssRuleBlock {
848                    path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into())] },
849                    declarations: vec![CssDeclaration::Static(blue.clone())]
850                },
851            ];
852            test_css(css_3, expected_rules);
853        }
854    }
855}
856
857// Assert that order of the style rules is correct (in same order as provided in CSS form)
858#[test]
859fn test_multiple_rules() {
860    use azul_css::*;
861    use self::CssPathSelector::*;
862
863    let parsed_css = new_from_str("
864        * { }
865        * div.my_class#my_id { }
866        * div#my_id { }
867        * #my_id { }
868        div.my_class.specific#my_id { }
869    ").unwrap();
870
871    let expected_rules = vec![
872        // Rules are sorted by order of appearance in source string
873        CssRuleBlock { path: CssPath { selectors: vec![Global] }, declarations: Vec::new() },
874        CssRuleBlock { path: CssPath { selectors: vec![Global, Type(NodeTypePath::Div), Class("my_class".into()), Id("my_id".into())] }, declarations: Vec::new() },
875        CssRuleBlock { path: CssPath { selectors: vec![Global, Type(NodeTypePath::Div), Id("my_id".into())] }, declarations: Vec::new() },
876        CssRuleBlock { path: CssPath { selectors: vec![Global, Id("my_id".into())] }, declarations: Vec::new() },
877        CssRuleBlock { path: CssPath { selectors: vec![Type(NodeTypePath::Div), Class("my_class".into()), Class("specific".into()), Id("my_id".into())] }, declarations: Vec::new() },
878    ];
879
880    assert_eq!(parsed_css, Css { stylesheets: vec![expected_rules.into()] });
881}
882
883#[test]
884fn test_case_issue_93() {
885
886    use azul_css::*;
887    use self::CssPathSelector::*;
888
889    let parsed_css = new_from_str("
890        .tabwidget-tab-label {
891          color: #FFFFFF;
892        }
893
894        .tabwidget-tab.active .tabwidget-tab-label {
895          color: #000000;
896        }
897
898        .tabwidget-tab.active .tabwidget-tab-close {
899          color: #FF0000;
900        }
901    ").unwrap();
902
903    fn declaration(classes: &[CssPathSelector], color: ColorU) -> CssRuleBlock {
904        CssRuleBlock {
905            path: CssPath {
906                selectors: classes.to_vec(),
907            },
908            declarations: vec![CssDeclaration::Static(CssProperty::TextColor(
909                CssPropertyValue::Exact(StyleTextColor(color)),
910            ))],
911        }
912    }
913
914    let expected_rules = vec![
915        declaration(&[Class("tabwidget-tab-label".into())], ColorU { r: 255, g: 255, b: 255, a: 255 }),
916        declaration(&[Class("tabwidget-tab".into()), Class("active".into()), Children, Class("tabwidget-tab-label".into())], ColorU { r: 0, g: 0, b: 0, a: 255 }),
917        declaration(&[Class("tabwidget-tab".into()), Class("active".into()), Children, Class("tabwidget-tab-close".into())], ColorU { r: 255, g: 0, b: 0, a: 255 }),
918    ];
919
920    assert_eq!(parsed_css, Css { stylesheets: vec![expected_rules.into()] });
921}