1use std::fs;
2use std::iter::Peekable;
3use std::path::Path;
4use std::str::Chars;
5
6use rustc_data_structures::fx::{FxIndexMap, IndexEntry};
7use rustc_errors::DiagCtxtHandle;
8
9#[cfg(test)]
10mod tests;
11
12#[derive(Debug)]
13pub(crate) struct CssPath {
14 pub(crate) rules: FxIndexMap<String, String>,
15 pub(crate) children: FxIndexMap<String, CssPath>,
16}
17
18fn get_string(iter: &mut Peekable<Chars<'_>>, string_start: char, buffer: &mut String) {
20 buffer.push(string_start);
21 while let Some(c) = iter.next() {
22 buffer.push(c);
23 if c == '\\' {
24 iter.next();
25 } else if c == string_start {
26 break;
27 }
28 }
29}
30
31fn get_inside_paren(
32 iter: &mut Peekable<Chars<'_>>,
33 paren_start: char,
34 paren_end: char,
35 buffer: &mut String,
36) {
37 buffer.push(paren_start);
38 while let Some(c) = iter.next() {
39 handle_common_chars(c, buffer, iter);
40 if c == paren_end {
41 break;
42 }
43 }
44}
45
46fn skip_comment(iter: &mut Peekable<Chars<'_>>) {
48 while let Some(c) = iter.next() {
49 if c == '*' && iter.next() == Some('/') {
50 break;
51 }
52 }
53}
54
55fn skip_line_comment(iter: &mut Peekable<Chars<'_>>) {
57 for c in iter.by_ref() {
58 if c == '\n' {
59 break;
60 }
61 }
62}
63
64fn handle_common_chars(c: char, buffer: &mut String, iter: &mut Peekable<Chars<'_>>) {
65 match c {
66 '"' | '\'' => get_string(iter, c, buffer),
67 '/' if iter.peek() == Some(&'*') => skip_comment(iter),
68 '/' if iter.peek() == Some(&'/') => skip_line_comment(iter),
69 '(' => get_inside_paren(iter, c, ')', buffer),
70 '[' => get_inside_paren(iter, c, ']', buffer),
71 _ => buffer.push(c),
72 }
73}
74
75fn parse_property_name(iter: &mut Peekable<Chars<'_>>) -> Result<Option<String>, String> {
81 let mut content = String::new();
82
83 while let Some(c) = iter.next() {
84 match c {
85 ':' => return Ok(Some(content.trim().to_owned())),
86 '{' => return Err("Unexpected `{` in a `{}` block".to_owned()),
87 '}' => break,
88 _ => handle_common_chars(c, &mut content, iter),
89 }
90 }
91 Ok(None)
92}
93
94fn parse_property_value(iter: &mut Peekable<Chars<'_>>) -> (String, bool) {
100 let mut value = String::new();
101 let mut out_block = false;
102
103 while let Some(c) = iter.next() {
104 match c {
105 ';' => break,
106 '}' => {
107 out_block = true;
108 break;
109 }
110 _ => handle_common_chars(c, &mut value, iter),
111 }
112 }
113 (value.trim().to_owned(), out_block)
114}
115
116fn parse_rules(
119 content: &str,
120 selector: String,
121 iter: &mut Peekable<Chars<'_>>,
122 paths: &mut FxIndexMap<String, CssPath>,
123) -> Result<(), String> {
124 let mut rules = FxIndexMap::default();
125 let mut children = FxIndexMap::default();
126
127 loop {
128 if selector.starts_with('@') {
131 parse_selectors(content, iter, &mut children)?;
132 break;
133 }
134 let rule = match parse_property_name(iter)? {
135 Some(r) => {
136 if r.is_empty() {
137 return Err(format!("Found empty rule in selector `{selector}`"));
138 }
139 r
140 }
141 None => break,
142 };
143 let (value, out_block) = parse_property_value(iter);
144 if value.is_empty() {
145 return Err(format!("Found empty value for rule `{rule}` in selector `{selector}`"));
146 }
147 match rules.entry(rule) {
148 IndexEntry::Occupied(mut o) => {
149 *o.get_mut() = value;
150 }
151 IndexEntry::Vacant(v) => {
152 v.insert(value);
153 }
154 }
155 if out_block {
156 break;
157 }
158 }
159
160 match paths.entry(selector) {
161 IndexEntry::Occupied(mut o) => {
162 let v = o.get_mut();
163 for (key, value) in rules.into_iter() {
164 v.rules.insert(key, value);
165 }
166 for (sel, child) in children.into_iter() {
167 v.children.insert(sel, child);
168 }
169 }
170 IndexEntry::Vacant(v) => {
171 v.insert(CssPath { rules, children });
172 }
173 }
174 Ok(())
175}
176
177pub(crate) fn parse_selectors(
178 content: &str,
179 iter: &mut Peekable<Chars<'_>>,
180 paths: &mut FxIndexMap<String, CssPath>,
181) -> Result<(), String> {
182 let mut selector = String::new();
183
184 while let Some(c) = iter.next() {
185 match c {
186 '{' => {
187 if selector.trim().starts_with(":root[data-theme") {
188 selector = String::from(":root");
189 }
190 let s = minifier::css::minify(selector.trim()).map(|s| s.to_string())?;
191 parse_rules(content, s, iter, paths)?;
192 selector.clear();
193 }
194 '}' => break,
195 ';' => selector.clear(), _ => handle_common_chars(c, &mut selector, iter),
197 }
198 }
199 Ok(())
200}
201
202pub(crate) fn load_css_paths(content: &str) -> Result<FxIndexMap<String, CssPath>, String> {
205 let mut iter = content.chars().peekable();
206 let mut paths = FxIndexMap::default();
207
208 parse_selectors(content, &mut iter, &mut paths)?;
209 Ok(paths)
210}
211
212pub(crate) fn get_differences(
213 origin: &FxIndexMap<String, CssPath>,
214 against: &FxIndexMap<String, CssPath>,
215 v: &mut Vec<String>,
216) {
217 for (selector, entry) in origin.iter() {
218 match against.get(selector) {
219 Some(a) => {
220 get_differences(&entry.children, &a.children, v);
221 if selector == ":root" {
222 for rule in entry.rules.keys() {
224 if !a.rules.contains_key(rule) {
225 v.push(format!(" Missing CSS variable `{rule}` in `:root`"));
226 }
227 }
228 }
229 }
230 None => v.push(format!(" Missing rule `{selector}`")),
231 }
232 }
233}
234
235pub(crate) fn test_theme_against<P: AsRef<Path>>(
236 f: &P,
237 origin: &FxIndexMap<String, CssPath>,
238 dcx: DiagCtxtHandle<'_>,
239) -> (bool, Vec<String>) {
240 let against = match fs::read_to_string(f)
241 .map_err(|e| e.to_string())
242 .and_then(|data| load_css_paths(&data))
243 {
244 Ok(c) => c,
245 Err(e) => {
246 dcx.err(e);
247 return (false, vec![]);
248 }
249 };
250
251 let mut ret = vec![];
252 get_differences(origin, &against, &mut ret);
253 (true, ret)
254}