]>
Commit | Line | Data |
---|---|---|
f2b60f7d FG |
1 | use rustc_data_structures::fx::FxHashMap; |
2 | use std::collections::hash_map::Entry; | |
0731742a | 3 | use std::fs; |
f2b60f7d | 4 | use std::iter::Peekable; |
2c00a5a8 | 5 | use std::path::Path; |
f2b60f7d | 6 | use std::str::Chars; |
2c00a5a8 | 7 | |
dfeec247 | 8 | use rustc_errors::Handler; |
94b46f34 | 9 | |
416331ca XL |
10 | #[cfg(test)] |
11 | mod tests; | |
12 | ||
f2b60f7d | 13 | #[derive(Debug)] |
923072b8 | 14 | pub(crate) struct CssPath { |
f2b60f7d FG |
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; | |
2c00a5a8 XL |
28 | } |
29 | } | |
30 | } | |
31 | ||
f2b60f7d FG |
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; | |
2c00a5a8 XL |
43 | } |
44 | } | |
45 | } | |
46 | ||
f2b60f7d FG |
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 | } | |
2c00a5a8 XL |
53 | } |
54 | } | |
55 | ||
f2b60f7d FG |
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; | |
2c00a5a8 XL |
61 | } |
62 | } | |
2c00a5a8 XL |
63 | } |
64 | ||
f2b60f7d FG |
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), | |
2c00a5a8 | 73 | } |
2c00a5a8 XL |
74 | } |
75 | ||
f2b60f7d FG |
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), | |
2c00a5a8 | 90 | } |
2c00a5a8 | 91 | } |
f2b60f7d FG |
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), | |
2c00a5a8 | 112 | } |
2c00a5a8 | 113 | } |
f2b60f7d | 114 | (value.trim().to_owned(), out_block) |
2c00a5a8 XL |
115 | } |
116 | ||
f2b60f7d FG |
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(); | |
2c00a5a8 | 127 | |
2c00a5a8 | 128 | loop { |
f2b60f7d FG |
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); | |
2c00a5a8 | 154 | } |
f2b60f7d FG |
155 | } |
156 | if out_block { | |
dfeec247 | 157 | break; |
2c00a5a8 | 158 | } |
2c00a5a8 | 159 | } |
2c00a5a8 | 160 | |
f2b60f7d FG |
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 | } | |
2c00a5a8 | 170 | } |
f2b60f7d FG |
171 | Entry::Vacant(v) => { |
172 | v.insert(CssPath { rules, children }); | |
2c00a5a8 | 173 | } |
f2b60f7d FG |
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(); | |
2c00a5a8 | 191 | } |
f2b60f7d FG |
192 | '}' => break, |
193 | ';' => selector.clear(), // We don't handle inline selectors like `@import`. | |
194 | _ => handle_common_chars(c, &mut selector, iter), | |
2c00a5a8 XL |
195 | } |
196 | } | |
f2b60f7d FG |
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 | } | |
2c00a5a8 | 225 | } |
2c00a5a8 XL |
226 | } |
227 | } | |
f2b60f7d | 228 | None => v.push(format!(" Missing rule `{selector}`")), |
2c00a5a8 XL |
229 | } |
230 | } | |
231 | } | |
232 | ||
923072b8 | 233 | pub(crate) fn test_theme_against<P: AsRef<Path>>( |
dc9dc135 | 234 | f: &P, |
f2b60f7d | 235 | origin: &FxHashMap<String, CssPath>, |
dc9dc135 XL |
236 | diag: &Handler, |
237 | ) -> (bool, Vec<String>) { | |
f2b60f7d FG |
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 | { | |
ba9703b0 XL |
242 | Ok(c) => c, |
243 | Err(e) => { | |
f2b60f7d | 244 | diag.struct_err(&e).emit(); |
ba9703b0 XL |
245 | return (false, vec![]); |
246 | } | |
247 | }; | |
60c5eb7d | 248 | |
0731742a | 249 | let mut ret = vec![]; |
f2b60f7d | 250 | get_differences(origin, &against, &mut ret); |
2c00a5a8 XL |
251 | (true, ret) |
252 | } |