From fb42dae9870c8e1085e9e524363123741a49a255 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 14 Sep 2022 17:38:44 +0200 Subject: [PATCH 1/5] Simplify CSS parser to check themes --- src/librustdoc/config.rs | 16 +- src/librustdoc/theme.rs | 369 +++++++++++++++++---------------------- 2 files changed, 178 insertions(+), 207 deletions(-) diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 8a8cc272e8195..932533db05c14 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -412,7 +412,13 @@ impl Options { let to_check = matches.opt_strs("check-theme"); if !to_check.is_empty() { - let paths = theme::load_css_paths(static_files::themes::LIGHT.as_bytes()); + let paths = match theme::load_css_paths(static_files::themes::LIGHT) { + Ok(p) => p, + Err(e) => { + diag.struct_err(&e.to_string()).emit(); + return Err(1); + } + }; let mut errors = 0; println!("rustdoc: [check-theme] Starting tests! (Ignoring all other arguments)"); @@ -547,7 +553,13 @@ impl Options { let mut themes = Vec::new(); if matches.opt_present("theme") { - let paths = theme::load_css_paths(static_files::themes::LIGHT.as_bytes()); + let paths = match theme::load_css_paths(static_files::themes::LIGHT) { + Ok(p) => p, + Err(e) => { + diag.struct_err(&e.to_string()).emit(); + return Err(1); + } + }; for (theme_file, theme_s) in matches.opt_strs("theme").iter().map(|s| (PathBuf::from(&s), s.to_owned())) diff --git a/src/librustdoc/theme.rs b/src/librustdoc/theme.rs index 0118d7dd20722..77f8359bd42ca 100644 --- a/src/librustdoc/theme.rs +++ b/src/librustdoc/theme.rs @@ -1,271 +1,230 @@ -use rustc_data_structures::fx::FxHashSet; +use rustc_data_structures::fx::FxHashMap; +use std::collections::hash_map::Entry; use std::fs; -use std::hash::{Hash, Hasher}; +use std::iter::Peekable; use std::path::Path; +use std::str::Chars; use rustc_errors::Handler; #[cfg(test)] mod tests; -#[derive(Debug, Clone, Eq)] +#[derive(Debug)] pub(crate) struct CssPath { - pub(crate) name: String, - pub(crate) children: FxHashSet, + pub(crate) rules: FxHashMap, + pub(crate) children: FxHashMap, } -// This PartialEq implementation IS NOT COMMUTATIVE!!! -// -// The order is very important: the second object must have all first's rules. -// However, the first is not required to have all of the second's rules. -impl PartialEq for CssPath { - fn eq(&self, other: &CssPath) -> bool { - if self.name != other.name { - false - } else { - for child in &self.children { - if !other.children.iter().any(|c| child == c) { - return false; - } - } - true - } - } -} +/// When encountering a `"` or a `'`, returns the whole string, including the quote characters. +fn get_string(iter: &mut Peekable>, string_start: char) -> String { + let mut s = String::with_capacity(2); -impl Hash for CssPath { - fn hash(&self, state: &mut H) { - self.name.hash(state); - for x in &self.children { - x.hash(state); + s.push(string_start); + while let Some(c) = iter.next() { + s.push(c); + if c == '\\' { + iter.next(); + } else if c == string_start { + break; } } + s } -impl CssPath { - fn new(name: String) -> CssPath { - CssPath { name, children: FxHashSet::default() } +/// Skips a `/*` comment. +fn skip_comment(iter: &mut Peekable>) { + while let Some(c) = iter.next() { + if c == '*' && iter.next() == Some('/') { + break; + } } } -/// All variants contain the position they occur. -#[derive(Debug, Clone, Copy)] -enum Events { - StartLineComment(usize), - StartComment(usize), - EndComment(usize), - InBlock(usize), - OutBlock(usize), -} - -impl Events { - fn get_pos(&self) -> usize { - match *self { - Events::StartLineComment(p) - | Events::StartComment(p) - | Events::EndComment(p) - | Events::InBlock(p) - | Events::OutBlock(p) => p, +/// Skips a line comment (`//`). +fn skip_line_comment(iter: &mut Peekable>) { + while let Some(c) = iter.next() { + if c == '\n' { + break; } } - - fn is_comment(&self) -> bool { - matches!( - self, - Events::StartLineComment(_) | Events::StartComment(_) | Events::EndComment(_) - ) - } } -fn previous_is_line_comment(events: &[Events]) -> bool { - matches!(events.last(), Some(&Events::StartLineComment(_))) -} - -fn is_line_comment(pos: usize, v: &[u8], events: &[Events]) -> bool { - if let Some(&Events::StartComment(_)) = events.last() { - return false; +fn handle_common_chars(c: char, buffer: &mut String, iter: &mut Peekable>) { + match c { + '"' | '\'' => buffer.push_str(&get_string(iter, c)), + '/' if iter.peek() == Some(&'*') => skip_comment(iter), + '/' if iter.peek() == Some(&'/') => skip_line_comment(iter), + _ => buffer.push(c), } - v[pos + 1] == b'/' } -fn load_css_events(v: &[u8]) -> Vec { - let mut pos = 0; - let mut events = Vec::with_capacity(100); - - while pos + 1 < v.len() { - match v[pos] { - b'/' if v[pos + 1] == b'*' => { - events.push(Events::StartComment(pos)); - pos += 1; - } - b'/' if is_line_comment(pos, v, &events) => { - events.push(Events::StartLineComment(pos)); - pos += 1; - } - b'\n' if previous_is_line_comment(&events) => { - events.push(Events::EndComment(pos)); - } - b'*' if v[pos + 1] == b'/' => { - events.push(Events::EndComment(pos + 2)); - pos += 1; - } - b'{' if !previous_is_line_comment(&events) => { - if let Some(&Events::StartComment(_)) = events.last() { - pos += 1; - continue; - } - events.push(Events::InBlock(pos + 1)); - } - b'}' if !previous_is_line_comment(&events) => { - if let Some(&Events::StartComment(_)) = events.last() { - pos += 1; - continue; - } - events.push(Events::OutBlock(pos + 1)); - } - _ => {} +/// Returns a CSS property name. Ends when encountering a `:` character. +/// +/// If the `:` character isn't found, returns `None`. +/// +/// If a `{` character is encountered, returns an error. +fn parse_property_name(iter: &mut Peekable>) -> Result, String> { + let mut content = String::new(); + + while let Some(c) = iter.next() { + match c { + ':' => return Ok(Some(content.trim().to_owned())), + '{' => return Err("Unexpected `{` in a `{}` block".to_owned()), + '}' => break, + _ => handle_common_chars(c, &mut content, iter), } - pos += 1; } - events -} - -fn get_useful_next(events: &[Events], pos: &mut usize) -> Option { - while *pos < events.len() { - if !events[*pos].is_comment() { - return Some(events[*pos]); + Ok(None) +} + +/// Try to get the value of a CSS property (the `#fff` in `color: #fff`). It'll stop when it +/// encounters a `{` or a `;` character. +/// +/// It returns the value string and a boolean set to `true` if the value is ended with a `}` because +/// it means that the parent block is done and that we should notify the parent caller. +fn parse_property_value(iter: &mut Peekable>) -> (String, bool) { + let mut value = String::new(); + let mut out_block = false; + + while let Some(c) = iter.next() { + match c { + ';' => break, + '}' => { + out_block = true; + break; + } + _ => handle_common_chars(c, &mut value, iter), } - *pos += 1; } - None + (value.trim().to_owned(), out_block) } -fn get_previous_positions(events: &[Events], mut pos: usize) -> Vec { - let mut ret = Vec::with_capacity(3); +/// This is used to parse inside a CSS `{}` block. If we encounter a new `{` inside it, we consider +/// it as a new block and therefore recurse into `parse_rules`. +fn parse_rules( + content: &str, + selector: String, + iter: &mut Peekable>, + paths: &mut FxHashMap, +) -> Result<(), String> { + let mut rules = FxHashMap::default(); + let mut children = FxHashMap::default(); - ret.push(events[pos].get_pos()); - if pos > 0 { - pos -= 1; - } loop { - if pos < 1 || !events[pos].is_comment() { - let x = events[pos].get_pos(); - if *ret.last().unwrap() != x { - ret.push(x); - } else { - ret.push(0); + // If the parent isn't a "normal" CSS selector, we only expect sub-selectors and not CSS + // properties. + if selector.starts_with('@') { + parse_selectors(content, iter, &mut children)?; + break; + } + let rule = match parse_property_name(iter)? { + Some(r) => { + if r.is_empty() { + return Err(format!("Found empty rule in selector `{selector}`")); + } + r } + None => break, + }; + let (value, out_block) = parse_property_value(iter); + if value.is_empty() { + return Err(format!("Found empty value for rule `{rule}` in selector `{selector}`")); + } + match rules.entry(rule) { + Entry::Occupied(mut o) => { + eprintln!("Duplicated rule `{}` in CSS selector `{selector}`", o.key()); + *o.get_mut() = value; + } + Entry::Vacant(v) => { + v.insert(value); + } + } + if out_block { break; } - ret.push(events[pos].get_pos()); - pos -= 1; } - if ret.len() & 1 != 0 && events[pos].is_comment() { - ret.push(0); - } - ret.iter().rev().cloned().collect() -} -fn build_rule(v: &[u8], positions: &[usize]) -> String { - minifier::css::minify( - &positions - .chunks(2) - .map(|x| ::std::str::from_utf8(&v[x[0]..x[1]]).unwrap_or("")) - .collect::() - .trim() - .chars() - .filter_map(|c| match c { - '\n' | '\t' => Some(' '), - '/' | '{' | '}' => None, - c => Some(c), - }) - .collect::() - .split(' ') - .filter(|s| !s.is_empty()) - .intersperse(" ") - .collect::(), - ) - .map(|css| css.to_string()) - .unwrap_or_else(|_| String::new()) -} - -fn inner(v: &[u8], events: &[Events], pos: &mut usize) -> FxHashSet { - let mut paths = Vec::with_capacity(50); - - while *pos < events.len() { - if let Some(Events::OutBlock(_)) = get_useful_next(events, pos) { - *pos += 1; - break; + match paths.entry(selector) { + Entry::Occupied(mut o) => { + eprintln!("Duplicated CSS selector: `{}`", o.key()); + let v = o.get_mut(); + for (key, value) in rules.into_iter() { + v.rules.insert(key, value); + } + for (sel, child) in children.into_iter() { + v.children.insert(sel, child); + } } - if let Some(Events::InBlock(_)) = get_useful_next(events, pos) { - paths.push(CssPath::new(build_rule(v, &get_previous_positions(events, *pos)))); - *pos += 1; + Entry::Vacant(v) => { + v.insert(CssPath { rules, children }); } - while let Some(Events::InBlock(_)) = get_useful_next(events, pos) { - if let Some(ref mut path) = paths.last_mut() { - for entry in inner(v, events, pos).iter() { - path.children.insert(entry.clone()); - } + } + Ok(()) +} + +pub(crate) fn parse_selectors( + content: &str, + iter: &mut Peekable>, + paths: &mut FxHashMap, +) -> Result<(), String> { + let mut selector = String::new(); + + while let Some(c) = iter.next() { + match c { + '{' => { + let s = minifier::css::minify(selector.trim()).map(|s| s.to_string())?; + parse_rules(content, s, iter, paths)?; + selector.clear(); } - } - if let Some(Events::OutBlock(_)) = get_useful_next(events, pos) { - *pos += 1; + '}' => break, + ';' => selector.clear(), // We don't handle inline selectors like `@import`. + _ => handle_common_chars(c, &mut selector, iter), } } - paths.iter().cloned().collect() + Ok(()) } -pub(crate) fn load_css_paths(v: &[u8]) -> CssPath { - let events = load_css_events(v); - let mut pos = 0; +/// The entry point to parse the CSS rules. Every time we encounter a `{`, we then parse the rules +/// inside it. +pub(crate) fn load_css_paths(content: &str) -> Result, String> { + let mut iter = content.chars().peekable(); + let mut paths = FxHashMap::default(); - let mut parent = CssPath::new("parent".to_owned()); - parent.children = inner(v, &events, &mut pos); - parent + parse_selectors(content, &mut iter, &mut paths)?; + Ok(paths) } -pub(crate) fn get_differences(against: &CssPath, other: &CssPath, v: &mut Vec) { - if against.name == other.name { - for child in &against.children { - let mut found = false; - let mut found_working = false; - let mut tmp = Vec::new(); - - for other_child in &other.children { - if child.name == other_child.name { - if child != other_child { - get_differences(child, other_child, &mut tmp); - } else { - found_working = true; - } - found = true; - break; - } - } - if !found { - v.push(format!(" Missing \"{}\" rule", child.name)); - } else if !found_working { - v.extend(tmp.iter().cloned()); - } +pub(crate) fn get_differences( + origin: &FxHashMap, + against: &FxHashMap, + v: &mut Vec, +) { + for (selector, entry) in origin.iter() { + match against.get(selector) { + Some(a) => get_differences(&a.children, &entry.children, v), + None => v.push(format!(" Missing rule `{}`", selector)), } } } pub(crate) fn test_theme_against>( f: &P, - against: &CssPath, + origin: &FxHashMap, diag: &Handler, ) -> (bool, Vec) { - let data = match fs::read(f) { + let against = match fs::read_to_string(f) + .map_err(|e| e.to_string()) + .and_then(|data| load_css_paths(&data)) + { Ok(c) => c, Err(e) => { - diag.struct_err(&e.to_string()).emit(); + diag.struct_err(&e).emit(); return (false, vec![]); } }; - let paths = load_css_paths(&data); let mut ret = vec![]; - get_differences(against, &paths, &mut ret); + get_differences(origin, &against, &mut ret); (true, ret) } From 0b037c17b867cc8d47c0391b3d681fbf5a66ce80 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 14 Sep 2022 17:39:32 +0200 Subject: [PATCH 2/5] Update theme check tests --- src/librustdoc/theme/tests.rs | 60 +++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/src/librustdoc/theme/tests.rs b/src/librustdoc/theme/tests.rs index ae8f43c6d55ba..54226c6cf3b15 100644 --- a/src/librustdoc/theme/tests.rs +++ b/src/librustdoc/theme/tests.rs @@ -44,11 +44,7 @@ rule j end {} "#; let mut ret = Vec::new(); - get_differences( - &load_css_paths(against.as_bytes()), - &load_css_paths(text.as_bytes()), - &mut ret, - ); + get_differences(&load_css_paths(against).unwrap(), &load_css_paths(text).unwrap(), &mut ret); assert!(ret.is_empty()); } @@ -61,46 +57,45 @@ a c // sdf d {} "#; - let paths = load_css_paths(text.as_bytes()); - assert!(paths.children.contains(&CssPath::new("a b c d".to_owned()))); + let paths = load_css_paths(text).unwrap(); + assert!(paths.contains_key(&"a b c d".to_owned())); } #[test] fn test_comparison() { let x = r#" -a { - b { - c {} - } +@a { + b {} } "#; let y = r#" -a { +@a { b {} + c {} } "#; - let against = load_css_paths(y.as_bytes()); - let other = load_css_paths(x.as_bytes()); + let against = load_css_paths(y).unwrap(); + let other = load_css_paths(x).unwrap(); let mut ret = Vec::new(); get_differences(&against, &other, &mut ret); assert!(ret.is_empty()); get_differences(&other, &against, &mut ret); - assert_eq!(ret, vec![" Missing \"c\" rule".to_owned()]); + assert_eq!(ret, vec![" Missing rule `c`".to_owned()]); } #[test] fn check_empty_css() { - let events = load_css_events(&[]); - assert_eq!(events.len(), 0); + let paths = load_css_paths("").unwrap(); + assert_eq!(paths.len(), 0); } #[test] fn check_invalid_css() { - let events = load_css_events(b"*"); - assert_eq!(events.len(), 0); + let paths = load_css_paths("*").unwrap(); + assert_eq!(paths.len(), 0); } #[test] @@ -108,10 +103,33 @@ fn test_with_minification() { let text = include_str!("../html/static/css/themes/dark.css"); let minified = minifier::css::minify(&text).expect("CSS minification failed").to_string(); - let against = load_css_paths(text.as_bytes()); - let other = load_css_paths(minified.as_bytes()); + let against = load_css_paths(text).unwrap(); + let other = load_css_paths(&minified).unwrap(); let mut ret = Vec::new(); get_differences(&against, &other, &mut ret); assert!(ret.is_empty()); } + +#[test] +fn test_media() { + let text = r#" +@media (min-width: 701px) { + a:hover { + color: #fff; + } + + b { + x: y; + } +} +"#; + + let paths = load_css_paths(text).unwrap(); + eprintln!("{:?}", paths); + let p = paths.get("@media (min-width:701px)"); + assert!(p.is_some()); + let p = p.unwrap(); + assert!(p.children.get("a:hover").is_some()); + assert!(p.children.get("b").is_some()); +} From e7d8ad62db3af48ce8fb3844fd44be6251e1cc15 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 14 Sep 2022 18:36:48 +0200 Subject: [PATCH 3/5] Add check for missing CSS variables --- src/librustdoc/theme.rs | 14 ++++++++-- src/librustdoc/theme/tests.rs | 52 +++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/librustdoc/theme.rs b/src/librustdoc/theme.rs index 77f8359bd42ca..87cfe78e10cbd 100644 --- a/src/librustdoc/theme.rs +++ b/src/librustdoc/theme.rs @@ -202,8 +202,18 @@ pub(crate) fn get_differences( ) { for (selector, entry) in origin.iter() { match against.get(selector) { - Some(a) => get_differences(&a.children, &entry.children, v), - None => v.push(format!(" Missing rule `{}`", selector)), + Some(a) => { + get_differences(&entry.children, &a.children, v); + if selector == ":root" { + // We need to check that all variables have been set. + for rule in entry.rules.keys() { + if !a.rules.contains_key(rule) { + v.push(format!(" Missing CSS variable `{rule}` in `:root`")); + } + } + } + } + None => v.push(format!(" Missing rule `{selector}`")), } } } diff --git a/src/librustdoc/theme/tests.rs b/src/librustdoc/theme/tests.rs index 54226c6cf3b15..ce1d77d98d7ad 100644 --- a/src/librustdoc/theme/tests.rs +++ b/src/librustdoc/theme/tests.rs @@ -63,26 +63,26 @@ d {} #[test] fn test_comparison() { - let x = r#" + let origin = r#" @a { b {} + c {} } "#; - let y = r#" + let against = r#" @a { b {} - c {} } "#; - let against = load_css_paths(y).unwrap(); - let other = load_css_paths(x).unwrap(); + let origin = load_css_paths(origin).unwrap(); + let against = load_css_paths(against).unwrap(); let mut ret = Vec::new(); - get_differences(&against, &other, &mut ret); + get_differences(&against, &origin, &mut ret); assert!(ret.is_empty()); - get_differences(&other, &against, &mut ret); + get_differences(&origin, &against, &mut ret); assert_eq!(ret, vec![" Missing rule `c`".to_owned()]); } @@ -123,13 +123,49 @@ fn test_media() { x: y; } } + +@media (max-width: 1001px) { + b { + x: y; + } +} "#; let paths = load_css_paths(text).unwrap(); - eprintln!("{:?}", paths); let p = paths.get("@media (min-width:701px)"); assert!(p.is_some()); let p = p.unwrap(); assert!(p.children.get("a:hover").is_some()); assert!(p.children.get("b").is_some()); + + eprintln!("{:?}", paths); + let p = paths.get("@media (max-width:1001px)"); + assert!(p.is_some()); + let p = p.unwrap(); + assert!(p.children.get("b").is_some()); +} + +#[test] +fn test_css_variables() { + let x = r#" +:root { + --a: #fff; +} +"#; + + let y = r#" +:root { + --a: #fff; + --b: #fff; +} +"#; + + let against = load_css_paths(x).unwrap(); + let other = load_css_paths(y).unwrap(); + + let mut ret = Vec::new(); + get_differences(&against, &other, &mut ret); + assert!(ret.is_empty()); + get_differences(&other, &against, &mut ret); + assert_eq!(ret, vec![" Missing CSS variable `--b` in `:root`".to_owned()]); } From a528f68e79f464f47fd41dd503bbb14e8f34a8ea Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 14 Sep 2022 20:23:19 +0200 Subject: [PATCH 4/5] Remove duplicate warnings --- src/librustdoc/theme.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/librustdoc/theme.rs b/src/librustdoc/theme.rs index 87cfe78e10cbd..ce0cb6c4559a3 100644 --- a/src/librustdoc/theme.rs +++ b/src/librustdoc/theme.rs @@ -133,7 +133,6 @@ fn parse_rules( } match rules.entry(rule) { Entry::Occupied(mut o) => { - eprintln!("Duplicated rule `{}` in CSS selector `{selector}`", o.key()); *o.get_mut() = value; } Entry::Vacant(v) => { @@ -147,7 +146,6 @@ fn parse_rules( match paths.entry(selector) { Entry::Occupied(mut o) => { - eprintln!("Duplicated CSS selector: `{}`", o.key()); let v = o.get_mut(); for (key, value) in rules.into_iter() { v.rules.insert(key, value); From d3529ceb6c5eeaaecd3d88e97ad17740258772a8 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 15 Sep 2022 13:53:20 +0200 Subject: [PATCH 5/5] Correctly handle parens --- src/librustdoc/theme.rs | 28 +++++++++++++++++++++------- src/librustdoc/theme/tests.rs | 18 +++++++++++++++++- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/librustdoc/theme.rs b/src/librustdoc/theme.rs index ce0cb6c4559a3..e7a26cb346ee6 100644 --- a/src/librustdoc/theme.rs +++ b/src/librustdoc/theme.rs @@ -17,19 +17,31 @@ pub(crate) struct CssPath { } /// When encountering a `"` or a `'`, returns the whole string, including the quote characters. -fn get_string(iter: &mut Peekable>, string_start: char) -> String { - let mut s = String::with_capacity(2); - - s.push(string_start); +fn get_string(iter: &mut Peekable>, string_start: char, buffer: &mut String) { + buffer.push(string_start); while let Some(c) = iter.next() { - s.push(c); + buffer.push(c); if c == '\\' { iter.next(); } else if c == string_start { break; } } - s +} + +fn get_inside_paren( + iter: &mut Peekable>, + paren_start: char, + paren_end: char, + buffer: &mut String, +) { + buffer.push(paren_start); + while let Some(c) = iter.next() { + handle_common_chars(c, buffer, iter); + if c == paren_end { + break; + } + } } /// Skips a `/*` comment. @@ -52,9 +64,11 @@ fn skip_line_comment(iter: &mut Peekable>) { fn handle_common_chars(c: char, buffer: &mut String, iter: &mut Peekable>) { match c { - '"' | '\'' => buffer.push_str(&get_string(iter, c)), + '"' | '\'' => get_string(iter, c, buffer), '/' if iter.peek() == Some(&'*') => skip_comment(iter), '/' if iter.peek() == Some(&'/') => skip_line_comment(iter), + '(' => get_inside_paren(iter, c, ')', buffer), + '[' => get_inside_paren(iter, c, ']', buffer), _ => buffer.push(c), } } diff --git a/src/librustdoc/theme/tests.rs b/src/librustdoc/theme/tests.rs index ce1d77d98d7ad..08a174d27d357 100644 --- a/src/librustdoc/theme/tests.rs +++ b/src/librustdoc/theme/tests.rs @@ -138,7 +138,6 @@ fn test_media() { assert!(p.children.get("a:hover").is_some()); assert!(p.children.get("b").is_some()); - eprintln!("{:?}", paths); let p = paths.get("@media (max-width:1001px)"); assert!(p.is_some()); let p = p.unwrap(); @@ -169,3 +168,20 @@ fn test_css_variables() { get_differences(&other, &against, &mut ret); assert_eq!(ret, vec![" Missing CSS variable `--b` in `:root`".to_owned()]); } + +#[test] +fn test_weird_rule_value() { + let x = r#" +a[text=("a")] { + b: url({;}.png); + c: #fff +} +"#; + + let paths = load_css_paths(&x).unwrap(); + let p = paths.get("a[text=(\"a\")]"); + assert!(p.is_some()); + let p = p.unwrap(); + assert_eq!(p.rules.get("b"), Some(&"url({;}.png)".to_owned())); + assert_eq!(p.rules.get("c"), Some(&"#fff".to_owned())); +}