seam/assemble/
css.rs

1//! Assembles an expanded tree into valid CSS.
2use super::{GenerationError, MarkupFormatter, Formatter};
3use crate::parse::parser::{ParseNode, ParseTree};
4
5use std::slice::Iter;
6
7#[derive(Debug, Clone)]
8pub struct CSSFormatter<'a> {
9    pub tree: ParseTree<'a>,
10}
11
12impl<'a> CSSFormatter<'a> {
13    pub fn new(tree: ParseTree<'a>) -> Self {
14        Self { tree }
15    }
16}
17
18pub const DEFAULT: &str = "\n";
19
20/// All CSS functions, I might have missed a few.
21const CSS_FUNCTIONS: [&str; 58] = [
22    "attr", "blur", "brightness", "calc", "circle", "color", "contrast",
23    "counter", "counters", "cubic-bezier", "drop-shadow", "ellipse", "format",
24    "grayscale", "hsl", "hsla", "hue-rotate", "hwb", "image", "inset",
25    "invert", "lab", "lch", "linear-gradient", "matrix", "matrix3d",
26    "opacity", "perspective", "polygon", "radial-gradient",
27    "repeating-linear-gradient", "repeating-radial-gradient",
28    "rgb", "rgba", "rotate", "rotate3d", "rotateX",
29    "rotateY", "rotateZ", "saturate", "sepia", "scale", "scale3d",
30    "scaleX", "scaleY", "scaleZ", "skew", "skewX", "skewY", "symbols",
31    "translate", "translate3d", "translateX", "translateY", "translateZ",
32    "url", "var", "supports"
33];
34
35/// Some CSS functions use commas as an argument delimiter,
36/// some use spaces!  Why not!
37const CSS_COMMA_DELIM: [&str; 2] = ["rgba", "hsla"];
38
39/// Intentionally left out "@viewport".  If they decided to make
40/// CSS a good and coherent language in the first place, we wouldn't
41/// have to deal with ridiculous stuff like this.
42const CSS_SPECIAL_SELECTORS: [&str; 12]
43    = [ "@charset"   , "@counter-style"       , "@document"
44      , "@font-face" , "@font-feature-values" , "@import"
45      , "@keyframes" , "@media"               , "@namespace"
46      , "@page"      , "@property"            , "@supports"
47      ];
48
49/// Special selectors that do not have a body.
50const CSS_ONELINE_RULES: [&str; 3]
51    = [ CSS_SPECIAL_SELECTORS[0]  //< @charset
52      , CSS_SPECIAL_SELECTORS[5]  //< @import
53      , CSS_SPECIAL_SELECTORS[8]  //< @namespace
54      ];
55
56/// Special selectors that do not have nested selectors as their body.
57const CSS_NON_NESTED_SELECTORS: [&str; 3]
58    = [ CSS_SPECIAL_SELECTORS[3]  //< @font-face
59      , CSS_SPECIAL_SELECTORS[9]  //< @page
60      , CSS_SPECIAL_SELECTORS[10] //< @property
61      ];
62
63/// The only four math operations supported by CSS calc(...),
64/// or at least I think.
65const BINARY_OPERATORS: [&str; 4] = ["+", "-", "*", "/"];
66
67fn convert_value<'a>(node: &'a ParseNode<'a>) -> Result<String, GenerationError<'a>> {
68    match node {
69        ParseNode::List { nodes: list, .. } => {
70            let result = match &**list {
71                [head, tail@..] => {
72                    let head = convert_value(head)?;
73
74                    let mut tail_tmp = vec![String::new(); tail.len()];
75                    for (i, e) in tail.iter().enumerate() {
76                        tail_tmp[i] = convert_value(e)?
77                    }
78                    let tail = tail_tmp.as_slice();
79
80                    let delim = if CSS_COMMA_DELIM.contains(&head.as_str()) {
81                        ", "
82                    } else {
83                        " "
84                    };
85                    let args = tail.join(delim);
86                    let args = args.trim();
87                    if CSS_FUNCTIONS.contains(&head.as_str()) {
88                        format!("{}({})", head, args)
89                    } else if BINARY_OPERATORS.contains(&head.as_str()) {
90                        let args = tail
91                            .join(&format!(" {} ", head));
92                        format!("({})", args)
93                    } else {
94                        format!("{} {}", head, args)
95                    }
96                },
97                [] => String::from("")
98            };
99            Ok(result)
100        },
101        ParseNode::Number(node)
102        | ParseNode::Symbol(node)
103        | ParseNode::String(node) =>
104            Ok(if node.value.chars().any(|c| c.is_whitespace()) {
105                format!("{:?}", node.value)
106            } else {
107                node.value.to_owned()
108            }),
109        ParseNode::Raw(node) => {
110            Ok(node.value.to_owned())
111        },
112        ParseNode::Attribute { .. } => Err(GenerationError::new("CSS-value",
113                "Incompatible structure (attribute) found in CSS \
114                 property value.",
115                &node.site()))
116    }
117}
118
119/// Function responsible for translating a CSS value (i.e.
120/// a value of a CSS property) from some s-expression into
121/// a valid CSS value.
122pub fn css_value<'a>(_property: &str, node: &'a ParseNode<'a>)
123-> Result<String, GenerationError<'a>> {
124    // Naïve way (in future consider the type of property,
125    //  and take care of special cases):
126    convert_value(node)
127}
128
129/// # A special-selector / @-rule looks like:
130/// S-expr:                          CSS:
131/// (@symbol arg)                      -> @symbol arg;
132/// (@symbol :attr arg)                -> @symbol (attr: arg); OR @symbol { attr: arg }
133/// (@symbol (select :prop val))       -> @symbol { select { prop: val; } }
134/// (@sym x :attr arg (sel :prop val)) -> @sym x (attr: arg) { sel { prop: val; } }
135fn generate_special_selector<'a>(f: Formatter,
136                                 selector: &str,
137                                 mut arguments: Iter<'a, ParseNode<'a>>)
138-> Result<(), GenerationError<'a>> {
139    let mut parsing_rules: bool = false;
140    let unexpected_node = |node: &'a ParseNode<'a>, rules: bool| {
141        if rules {
142            Err(GenerationError::new("CSS",
143                "Expected list (i.e. a CSS rule) here!",
144                &node.site()))
145        } else {
146            Ok(())
147        }
148    };
149    // Deal with one-line rules quickly.
150    if CSS_ONELINE_RULES.contains(&selector) {
151        write!(f, "{} ", selector)?;
152        arguments.next();  // Skip `@`-selector.
153        for arg in arguments {
154            match arg {
155                ParseNode::Attribute { ref keyword, node, .. } => {
156                    write!(f, "({}: {}) ", keyword, css_value(keyword, &*node)?)?;
157                },
158                _ => write!(f, "{} ", css_value(selector, arg)?)?
159            }
160        }
161        writeln!(f, ";")?;
162        return Ok(());
163    }
164
165    // Handle @-rules with nested elements.
166    write!(f, "{} ", selector)?;
167    arguments.next();  // Skip `@`-selector.
168
169    for arg in arguments {
170        match arg {
171            ParseNode::Attribute { ref keyword, node, .. } => {
172                unexpected_node(&arg, parsing_rules)?;
173                write!(f, "({}: {}) ", keyword, css_value(keyword, &*node)?)?;
174            },
175            ParseNode::List { nodes: ref rule, leading_whitespace, .. } => {
176                // Now we parse nested rules!
177                if !parsing_rules {
178                    writeln!(f, "{{")?;
179                }
180                parsing_rules = true;
181                write!(f, "{}", leading_whitespace)?;
182                generate_css_rule(f, rule.into_iter())?;
183            },
184            _ => {
185                unexpected_node(&arg, parsing_rules)?;
186                write!(f, "{} ", css_value(selector, arg)?)?;
187            }
188        }
189    }
190    write!(f, "}}")?;
191    Ok(())
192}
193
194fn generate_css_rule<'a>(f: Formatter, iter: Iter<'a, ParseNode<'a>>) -> Result<(), GenerationError<'a>> {
195    let mut prop_i = 0; // Index of first property.
196    // TODO: Selector functions such as nth-child(...), etc.
197    // e.g. (ul li(:nth-child (+ 2n 1))) -> ul li:nth-child(2n + 1).
198    let mut selectors = iter.clone()
199        .take_while(|n| { prop_i += 1; n.atomic().is_some() })
200        .map(|n| n.atomic().unwrap()) // We've checked.
201        .peekable();
202
203    // Check we were actually provided with
204    // some selectors.
205    let head = if let Some(head) = selectors.next() {
206        head
207    } else {
208        return Err(GenerationError::new("CSS",
209            "CSS selector(s) missing. \
210             Expected a symbol/identifier node, none was found!",
211             &selectors.peek().unwrap().site));
212    };
213
214    // Handle special @-rule selectors.
215    if CSS_SPECIAL_SELECTORS.contains(&head.value.as_ref())
216    && !CSS_NON_NESTED_SELECTORS.contains(&head.value.as_ref()) {
217        // Don't do anything special for non-nested selectors.
218        return generate_special_selector(f, &head.value, iter);
219    }
220
221    // Join the selectors togeher.
222    write!(f, "{} ", head.value)?;
223    for selector in selectors {
224        write!(f, "{} ", selector.value)?;
225    }
226    writeln!(f, "{{")?;
227
228    let properties = iter.skip(prop_i - 1);
229
230    for property in properties {
231        let ParseNode::Attribute { ref node, ref keyword, .. } = property else {
232            return Err(GenerationError::new("CSS",
233                "CSS property-value pairs must be in the \
234                 form of attributes, i.e. `:property value`.",
235                 &property.site()));
236        };
237        writeln!(f, "  {}: {};", keyword, css_value(keyword, node)?)?;
238    }
239    write!(f, "}}")?;
240    Ok(())
241}
242
243impl<'a> MarkupFormatter for CSSFormatter<'a> {
244    fn document(&self) -> Result<String, GenerationError> {
245        let mut doc = String::new();
246        if self.tree.is_empty() {
247            return Ok(String::from(DEFAULT));
248        }
249        doc += &self.display()?;
250        Ok(doc)
251    }
252
253    fn generate(&self, f: Formatter)
254    -> Result<(), GenerationError> {
255        let mut tree_iter = self.tree.iter().peekable();
256        while let Some(node) = tree_iter.next() {
257            match node {
258                ParseNode::List { nodes: list, leading_whitespace, .. } => {
259                    write!(f, "{}", leading_whitespace)?;
260                    generate_css_rule(f, list.into_iter())?;
261                },
262                ParseNode::Attribute { site, .. }  => {
263                    return Err(GenerationError::new("CSS",
264                        "Attribute not expected here, CSS documents \
265                         are supposed to be a series of selectors \
266                         and property-value pairs, wrapped in parentheses.",
267                         &site.to_owned()));
268                },
269                ParseNode::Symbol(node)
270                | ParseNode::Number(node)
271                | ParseNode::String(node)
272                | ParseNode::Raw(node) => {
273                    let site = node.site.to_owned();
274                    return Err(GenerationError::new("CSS",
275                        "Symbolic node not expected here, CSS documents \
276                         are supposed to be a series of selectors \
277                         and property-value pairs, wrapped in parentheses.",
278                         &site));
279                }
280            }
281        }
282        Ok(())
283    }
284}