rustdoc/
theme.rs

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
18/// When encountering a `"` or a `'`, returns the whole string, including the quote characters.
19fn 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
46/// Skips a `/*` comment.
47fn 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
55/// Skips a line comment (`//`).
56fn 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
75/// Returns a CSS property name. Ends when encountering a `:` character.
76///
77/// If the `:` character isn't found, returns `None`.
78///
79/// If a `{` character is encountered, returns an error.
80fn 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
94/// Try to get the value of a CSS property (the `#fff` in `color: #fff`). It'll stop when it
95/// encounters a `{` or a `;` character.
96///
97/// It returns the value string and a boolean set to `true` if the value is ended with a `}` because
98/// it means that the parent block is done and that we should notify the parent caller.
99fn 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
116/// This is used to parse inside a CSS `{}` block. If we encounter a new `{` inside it, we consider
117/// it as a new block and therefore recurse into `parse_rules`.
118fn 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 the parent isn't a "normal" CSS selector, we only expect sub-selectors and not CSS
129        // properties.
130        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(), // We don't handle inline selectors like `@import`.
196            _ => handle_common_chars(c, &mut selector, iter),
197        }
198    }
199    Ok(())
200}
201
202/// The entry point to parse the CSS rules. Every time we encounter a `{`, we then parse the rules
203/// inside it.
204pub(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                    // We need to check that all variables have been set.
223                    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}