cadmus_core/document/html/
style.rs

1use super::css::{AttributeOperator, Combinator, PseudoClass};
2use super::css::{CssParser, Rule, Selector, SimpleSelector};
3use super::dom::NodeRef;
4use fxhash::FxHashMap;
5
6pub type PropertyMap = FxHashMap<String, String>;
7
8#[derive(Debug, Clone)]
9pub struct StyleSheet {
10    pub rules: Vec<Rule>,
11}
12
13impl StyleSheet {
14    pub fn new() -> Self {
15        StyleSheet { rules: Vec::new() }
16    }
17
18    pub fn append(&mut self, other: &mut Self, sort: bool) {
19        if sort {
20            other.sort();
21        }
22        self.rules.append(&mut other.rules);
23    }
24
25    pub fn sort(&mut self) {
26        self.rules
27            .sort_by_cached_key(|rule| rule.selector.specificity());
28    }
29}
30
31pub fn specified_values(node: NodeRef, stylesheet: &StyleSheet) -> PropertyMap {
32    let mut props = FxHashMap::default();
33    let mut important = Vec::new();
34
35    for rule in stylesheet
36        .rules
37        .iter()
38        .filter(|rule| rule.selector.matches(node))
39    {
40        for declaration in &rule.declarations {
41            if declaration.important {
42                important.push([&declaration.name, &declaration.value]);
43            } else {
44                expand_and_insert(&declaration.name, &declaration.value, &mut props);
45            }
46        }
47    }
48
49    let local_declarations = node
50        .attribute("style")
51        .map(|text| CssParser::new(text).parse_declarations())
52        .unwrap_or_default();
53
54    for declaration in &local_declarations {
55        expand_and_insert(&declaration.name, &declaration.value, &mut props);
56    }
57
58    for [name, value] in important {
59        expand_and_insert(name, value, &mut props);
60    }
61
62    props
63}
64
65impl Selector {
66    fn matches(&self, node: NodeRef) -> bool {
67        let index = self.simple_selectors.len().saturating_sub(1);
68        self.matches_rec(node, index)
69    }
70
71    fn matches_rec(&self, node: NodeRef, index: usize) -> bool {
72        let comb = self.combinators[index];
73        let selec = &self.simple_selectors[index];
74
75        if !selec.matches(node) {
76            return false;
77        }
78
79        match comb {
80            Combinator::Child => {
81                if let Some(parent) = node.parent_element() {
82                    if self.matches_rec(parent, index - 1) {
83                        return true;
84                    }
85                }
86                false
87            }
88            Combinator::Descendant => {
89                for anc in node.ancestor_elements() {
90                    if self.matches_rec(anc, index - 1) {
91                        return true;
92                    }
93                }
94                false
95            }
96            Combinator::NextSibling => {
97                if let Some(nsib) = node.previous_sibling_element() {
98                    if self.matches_rec(nsib, index - 1) {
99                        return true;
100                    }
101                }
102                false
103            }
104            Combinator::SubsequentSibling => {
105                for sib in node.previous_sibling_elements() {
106                    if self.matches_rec(sib, index - 1) {
107                        return true;
108                    }
109                }
110                false
111            }
112            Combinator::None => true,
113        }
114    }
115}
116
117impl SimpleSelector {
118    fn matches(&self, node: NodeRef) -> bool {
119        if self
120            .tag_name
121            .iter()
122            .any(|name| node.tag_name() != Some(name))
123        {
124            return false;
125        }
126
127        if self
128            .classes
129            .iter()
130            .any(|class| node.classes().all(|c| c != class))
131        {
132            return false;
133        }
134
135        if self.id.iter().any(|id| node.id() != Some(id)) {
136            return false;
137        }
138
139        if self.attributes.iter().any(|attr| {
140            node.attribute(&attr.name)
141                .map(|value| attr.operator.matches(value))
142                != Some(true)
143        }) {
144            return false;
145        }
146
147        if self.pseudo_classes.iter().any(|pc| !pc.matches(node)) {
148            return false;
149        }
150
151        true
152    }
153}
154
155impl AttributeOperator {
156    fn matches(&self, value: &str) -> bool {
157        match self {
158            AttributeOperator::Exists => true,
159            AttributeOperator::Matches(v) => v == value,
160            AttributeOperator::Contains(v) => value.split_whitespace().any(|value| value == v),
161            AttributeOperator::StartsWith(v) => {
162                v == value || (value.starts_with(v) && value[v.len()..].starts_with('-'))
163            }
164        }
165    }
166}
167
168impl PseudoClass {
169    fn matches(&self, node: NodeRef) -> bool {
170        match self {
171            PseudoClass::FirstChild => node.previous_sibling_element().is_none(),
172            PseudoClass::LastChild => node.next_sibling_element().is_none(),
173        }
174    }
175}
176
177fn expand_and_insert(name: &str, value: &str, props: &mut PropertyMap) {
178    match name {
179        "margin" | "padding" => {
180            let values = value.split_whitespace().collect::<Vec<&str>>();
181            match values.len() {
182                1 => {
183                    props.insert(format!("{}-top", name), value.to_string());
184                    props.insert(format!("{}-right", name), value.to_string());
185                    props.insert(format!("{}-bottom", name), value.to_string());
186                    props.insert(format!("{}-left", name), value.to_string());
187                }
188                2 => {
189                    let vertical = values[0];
190                    let horizontal = values[1];
191                    props.insert(format!("{}-top", name), vertical.to_string());
192                    props.insert(format!("{}-right", name), horizontal.to_string());
193                    props.insert(format!("{}-bottom", name), vertical.to_string());
194                    props.insert(format!("{}-left", name), horizontal.to_string());
195                }
196                3 => {
197                    let top = values[0];
198                    let horizontal = values[1];
199                    let bottom = values[2];
200                    props.insert(format!("{}-top", name), top.to_string());
201                    props.insert(format!("{}-right", name), horizontal.to_string());
202                    props.insert(format!("{}-bottom", name), bottom.to_string());
203                    props.insert(format!("{}-left", name), horizontal.to_string());
204                }
205                4 => {
206                    let top = values[0];
207                    let right = values[1];
208                    let bottom = values[2];
209                    let left = values[3];
210                    props.insert(format!("{}-top", name), top.to_string());
211                    props.insert(format!("{}-right", name), right.to_string());
212                    props.insert(format!("{}-bottom", name), bottom.to_string());
213                    props.insert(format!("{}-left", name), left.to_string());
214                }
215                _ => (),
216            }
217        }
218        // TODO: border -> border-{top,right,bottom,left}-{width,style,color}
219        // border-left -> border-left-{width,style,color}
220        // border-style -> border-{top,right,bottom,left}-style
221        _ => {
222            props.insert(name.to_string(), value.to_string());
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::super::css::CssParser;
230    use super::super::xml::XmlParser;
231    use super::specified_values;
232
233    #[test]
234    fn simple_style() {
235        let xml1 = XmlParser::new("<a class='c x y' style='c: 7'/>").parse();
236        let xml2 = XmlParser::new("<a id='e' class='x y'/>").parse();
237        let mut css = CssParser::new(
238            "a { b: 23 }\
239                                      .c.x.y { b: 6; c: 3 }\
240                                      #e { b: 5 }\
241                                      .y { b: 2 }",
242        )
243        .parse();
244        css.sort();
245        let n1 = xml1.root().first_child().unwrap();
246        let n2 = xml2.root().first_child().unwrap();
247        assert_eq!(
248            specified_values(n1, &css),
249            [
250                ("b".to_string(), "6".to_string()),
251                ("c".to_string(), "7".to_string())
252            ]
253            .iter()
254            .cloned()
255            .collect()
256        );
257        assert_eq!(
258            specified_values(n2, &css),
259            [("b".to_string(), "5".to_string())]
260                .iter()
261                .cloned()
262                .collect()
263        );
264    }
265}