]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/theme.rs
Rollup merge of #107531 - GuillaumeGomez:inline-images-in-css, r=notriddle
[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, buffer: &mut String) {
21     buffer.push(string_start);
22     while let Some(c) = iter.next() {
23         buffer.push(c);
24         if c == '\\' {
25             iter.next();
26         } else if c == string_start {
27             break;
28         }
29     }
30 }
31
32 fn get_inside_paren(
33     iter: &mut Peekable<Chars<'_>>,
34     paren_start: char,
35     paren_end: char,
36     buffer: &mut String,
37 ) {
38     buffer.push(paren_start);
39     while let Some(c) = iter.next() {
40         handle_common_chars(c, buffer, iter);
41         if c == paren_end {
42             break;
43         }
44     }
45 }
46
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('/') {
51             break;
52         }
53     }
54 }
55
56 /// Skips a line comment (`//`).
57 fn skip_line_comment(iter: &mut Peekable<Chars<'_>>) {
58     while let Some(c) = iter.next() {
59         if c == '\n' {
60             break;
61         }
62     }
63 }
64
65 fn handle_common_chars(c: char, buffer: &mut String, iter: &mut Peekable<Chars<'_>>) {
66     match c {
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),
72         _ => buffer.push(c),
73     }
74 }
75
76 /// Returns a CSS property name. Ends when encountering a `:` character.
77 ///
78 /// If the `:` character isn't found, returns `None`.
79 ///
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();
83
84     while let Some(c) = iter.next() {
85         match c {
86             ':' => return Ok(Some(content.trim().to_owned())),
87             '{' => return Err("Unexpected `{` in a `{}` block".to_owned()),
88             '}' => break,
89             _ => handle_common_chars(c, &mut content, iter),
90         }
91     }
92     Ok(None)
93 }
94
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.
97 ///
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;
103
104     while let Some(c) = iter.next() {
105         match c {
106             ';' => break,
107             '}' => {
108                 out_block = true;
109                 break;
110             }
111             _ => handle_common_chars(c, &mut value, iter),
112         }
113     }
114     (value.trim().to_owned(), out_block)
115 }
116
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`.
119 fn parse_rules(
120     content: &str,
121     selector: String,
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();
127
128     loop {
129         // If the parent isn't a "normal" CSS selector, we only expect sub-selectors and not CSS
130         // properties.
131         if selector.starts_with('@') {
132             parse_selectors(content, iter, &mut children)?;
133             break;
134         }
135         let rule = match parse_property_name(iter)? {
136             Some(r) => {
137                 if r.is_empty() {
138                     return Err(format!("Found empty rule in selector `{selector}`"));
139                 }
140                 r
141             }
142             None => break,
143         };
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}`"));
147         }
148         match rules.entry(rule) {
149             Entry::Occupied(mut o) => {
150                 *o.get_mut() = value;
151             }
152             Entry::Vacant(v) => {
153                 v.insert(value);
154             }
155         }
156         if out_block {
157             break;
158         }
159     }
160
161     match paths.entry(selector) {
162         Entry::Occupied(mut o) => {
163             let v = o.get_mut();
164             for (key, value) in rules.into_iter() {
165                 v.rules.insert(key, value);
166             }
167             for (sel, child) in children.into_iter() {
168                 v.children.insert(sel, child);
169             }
170         }
171         Entry::Vacant(v) => {
172             v.insert(CssPath { rules, children });
173         }
174     }
175     Ok(())
176 }
177
178 pub(crate) fn parse_selectors(
179     content: &str,
180     iter: &mut Peekable<Chars<'_>>,
181     paths: &mut FxHashMap<String, CssPath>,
182 ) -> Result<(), String> {
183     let mut selector = String::new();
184
185     while let Some(c) = iter.next() {
186         match c {
187             '{' => {
188                 let s = minifier::css::minify(selector.trim()).map(|s| s.to_string())?;
189                 parse_rules(content, s, iter, paths)?;
190                 selector.clear();
191             }
192             '}' => break,
193             ';' => selector.clear(), // We don't handle inline selectors like `@import`.
194             _ => handle_common_chars(c, &mut selector, iter),
195         }
196     }
197     Ok(())
198 }
199
200 /// The entry point to parse the CSS rules. Every time we encounter a `{`, we then parse the rules
201 /// inside it.
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();
205
206     parse_selectors(content, &mut iter, &mut paths)?;
207     Ok(paths)
208 }
209
210 pub(crate) fn get_differences(
211     origin: &FxHashMap<String, CssPath>,
212     against: &FxHashMap<String, CssPath>,
213     v: &mut Vec<String>,
214 ) {
215     for (selector, entry) in origin.iter() {
216         match against.get(selector) {
217             Some(a) => {
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`"));
224                         }
225                     }
226                 }
227             }
228             None => v.push(format!("  Missing rule `{selector}`")),
229         }
230     }
231 }
232
233 pub(crate) fn test_theme_against<P: AsRef<Path>>(
234     f: &P,
235     origin: &FxHashMap<String, CssPath>,
236     diag: &Handler,
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))
241     {
242         Ok(c) => c,
243         Err(e) => {
244             diag.struct_err(&e).emit();
245             return (false, vec![]);
246         }
247     };
248
249     let mut ret = vec![];
250     get_differences(origin, &against, &mut ret);
251     (true, ret)
252 }