use rustc_data_structures::fx::FxHashMap; use std::collections::hash_map::Entry; use std::fs; use std::iter::Peekable; use std::path::Path; use std::str::Chars; use rustc_errors::DiagCtxt; #[cfg(test)] mod tests; #[derive(Debug)] pub(crate) struct CssPath { pub(crate) rules: FxHashMap, pub(crate) children: FxHashMap, } /// When encountering a `"` or a `'`, returns the whole string, including the quote characters. fn get_string(iter: &mut Peekable>, string_start: char, buffer: &mut String) { buffer.push(string_start); while let Some(c) = iter.next() { buffer.push(c); if c == '\\' { iter.next(); } else if c == string_start { break; } } } 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. fn skip_comment(iter: &mut Peekable>) { while let Some(c) = iter.next() { if c == '*' && iter.next() == Some('/') { break; } } } /// Skips a line comment (`//`). fn skip_line_comment(iter: &mut Peekable>) { while let Some(c) = iter.next() { if c == '\n' { break; } } } fn handle_common_chars(c: char, buffer: &mut String, iter: &mut Peekable>) { match 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), } } /// 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), } } 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), } } (value.trim().to_owned(), out_block) } /// 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(); loop { // 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) => { *o.get_mut() = value; } Entry::Vacant(v) => { v.insert(value); } } if out_block { break; } } match paths.entry(selector) { Entry::Occupied(mut o) => { 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); } } Entry::Vacant(v) => { v.insert(CssPath { rules, children }); } } 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 { '{' => { if selector.trim().starts_with(":root[data-theme") { selector = String::from(":root"); } let s = minifier::css::minify(selector.trim()).map(|s| s.to_string())?; parse_rules(content, s, iter, paths)?; selector.clear(); } '}' => break, ';' => selector.clear(), // We don't handle inline selectors like `@import`. _ => handle_common_chars(c, &mut selector, iter), } } Ok(()) } /// 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(); parse_selectors(content, &mut iter, &mut paths)?; Ok(paths) } 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(&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}`")), } } } pub(crate) fn test_theme_against>( f: &P, origin: &FxHashMap, dcx: &DiagCtxt, ) -> (bool, Vec) { 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) => { dcx.struct_err(e).emit(); return (false, vec![]); } }; let mut ret = vec![]; get_differences(origin, &against, &mut ret); (true, ret) }