serval/
style.rs

1// use super::css::{Rule, Selector, SimpleSelector, Specificity, Stylesheet, Value};
2use super::css;
3// use super::dom::{ElementData, Node, NodeType};
4use super::dom;
5use super::dom::Node;
6use std::collections::HashMap;
7
8pub type CssPropertyMap = HashMap<String, css::Value>;
9
10#[derive(PartialEq)]
11pub enum Display {
12    Inline,
13    Block,
14    None,
15}
16
17pub struct StyledNode<'a> {
18    pub node: &'a Node,
19    pub css_specified_values: CssPropertyMap,
20    pub children: Vec<StyledNode<'a>>,
21}
22
23impl<'a> StyledNode<'a> {
24    pub fn value(&'a self, name: &str) -> Option<&'a css::Value> {
25        self.css_specified_values.get(name)
26    }
27
28    pub fn lookup(
29        &'a self,
30        name: &str,
31        fallback_name: &str,
32        default: &'a css::Value,
33    ) -> &'a css::Value {
34        self.value(name)
35            .unwrap_or_else(|| self.value(fallback_name).unwrap_or_else(|| default))
36    }
37
38    pub fn display(&self) -> Display {
39        match self.value("display") {
40            Some(css::Value::Keyword(s)) => match s.as_str() {
41                "block" => Display::Block,
42                "none" => Display::None,
43                _ => Display::Inline,
44            },
45            _ => Display::Inline,
46        }
47    }
48}
49
50pub fn style_tree<'a>(root: &'a Node, stylesheet: &'a css::Stylesheet) -> StyledNode<'a> {
51    StyledNode {
52        node: root,
53        css_specified_values: match root {
54            Node::Element(data) => css_specified_values(data, stylesheet),
55            Node::Text(_) => HashMap::new(),
56        },
57        children: root
58            .children()
59            .iter()
60            .map(|child| style_tree(child, stylesheet))
61            .collect(),
62    }
63}
64
65fn css_specified_values(elem: &dom::ElementData, stylesheet: &css::Stylesheet) -> CssPropertyMap {
66    let mut values = HashMap::new();
67    let mut rules = matching_rules(elem, stylesheet);
68
69    // Go through the rules from lowest to highest specificity.
70    rules.sort_by(|&(a, _), &(b, _)| a.cmp(&b));
71    for (_, rule) in rules {
72        for declaration in &rule.declarations {
73            values.insert(declaration.name.clone(), declaration.value.clone());
74        }
75    }
76    values
77}
78
79type MatchedRule<'a> = (css::Specifity, &'a css::Rule);
80
81fn matching_rules<'a>(
82    elem: &'a dom::ElementData,
83    stylesheet: &'a css::Stylesheet,
84) -> Vec<MatchedRule<'a>> {
85    stylesheet
86        .rules
87        .iter()
88        .filter_map(|rule| match_rule(elem, rule))
89        .collect()
90}
91
92fn match_rule<'a>(elem: &dom::ElementData, rule: &'a css::Rule) -> Option<MatchedRule<'a>> {
93    match_selectors(elem, &rule.selectors).map(|selector| (selector.specifity(), rule))
94}
95
96fn match_selectors<'a>(
97    elem: &dom::ElementData,
98    sorted_selectors: &'a css::SortedSelectors,
99) -> Option<&'a css::Selector> {
100    // Find the first (most specific) matching selector.
101    sorted_selectors
102        .selectors
103        .iter()
104        .find(|selector| matches(elem, *selector))
105}
106
107fn matches(elem: &dom::ElementData, selector: &css::Selector) -> bool {
108    match selector {
109        css::Selector::Simple(simple_selector) => matches_simple_selector(elem, simple_selector),
110    }
111}
112
113fn matches_simple_selector(elem: &dom::ElementData, selector: &css::SimpleSelector) -> bool {
114    // Check type selector
115    if !selector.tag_name.iter().all(|name| elem.tag_name == *name) {
116        return false;
117    }
118
119    // Check ID selector
120    if !selector.id.iter().all(|id| elem.id() == Some(id)) {
121        return false;
122    }
123
124    // Check class selectors
125    let elem_classes = elem.classes();
126    if !selector
127        .classes
128        .iter()
129        .all(|class| elem_classes.contains(&**class))
130    {
131        return false;
132    }
133
134    // We didn't find any non-matching selector components.
135    true
136}
137
138#[cfg(test)]
139mod test {
140    use super::*;
141    use crate::css;
142    use crate::dom;
143    use maplit::*;
144
145    #[test]
146    fn simple_selector_match_test() {
147        let div_selector = css::SimpleSelector {
148            tag_name: Some("div".to_string()),
149            ..Default::default()
150        };
151
152        let div_elem = dom::ElementData {
153            tag_name: "div".to_string(),
154            ..Default::default()
155        };
156
157        let p_elem = dom::ElementData {
158            tag_name: "p".to_string(),
159            ..Default::default()
160        };
161
162        assert!(matches_simple_selector(&div_elem, &div_selector));
163        assert!(!matches_simple_selector(&p_elem, &div_selector));
164
165        let class_foo_selector = css::SimpleSelector {
166            classes: btreeset! { "foo".to_string() },
167            ..Default::default()
168        };
169
170        assert!(!matches_simple_selector(&div_elem, &class_foo_selector));
171        assert!(!matches_simple_selector(&p_elem, &class_foo_selector));
172
173        let div_class_foo_elem = dom::ElementData {
174            tag_name: "div".to_string(),
175            attrs: btreemap! {
176                "class".to_string() => "foo".to_string()
177            },
178            ..Default::default()
179        };
180
181        let div_class_bar_elem = dom::ElementData {
182            tag_name: "div".to_string(),
183            attrs: btreemap! {
184                "class".to_string() => "bar".to_string()
185            },
186            ..Default::default()
187        };
188
189        let div_class_foo_bar_elem = dom::ElementData {
190            tag_name: "div".to_string(),
191            attrs: btreemap! {
192                "class".to_string() => "foo bar".to_string()
193            },
194            ..Default::default()
195        };
196
197        assert!(matches_simple_selector(
198            &div_class_foo_elem,
199            &class_foo_selector
200        ));
201
202        assert!(!matches_simple_selector(
203            &div_class_bar_elem,
204            &class_foo_selector
205        ));
206
207        assert!(matches_simple_selector(
208            &div_class_foo_bar_elem,
209            &class_foo_selector
210        ));
211
212        let universal_selector: css::SimpleSelector = Default::default();
213        assert!(matches_simple_selector(&div_elem, &universal_selector));
214
215        let div_id_foo_elem = dom::ElementData {
216            tag_name: "div".to_string(),
217            attrs: btreemap! {
218                "id".to_string() => "foo".to_string()
219            },
220            ..Default::default()
221        };
222
223        assert!(matches_simple_selector(
224            &div_id_foo_elem,
225            &css::SimpleSelector {
226                id: Some("foo".to_string()),
227                ..Default::default()
228            }
229        ));
230
231        assert!(!matches_simple_selector(
232            &div_id_foo_elem,
233            &css::SimpleSelector {
234                id: Some("xxx".to_string()),
235                ..Default::default()
236            }
237        ));
238
239        let div_id_foo_class1_class2_elem = dom::ElementData {
240            tag_name: "div".to_string(),
241            attrs: btreemap! {
242                "id".to_string() => "foo".to_string(),
243                "class".to_string() => "class1 class2".to_string(),
244            },
245            ..Default::default()
246        };
247
248        assert!(matches_simple_selector(
249            &div_id_foo_class1_class2_elem,
250            &css::SimpleSelector {
251                id: Some("foo".to_string()),
252                classes: btreeset! {"class1".to_string()},
253                ..Default::default()
254            }
255        ));
256
257        assert!(!matches_simple_selector(
258            &div_id_foo_class1_class2_elem,
259            &css::SimpleSelector {
260                id: Some("foo".to_string()),
261                classes: btreeset! {"class1 classxx".to_string()},
262                ..Default::default()
263            }
264        ));
265    }
266
267    #[test]
268    fn match_selectors_test() {
269        let div = dom::ElementData {
270            tag_name: "div".to_string(),
271            ..Default::default()
272        };
273
274        assert!(match_selectors(&div, &css::SortedSelectors::new(vec![])).is_none());
275        assert!(match_selectors(
276            &div,
277            &css::SortedSelectors::new(vec![css::Selector::id("XXX")]),
278        )
279        .is_none());
280
281        assert_eq!(
282            match_selectors(
283                &div,
284                &css::SortedSelectors::new(vec![css::Selector::universal()]),
285            ),
286            Some(&css::Selector::universal())
287        );
288
289        let elem = dom::ElementData {
290            tag_name: "div".to_string(),
291            attrs: btreemap! {
292                "id".to_string() => "foo".to_string(),
293                "class".to_string() => "class1 class2".to_string(),
294            },
295            ..Default::default()
296        };
297
298        assert_eq!(
299            match_selectors(
300                &elem,
301                &css::SortedSelectors::new(vec![
302                    css::Selector::tag("div"),
303                    css::Selector::class(&["class1"]),
304                    css::Selector::id("foo"),
305                ])
306            ),
307            Some(&css::Selector::id("foo")),
308            "id should win"
309        );
310
311        assert_eq!(
312            match_selectors(
313                &elem,
314                &css::SortedSelectors::new(vec![
315                    css::Selector::tag("div"),
316                    css::Selector::class(&["class1"]),
317                ])
318            ),
319            Some(&css::Selector::class(&["class1"])),
320            "class should win"
321        );
322
323        assert_eq!(
324            match_selectors(
325                &elem,
326                &css::SortedSelectors::new(vec![
327                    css::Selector::class(&["class1"]),
328                    css::Selector::class(&["class1", "class2"]),
329                    css::Selector::class(&["class2"]),
330                ])
331            ),
332            Some(&css::Selector::class(&["class1", "class2"])),
333            "More classes should win"
334        );
335    }
336
337    #[test]
338    fn matching_rules_test() {
339        let stylesheet = css::Stylesheet {
340            rules: vec![
341                css::Rule {
342                    selectors: css::SortedSelectors::new(vec![css::Selector::tag("div")]),
343                    declarations: vec![css::Declaration::color((0, 0, 0))],
344                },
345                css::Rule {
346                    selectors: css::SortedSelectors::new(vec![css::Selector::tag("foo")]),
347                    declarations: vec![css::Declaration::color((1, 1, 1))],
348                },
349                css::Rule {
350                    selectors: css::SortedSelectors::new(vec![css::Selector::tag("div")]),
351                    declarations: vec![css::Declaration::color((2, 2, 2))],
352                },
353            ],
354        };
355
356        let div = dom::ElementData {
357            tag_name: "div".to_string(),
358            ..Default::default()
359        };
360
361        let matched_declarations = matching_rules(&div, &stylesheet)
362            .into_iter()
363            .map(|(_speficity, rule)| &rule.declarations)
364            .collect::<Vec<_>>();
365
366        assert_eq!(
367            matched_declarations,
368            vec![
369                &vec![css::Declaration::color((0, 0, 0))],
370                &vec![css::Declaration::color((2, 2, 2))],
371            ]
372        );
373    }
374
375    #[test]
376    fn css_specified_values_test() {
377        let stylesheet = css::Stylesheet {
378            rules: vec![
379                css::Rule {
380                    selectors: css::SortedSelectors::new(vec![css::Selector::tag("div")]),
381                    declarations: vec![css::Declaration::color((0, 0, 0))],
382                },
383                css::Rule {
384                    selectors: css::SortedSelectors::new(vec![css::Selector::id("foo")]),
385                    declarations: vec![css::Declaration::color((1, 1, 1))],
386                },
387                css::Rule {
388                    selectors: css::SortedSelectors::new(vec![css::Selector::id("foo")]),
389                    declarations: vec![css::Declaration::color((2, 2, 2))],
390                },
391                css::Rule {
392                    selectors: css::SortedSelectors::new(vec![css::Selector::tag("div")]),
393                    declarations: vec![css::Declaration::color((3, 3, 3))],
394                },
395            ],
396        };
397
398        let div = dom::ElementData {
399            tag_name: "div".to_string(),
400            attrs: btreemap! {
401                "id".to_string() => "foo".to_string()
402            },
403            ..Default::default()
404        };
405
406        let values = css_specified_values(&div, &stylesheet);
407        assert_eq!(
408            values,
409            hashmap! { "color".to_string() => css::Value::color((2, 2, 2)) }
410        );
411    }
412}