]> git.proxmox.com Git - rustc.git/blame - src/librustdoc/theme.rs
New upstream version 1.74.1+dfsg1
[rustc.git] / src / librustdoc / theme.rs
CommitLineData
f2b60f7d
FG
1use rustc_data_structures::fx::FxHashMap;
2use std::collections::hash_map::Entry;
0731742a 3use std::fs;
f2b60f7d 4use std::iter::Peekable;
2c00a5a8 5use std::path::Path;
f2b60f7d 6use std::str::Chars;
2c00a5a8 7
dfeec247 8use rustc_errors::Handler;
94b46f34 9
416331ca
XL
10#[cfg(test)]
11mod tests;
12
f2b60f7d 13#[derive(Debug)]
923072b8 14pub(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.
20fn 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
32fn 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.
48fn 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 (`//`).
57fn 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
65fn 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.
81fn 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.
100fn 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`.
119fn 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
178pub(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 '{' => {
781aab86
FG
188 if selector.trim().starts_with(":root[data-theme") {
189 selector = String::from(":root");
190 }
f2b60f7d
FG
191 let s = minifier::css::minify(selector.trim()).map(|s| s.to_string())?;
192 parse_rules(content, s, iter, paths)?;
193 selector.clear();
2c00a5a8 194 }
f2b60f7d
FG
195 '}' => break,
196 ';' => selector.clear(), // We don't handle inline selectors like `@import`.
197 _ => handle_common_chars(c, &mut selector, iter),
2c00a5a8
XL
198 }
199 }
f2b60f7d
FG
200 Ok(())
201}
202
203/// The entry point to parse the CSS rules. Every time we encounter a `{`, we then parse the rules
204/// inside it.
205pub(crate) fn load_css_paths(content: &str) -> Result<FxHashMap<String, CssPath>, String> {
206 let mut iter = content.chars().peekable();
207 let mut paths = FxHashMap::default();
208
209 parse_selectors(content, &mut iter, &mut paths)?;
210 Ok(paths)
211}
212
213pub(crate) fn get_differences(
214 origin: &FxHashMap<String, CssPath>,
215 against: &FxHashMap<String, CssPath>,
216 v: &mut Vec<String>,
217) {
218 for (selector, entry) in origin.iter() {
219 match against.get(selector) {
220 Some(a) => {
221 get_differences(&entry.children, &a.children, v);
222 if selector == ":root" {
223 // We need to check that all variables have been set.
224 for rule in entry.rules.keys() {
225 if !a.rules.contains_key(rule) {
226 v.push(format!(" Missing CSS variable `{rule}` in `:root`"));
227 }
2c00a5a8 228 }
2c00a5a8
XL
229 }
230 }
f2b60f7d 231 None => v.push(format!(" Missing rule `{selector}`")),
2c00a5a8
XL
232 }
233 }
234}
235
923072b8 236pub(crate) fn test_theme_against<P: AsRef<Path>>(
dc9dc135 237 f: &P,
f2b60f7d 238 origin: &FxHashMap<String, CssPath>,
dc9dc135
XL
239 diag: &Handler,
240) -> (bool, Vec<String>) {
f2b60f7d
FG
241 let against = match fs::read_to_string(f)
242 .map_err(|e| e.to_string())
243 .and_then(|data| load_css_paths(&data))
244 {
ba9703b0
XL
245 Ok(c) => c,
246 Err(e) => {
49aad941 247 diag.struct_err(e).emit();
ba9703b0
XL
248 return (false, vec![]);
249 }
250 };
60c5eb7d 251
0731742a 252 let mut ret = vec![];
f2b60f7d 253 get_differences(origin, &against, &mut ret);
2c00a5a8
XL
254 (true, ret)
255}