rcss_layers/
lib.rs

1//! Layered CSS representation
2//! Uses information from `ScopeChain` or one that user provide to build a layered representation of styles.
3//!
4//! Example:
5//! ```no_compile
6//! use crate::*;
7//! let mut chain = LayeredCss::new();
8//! chain.add_style_from_parts("root", 0, "root", "style1{color:red}");
9//! chain.add_style_from_parts("root", 1, "layer2", "style2{color:blue}");
10//! chain.add_style_from_parts("root", 2, "layer3", "style3{color:green}");
11//! let representation = chain
12//!       .styles
13//!       .get("root")
14//!       .unwrap()
15//!       .render(false, "root")
16//!       .unwrap();
17//! let mut expectation = String::from("@layer root,layer2,layer3;");
18//! expectation.push_str("@layer root{style1{color:red}}");
19//! expectation.push_str("@layer layer2{style2{color:blue}}");
20//! expectation.push_str("@layer layer3{style3{color:green}}");
21//! assert_eq!(representation, expectation);
22//! ```
23//!
24use std::{borrow::Cow, collections::BTreeMap};
25
26/// Identifier of scoped style.
27/// rcss uses `&'static str` as scope_id but we use `Cow<'static, str>` to support dynamic extension.
28pub type ScopeId = Cow<'static, str>;
29/// Style representation.
30/// rcss uses `&'static str` as style representation but we use `Cow<'static, str>` to support dynamic extension.
31pub type Style = Cow<'static, str>;
32/// Order of layers is determined by how far a layer is from root.
33pub type Order = u32;
34
35#[derive(Debug, Default, Clone)]
36pub struct LayeredCss {
37    /// We store each chains in a groups
38    /// with root_scope_id as identifier of the group.
39    pub styles: BTreeMap<ScopeId, StyleTree>,
40}
41impl LayeredCss {
42    pub fn new() -> Self {
43        Self {
44            styles: BTreeMap::new(),
45        }
46    }
47    pub fn root_scope_exist(&self, root_scope_id: impl Into<ScopeId>) -> bool {
48        self.styles.contains_key(&root_scope_id.into())
49    }
50    pub fn layer_exist_in_root_scope(
51        &self,
52        root_scope_id: impl Into<ScopeId>,
53        layer_scope_id: impl Into<ScopeId>,
54    ) -> bool {
55        let root_scope_id = root_scope_id.into();
56        let layer_scope_id = layer_scope_id.into();
57        self.styles
58            .get(&root_scope_id)
59            .map(|style_tree| style_tree.uniq_layers.contains_key(&layer_scope_id))
60            .unwrap_or(false)
61    }
62
63    /// Add new style layer to root_scope_id.
64    /// Returns false if layer already exists.
65    pub fn add_style_from_parts(
66        &mut self,
67        root_scope_id: impl Into<ScopeId>,
68        order_in_chain: Order,
69        layer_scope_id: impl Into<ScopeId>,
70        style: impl Into<Style>,
71    ) -> bool {
72        let root_scope_id = root_scope_id.into();
73        let layer_scope_id = layer_scope_id.into();
74        let style = style.into();
75        let layers_of_chain = &mut self
76            .styles
77            .entry(root_scope_id)
78            .or_insert_with(Default::default)
79            .uniq_layers;
80        if let Some((order, _)) = layers_of_chain.get(&layer_scope_id) {
81            debug_assert_eq!(*order, order_in_chain);
82            return false;
83        }
84        if style.is_empty() {
85            return false;
86        }
87        let res = layers_of_chain
88            .insert(layer_scope_id, (order_in_chain, style))
89            .is_none();
90        debug_assert!(res);
91        true
92    }
93    /// Add new style layer.
94    /// Uses `ScopeCommon` implementation to retrieve layer_scope_id and style.
95    /// Returns false if layer already exists.
96    #[cfg(feature = "rcss_enable")]
97    pub fn add_style_with_order<T>(&mut self, root_scope_id: ScopeId, order_in_chain: Order) -> bool
98    where
99        T: rcss::ScopeCommon,
100    {
101        self.add_style_from_parts(root_scope_id, order_in_chain, T::SCOPE_ID, T::STYLE)
102    }
103
104    /// Add chain of styles to registry.
105    /// Uses information from `ScopeChain` implementation,
106    /// to retrieve scope_id of each layer and its style.
107    /// Automatically detects root_scope_id and sets order based on distance from root.
108    /// Ignore empty styles.
109    ///
110    /// Return true if any of the layers is new.
111    #[cfg(feature = "rcss_enable")]
112    pub fn add_style_chain<C>(&mut self, ts_chain: &C) -> bool
113    where
114        C: rcss::extend::in_chain_ops::ScopeChainOps,
115    {
116        let root_scope_id = ts_chain.root_scope_id();
117        // for_each is provide (scope_id, style) starting from bottom of the chain.
118        // So we reverse it to get order from root.
119        let mut chain = vec![];
120
121        ts_chain.for_each(|scope_id, style| {
122            chain.push((scope_id, style));
123        });
124
125        debug_assert_eq!(
126            chain.last().expect("Chain is empty").0,
127            ts_chain.root_scope_id()
128        );
129        let mut any_update = false;
130
131        for (order, (scope_id, style)) in chain.into_iter().rev().enumerate() {
132            any_update |= self.add_style_from_parts(root_scope_id, order as Order, scope_id, style);
133        }
134        any_update
135    }
136}
137/// Store chain of extensions.
138#[derive(Default, Clone, Debug)]
139pub struct StyleTree {
140    /// Style by id of its layer
141    /// We store it in a map[ScopeId => _] and not in map[Order => _] because
142    /// we want to prevent usage of same style in different chains.
143    pub uniq_layers: BTreeMap<ScopeId, (Order, Style)>,
144}
145
146impl StyleTree {
147    /// Returns None if root_scope_id is not registered.
148    /// If always_output_layer is true, it will output layered css even when there only one layer.
149    /// If always_output_layer is false, for single layer it will return plain css without layers.
150    pub fn render(
151        &self,
152        always_output_layer: bool,
153        root_scope_id: impl Into<ScopeId>,
154    ) -> Option<String> {
155        let root_scope_id = root_scope_id.into();
156        let root_scope = self.uniq_layers.get(&root_scope_id)?.clone();
157
158        debug_assert_eq!(root_scope.0, 0, "Root layer must have order 0");
159
160        let mut rest = self
161            .uniq_layers
162            .iter()
163            .filter(|(layer, _)| **layer != root_scope_id)
164            .peekable();
165
166        if !always_output_layer && rest.peek().is_none() {
167            // if no other layers just return plain css without layers.
168            return Some(root_scope.1.to_string());
169        }
170        let mut ordered_layers: Vec<_> = rest
171            .map(|(layer, (order, style))| (order, layer, style))
172            .collect();
173        ordered_layers.sort_by_key(|(order, _, _)| *order);
174        ordered_layers.insert(0, (&root_scope.0, &root_scope_id, &root_scope.1));
175
176        let mut first: bool = true;
177        let mut header = String::from("@layer ");
178
179        for (_, scope_id, _) in &ordered_layers {
180            if !first {
181                header.push(',');
182            }
183            header.push_str(scope_id);
184            first = false;
185        }
186        header.push(';');
187
188        let mut style = header;
189
190        // Push all layer declaration to header
191        for (_, scope_id, layer_impl) in ordered_layers {
192            style.push_str("@layer ");
193            style.push_str(scope_id);
194            style.push_str("{");
195            style.push_str(layer_impl);
196            style.push_str("}");
197        }
198        Some(style)
199    }
200}
201
202#[cfg(test)]
203#[cfg(feature = "rcss_enable")]
204mod test {
205    use rcss::ScopeCommon;
206
207    use super::*;
208    #[test]
209    fn check_raw_api() {
210        let mut chain = LayeredCss::new();
211        assert!(chain.add_style_from_parts("root", 0, "root", "style1{color:red}"));
212        assert!(chain.add_style_from_parts("root", 1, "layer2", "style2{color:blue}"));
213        assert!(chain.add_style_from_parts("root", 2, "layer3", "style3{color:green}"));
214        // adding same element should return false
215        assert!(!chain.add_style_from_parts("root", 2, "layer3", "style3{color:green}"));
216
217        let representation = chain
218            .styles
219            .get("root")
220            .unwrap()
221            .render(false, "root")
222            .unwrap();
223        let mut expectation = String::from("@layer root,layer2,layer3;");
224        expectation.push_str("@layer root{style1{color:red}}");
225        expectation.push_str("@layer layer2{style2{color:blue}}");
226        expectation.push_str("@layer layer3{style3{color:green}}");
227        assert_eq!(representation, expectation);
228    }
229
230    #[test]
231    fn test_chain() {
232        rcss::css! {
233            @rcss(pub struct Style1);
234            .foo{color:red}
235        }
236        rcss::css! {
237            @rcss(pub struct Style2);
238            @rcss(extend Style1);
239            .foo{color:orange}
240        }
241        rcss::css! {
242            @rcss(pub struct Style3);
243            @rcss(extend Style2);
244            .foo{color:green}
245        }
246        let mut chain = LayeredCss::new();
247        assert!(chain.add_style_chain(&Style3::new()));
248        let root_id = Style1::SCOPE_ID;
249        let layer2_id = Style2::SCOPE_ID;
250        let layer3_id = Style3::SCOPE_ID;
251        let root_foo = Style1::new().foo;
252        let layer2_foo = Style2::new().foo.split_whitespace().last().unwrap();
253        let layer3_foo = Style3::new().foo.split_whitespace().last().unwrap();
254
255        let mut expectation = format!("@layer {root_id},{layer2_id},{layer3_id};");
256
257        expectation.push_str(&format!("@layer {root_id}{{.{root_foo}{{color:red}}}}"));
258        expectation.push_str(&format!(
259            "@layer {layer2_id}{{.{layer2_foo}{{color:orange}}}}"
260        ));
261        expectation.push_str(&format!(
262            "@layer {layer3_id}{{.{layer3_foo}{{color:green}}}}"
263        ));
264
265        assert_eq!(
266            chain
267                .styles
268                .get(root_id)
269                .unwrap()
270                .render(false, root_id)
271                .unwrap(),
272            expectation
273        );
274    }
275}