1 use rustc_data_structures::fx::FxHashMap;
2 use std::collections::hash_map::Entry;
4 use std::iter::Peekable;
8 use rustc_errors::Handler;
14 pub(crate) struct CssPath {
15 pub(crate) rules: FxHashMap<String, String>,
16 pub(crate) children: FxHashMap<String, CssPath>,
19 /// When encountering a `"` or a `'`, returns the whole string, including the quote characters.
20 fn get_string(iter: &mut Peekable<Chars<'_>>, string_start: char) -> String {
21 let mut s = String::with_capacity(2);
24 while let Some(c) = iter.next() {
28 } else if c == string_start {
35 /// Skips a `/*` comment.
36 fn skip_comment(iter: &mut Peekable<Chars<'_>>) {
37 while let Some(c) = iter.next() {
38 if c == '*' && iter.next() == Some('/') {
44 /// Skips a line comment (`//`).
45 fn skip_line_comment(iter: &mut Peekable<Chars<'_>>) {
46 while let Some(c) = iter.next() {
53 fn handle_common_chars(c: char, buffer: &mut String, iter: &mut Peekable<Chars<'_>>) {
55 '"' | '\'' => buffer.push_str(&get_string(iter, c)),
56 '/' if iter.peek() == Some(&'*') => skip_comment(iter),
57 '/' if iter.peek() == Some(&'/') => skip_line_comment(iter),
62 /// Returns a CSS property name. Ends when encountering a `:` character.
64 /// If the `:` character isn't found, returns `None`.
66 /// If a `{` character is encountered, returns an error.
67 fn parse_property_name(iter: &mut Peekable<Chars<'_>>) -> Result<Option<String>, String> {
68 let mut content = String::new();
70 while let Some(c) = iter.next() {
72 ':' => return Ok(Some(content.trim().to_owned())),
73 '{' => return Err("Unexpected `{` in a `{}` block".to_owned()),
75 _ => handle_common_chars(c, &mut content, iter),
81 /// Try to get the value of a CSS property (the `#fff` in `color: #fff`). It'll stop when it
82 /// encounters a `{` or a `;` character.
84 /// It returns the value string and a boolean set to `true` if the value is ended with a `}` because
85 /// it means that the parent block is done and that we should notify the parent caller.
86 fn parse_property_value(iter: &mut Peekable<Chars<'_>>) -> (String, bool) {
87 let mut value = String::new();
88 let mut out_block = false;
90 while let Some(c) = iter.next() {
97 _ => handle_common_chars(c, &mut value, iter),
100 (value.trim().to_owned(), out_block)
103 /// This is used to parse inside a CSS `{}` block. If we encounter a new `{` inside it, we consider
104 /// it as a new block and therefore recurse into `parse_rules`.
108 iter: &mut Peekable<Chars<'_>>,
109 paths: &mut FxHashMap<String, CssPath>,
110 ) -> Result<(), String> {
111 let mut rules = FxHashMap::default();
112 let mut children = FxHashMap::default();
115 // If the parent isn't a "normal" CSS selector, we only expect sub-selectors and not CSS
117 if selector.starts_with('@') {
118 parse_selectors(content, iter, &mut children)?;
121 let rule = match parse_property_name(iter)? {
124 return Err(format!("Found empty rule in selector `{selector}`"));
130 let (value, out_block) = parse_property_value(iter);
131 if value.is_empty() {
132 return Err(format!("Found empty value for rule `{rule}` in selector `{selector}`"));
134 match rules.entry(rule) {
135 Entry::Occupied(mut o) => {
136 eprintln!("Duplicated rule `{}` in CSS selector `{selector}`", o.key());
137 *o.get_mut() = value;
139 Entry::Vacant(v) => {
148 match paths.entry(selector) {
149 Entry::Occupied(mut o) => {
150 eprintln!("Duplicated CSS selector: `{}`", o.key());
152 for (key, value) in rules.into_iter() {
153 v.rules.insert(key, value);
155 for (sel, child) in children.into_iter() {
156 v.children.insert(sel, child);
159 Entry::Vacant(v) => {
160 v.insert(CssPath { rules, children });
166 pub(crate) fn parse_selectors(
168 iter: &mut Peekable<Chars<'_>>,
169 paths: &mut FxHashMap<String, CssPath>,
170 ) -> Result<(), String> {
171 let mut selector = String::new();
173 while let Some(c) = iter.next() {
176 let s = minifier::css::minify(selector.trim()).map(|s| s.to_string())?;
177 parse_rules(content, s, iter, paths)?;
181 ';' => selector.clear(), // We don't handle inline selectors like `@import`.
182 _ => handle_common_chars(c, &mut selector, iter),
188 /// The entry point to parse the CSS rules. Every time we encounter a `{`, we then parse the rules
190 pub(crate) fn load_css_paths(content: &str) -> Result<FxHashMap<String, CssPath>, String> {
191 let mut iter = content.chars().peekable();
192 let mut paths = FxHashMap::default();
194 parse_selectors(content, &mut iter, &mut paths)?;
198 pub(crate) fn get_differences(
199 origin: &FxHashMap<String, CssPath>,
200 against: &FxHashMap<String, CssPath>,
203 for (selector, entry) in origin.iter() {
204 match against.get(selector) {
205 Some(a) => get_differences(&a.children, &entry.children, v),
206 None => v.push(format!(" Missing rule `{}`", selector)),
211 pub(crate) fn test_theme_against<P: AsRef<Path>>(
213 origin: &FxHashMap<String, CssPath>,
215 ) -> (bool, Vec<String>) {
216 let against = match fs::read_to_string(f)
217 .map_err(|e| e.to_string())
218 .and_then(|data| load_css_paths(&data))
222 diag.struct_err(&e).emit();
223 return (false, vec![]);
227 let mut ret = vec![];
228 get_differences(origin, &against, &mut ret);