]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/theme.rs
Add check for missing CSS variables
[rust.git] / src / librustdoc / theme.rs
1 use rustc_data_structures::fx::FxHashMap;
2 use std::collections::hash_map::Entry;
3 use std::fs;
4 use std::iter::Peekable;
5 use std::path::Path;
6 use std::str::Chars;
7
8 use rustc_errors::Handler;
9
10 #[cfg(test)]
11 mod tests;
12
13 #[derive(Debug)]
14 pub(crate) struct CssPath {
15     pub(crate) rules: FxHashMap<String, String>,
16     pub(crate) children: FxHashMap<String, CssPath>,
17 }
18
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);
22
23     s.push(string_start);
24     while let Some(c) = iter.next() {
25         s.push(c);
26         if c == '\\' {
27             iter.next();
28         } else if c == string_start {
29             break;
30         }
31     }
32     s
33 }
34
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('/') {
39             break;
40         }
41     }
42 }
43
44 /// Skips a line comment (`//`).
45 fn skip_line_comment(iter: &mut Peekable<Chars<'_>>) {
46     while let Some(c) = iter.next() {
47         if c == '\n' {
48             break;
49         }
50     }
51 }
52
53 fn handle_common_chars(c: char, buffer: &mut String, iter: &mut Peekable<Chars<'_>>) {
54     match c {
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),
58         _ => buffer.push(c),
59     }
60 }
61
62 /// Returns a CSS property name. Ends when encountering a `:` character.
63 ///
64 /// If the `:` character isn't found, returns `None`.
65 ///
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();
69
70     while let Some(c) = iter.next() {
71         match c {
72             ':' => return Ok(Some(content.trim().to_owned())),
73             '{' => return Err("Unexpected `{` in a `{}` block".to_owned()),
74             '}' => break,
75             _ => handle_common_chars(c, &mut content, iter),
76         }
77     }
78     Ok(None)
79 }
80
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.
83 ///
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;
89
90     while let Some(c) = iter.next() {
91         match c {
92             ';' => break,
93             '}' => {
94                 out_block = true;
95                 break;
96             }
97             _ => handle_common_chars(c, &mut value, iter),
98         }
99     }
100     (value.trim().to_owned(), out_block)
101 }
102
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`.
105 fn parse_rules(
106     content: &str,
107     selector: String,
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();
113
114     loop {
115         // If the parent isn't a "normal" CSS selector, we only expect sub-selectors and not CSS
116         // properties.
117         if selector.starts_with('@') {
118             parse_selectors(content, iter, &mut children)?;
119             break;
120         }
121         let rule = match parse_property_name(iter)? {
122             Some(r) => {
123                 if r.is_empty() {
124                     return Err(format!("Found empty rule in selector `{selector}`"));
125                 }
126                 r
127             }
128             None => break,
129         };
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}`"));
133         }
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;
138             }
139             Entry::Vacant(v) => {
140                 v.insert(value);
141             }
142         }
143         if out_block {
144             break;
145         }
146     }
147
148     match paths.entry(selector) {
149         Entry::Occupied(mut o) => {
150             eprintln!("Duplicated CSS selector: `{}`", o.key());
151             let v = o.get_mut();
152             for (key, value) in rules.into_iter() {
153                 v.rules.insert(key, value);
154             }
155             for (sel, child) in children.into_iter() {
156                 v.children.insert(sel, child);
157             }
158         }
159         Entry::Vacant(v) => {
160             v.insert(CssPath { rules, children });
161         }
162     }
163     Ok(())
164 }
165
166 pub(crate) fn parse_selectors(
167     content: &str,
168     iter: &mut Peekable<Chars<'_>>,
169     paths: &mut FxHashMap<String, CssPath>,
170 ) -> Result<(), String> {
171     let mut selector = String::new();
172
173     while let Some(c) = iter.next() {
174         match c {
175             '{' => {
176                 let s = minifier::css::minify(selector.trim()).map(|s| s.to_string())?;
177                 parse_rules(content, s, iter, paths)?;
178                 selector.clear();
179             }
180             '}' => break,
181             ';' => selector.clear(), // We don't handle inline selectors like `@import`.
182             _ => handle_common_chars(c, &mut selector, iter),
183         }
184     }
185     Ok(())
186 }
187
188 /// The entry point to parse the CSS rules. Every time we encounter a `{`, we then parse the rules
189 /// inside it.
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();
193
194     parse_selectors(content, &mut iter, &mut paths)?;
195     Ok(paths)
196 }
197
198 pub(crate) fn get_differences(
199     origin: &FxHashMap<String, CssPath>,
200     against: &FxHashMap<String, CssPath>,
201     v: &mut Vec<String>,
202 ) {
203     for (selector, entry) in origin.iter() {
204         match against.get(selector) {
205             Some(a) => {
206                 get_differences(&entry.children, &a.children, v);
207                 if selector == ":root" {
208                     // We need to check that all variables have been set.
209                     for rule in entry.rules.keys() {
210                         if !a.rules.contains_key(rule) {
211                             v.push(format!("  Missing CSS variable `{rule}` in `:root`"));
212                         }
213                     }
214                 }
215             }
216             None => v.push(format!("  Missing rule `{selector}`")),
217         }
218     }
219 }
220
221 pub(crate) fn test_theme_against<P: AsRef<Path>>(
222     f: &P,
223     origin: &FxHashMap<String, CssPath>,
224     diag: &Handler,
225 ) -> (bool, Vec<String>) {
226     let against = match fs::read_to_string(f)
227         .map_err(|e| e.to_string())
228         .and_then(|data| load_css_paths(&data))
229     {
230         Ok(c) => c,
231         Err(e) => {
232             diag.struct_err(&e).emit();
233             return (false, vec![]);
234         }
235     };
236
237     let mut ret = vec![];
238     get_differences(origin, &against, &mut ret);
239     (true, ret)
240 }