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, buffer: &mut String) {
21 buffer.push(string_start);
22 while let Some(c) = iter.next() {
26 } else if c == string_start {
33 iter: &mut Peekable<Chars<'_>>,
38 buffer.push(paren_start);
39 while let Some(c) = iter.next() {
40 handle_common_chars(c, buffer, iter);
47 /// Skips a `/*` comment.
48 fn skip_comment(iter: &mut Peekable<Chars<'_>>) {
49 while let Some(c) = iter.next() {
50 if c == '*' && iter.next() == Some('/') {
56 /// Skips a line comment (`//`).
57 fn skip_line_comment(iter: &mut Peekable<Chars<'_>>) {
58 while let Some(c) = iter.next() {
65 fn handle_common_chars(c: char, buffer: &mut String, iter: &mut Peekable<Chars<'_>>) {
67 '"' | '\'' => get_string(iter, c, buffer),
68 '/' if iter.peek() == Some(&'*') => skip_comment(iter),
69 '/' if iter.peek() == Some(&'/') => skip_line_comment(iter),
70 '(' => get_inside_paren(iter, c, ')', buffer),
71 '[' => get_inside_paren(iter, c, ']', buffer),
76 /// Returns a CSS property name. Ends when encountering a `:` character.
78 /// If the `:` character isn't found, returns `None`.
80 /// If a `{` character is encountered, returns an error.
81 fn parse_property_name(iter: &mut Peekable<Chars<'_>>) -> Result<Option<String>, String> {
82 let mut content = String::new();
84 while let Some(c) = iter.next() {
86 ':' => return Ok(Some(content.trim().to_owned())),
87 '{' => return Err("Unexpected `{` in a `{}` block".to_owned()),
89 _ => handle_common_chars(c, &mut content, iter),
95 /// Try to get the value of a CSS property (the `#fff` in `color: #fff`). It'll stop when it
96 /// encounters a `{` or a `;` character.
98 /// It returns the value string and a boolean set to `true` if the value is ended with a `}` because
99 /// it means that the parent block is done and that we should notify the parent caller.
100 fn parse_property_value(iter: &mut Peekable<Chars<'_>>) -> (String, bool) {
101 let mut value = String::new();
102 let mut out_block = false;
104 while let Some(c) = iter.next() {
111 _ => handle_common_chars(c, &mut value, iter),
114 (value.trim().to_owned(), out_block)
117 /// This is used to parse inside a CSS `{}` block. If we encounter a new `{` inside it, we consider
118 /// it as a new block and therefore recurse into `parse_rules`.
122 iter: &mut Peekable<Chars<'_>>,
123 paths: &mut FxHashMap<String, CssPath>,
124 ) -> Result<(), String> {
125 let mut rules = FxHashMap::default();
126 let mut children = FxHashMap::default();
129 // If the parent isn't a "normal" CSS selector, we only expect sub-selectors and not CSS
131 if selector.starts_with('@') {
132 parse_selectors(content, iter, &mut children)?;
135 let rule = match parse_property_name(iter)? {
138 return Err(format!("Found empty rule in selector `{selector}`"));
144 let (value, out_block) = parse_property_value(iter);
145 if value.is_empty() {
146 return Err(format!("Found empty value for rule `{rule}` in selector `{selector}`"));
148 match rules.entry(rule) {
149 Entry::Occupied(mut o) => {
150 *o.get_mut() = value;
152 Entry::Vacant(v) => {
161 match paths.entry(selector) {
162 Entry::Occupied(mut o) => {
164 for (key, value) in rules.into_iter() {
165 v.rules.insert(key, value);
167 for (sel, child) in children.into_iter() {
168 v.children.insert(sel, child);
171 Entry::Vacant(v) => {
172 v.insert(CssPath { rules, children });
178 pub(crate) fn parse_selectors(
180 iter: &mut Peekable<Chars<'_>>,
181 paths: &mut FxHashMap<String, CssPath>,
182 ) -> Result<(), String> {
183 let mut selector = String::new();
185 while let Some(c) = iter.next() {
188 let s = minifier::css::minify(selector.trim()).map(|s| s.to_string())?;
189 parse_rules(content, s, iter, paths)?;
193 ';' => selector.clear(), // We don't handle inline selectors like `@import`.
194 _ => handle_common_chars(c, &mut selector, iter),
200 /// The entry point to parse the CSS rules. Every time we encounter a `{`, we then parse the rules
202 pub(crate) fn load_css_paths(content: &str) -> Result<FxHashMap<String, CssPath>, String> {
203 let mut iter = content.chars().peekable();
204 let mut paths = FxHashMap::default();
206 parse_selectors(content, &mut iter, &mut paths)?;
210 pub(crate) fn get_differences(
211 origin: &FxHashMap<String, CssPath>,
212 against: &FxHashMap<String, CssPath>,
215 for (selector, entry) in origin.iter() {
216 match against.get(selector) {
218 get_differences(&entry.children, &a.children, v);
219 if selector == ":root" {
220 // We need to check that all variables have been set.
221 for rule in entry.rules.keys() {
222 if !a.rules.contains_key(rule) {
223 v.push(format!(" Missing CSS variable `{rule}` in `:root`"));
228 None => v.push(format!(" Missing rule `{selector}`")),
233 pub(crate) fn test_theme_against<P: AsRef<Path>>(
235 origin: &FxHashMap<String, CssPath>,
237 ) -> (bool, Vec<String>) {
238 let against = match fs::read_to_string(f)
239 .map_err(|e| e.to_string())
240 .and_then(|data| load_css_paths(&data))
244 diag.struct_err(&e).emit();
245 return (false, vec![]);
249 let mut ret = vec![];
250 get_differences(origin, &against, &mut ret);