1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
//! Assembles an expanded tree into valid CSS.
use super::{GenerationError, MarkupDisplay, Formatter};
use crate::parse::parser::{self, ParseNode, ParseTree};

use std::slice::Iter;

#[derive(Debug, Clone)]
pub struct CSSFormatter {
    pub tree : ParseTree,
}

impl CSSFormatter {
    pub fn new(tree : ParseTree) -> Self {
        Self { tree }
    }
}

pub const DEFAULT : &str = "\n";

/// All CSS functions, I might have missed a few.
const CSS_FUNCTIONS : [&str; 57] = [
    "attr", "blur", "brightness", "calc", "circle", "color", "contrast",
    "counter", "counters", "cubic-bezier", "drop-shadow", "ellipse",
    "grayscale", "hsl", "hsla", "hue-rotate", "hwb", "image", "inset",
    "invert", "lab", "lch", "linear-gradient", "matrix", "matrix3d",
    "opacity", "perspective", "polygon", "radial-gradient",
    "repeating-linear-gradient", "repeating-radial-gradient",
    "rgb", "rgba", "rotate", "rotate3d", "rotateX",
    "rotateY", "rotateZ", "saturate", "sepia", "scale", "scale3d",
    "scaleX", "scaleY", "scaleZ", "skew", "skewX", "skewY", "symbols",
    "translate", "translate3d", "translateX", "translateY", "translateZ",
    "url", "var", "supports"
];

/// Some CSS functions use commas as an argument delimiter,
/// some use spaces!  Why not!
const CSS_COMMA_DELIM : [&str; 2] = ["rgba", "hsla"];

/// Intentionally left out "@viewport".  If they decided to make
/// CSS a good and coherent language in the first place, we wouldn't
/// have to deal with ridiculous stuff like this.
const CSS_SPECIAL_SELECTORS : [&str; 12]
    = [ "@charset"   , "@counter-style"       , "@document"
      , "@font-face" , "@font-feature-values" , "@import"
      , "@keyframes" , "@media"               , "@namespace"
      , "@page"      , "@property"            , "@supports"
      ];

/// Special selectors that do not have a body.
const CSS_ONELINE_RULES : [&str; 3]
    = [ CSS_SPECIAL_SELECTORS[0]  //< @charset
      , CSS_SPECIAL_SELECTORS[5]  //< @import
      , CSS_SPECIAL_SELECTORS[8]  //< @namespace
      ];

/// The only four math operations supported by CSS calc(...),
/// or at least I think.
const BINARY_OPERATORS : [&str; 4] = ["+", "-", "*", "/"];

fn convert_value(node : &ParseNode) -> Result<String, GenerationError> {
    match node {
        ParseNode::List(list) => {
            let list = parser::strip(list, false);
            let result = match list.as_slice() {
                [head, tail@..] => {
                    let head = convert_value(head)?;

                    let mut tail_tmp = vec![String::new(); tail.len()];
                    for (i, e) in tail.iter().enumerate() {
                        tail_tmp[i] = convert_value(e)?
                    }
                    let tail = tail_tmp.as_slice();

                    let delim = if CSS_COMMA_DELIM.contains(&head.as_str()) {
                        ", "
                    } else {
                        " "
                    };
                    let args = tail.join(delim);
                    let args = args.trim();
                    if CSS_FUNCTIONS.contains(&head.as_str()) {
                        format!("{}({})", head, args)
                    } else if BINARY_OPERATORS.contains(&head.as_str()) {
                        let args = tail
                            .join(&format!(" {} ", head));
                        format!("({})", args)
                    } else {
                        format!("{} {}", head, args)
                    }
                },
                [] => String::from("")
            };
            Ok(result)
        },
        ParseNode::Number(node)
        | ParseNode::Symbol(node)
        | ParseNode::String(node) =>
            Ok(if node.value.chars().any(|c| c.is_whitespace()) {
                format!("\"{}\"", node.value)
            } else {
                node.value.to_owned()
            }),
        ParseNode::Attribute(_) => Err(GenerationError::new("CSS-value",
                "Incompatible structure (attribute) found in CSS \
                 property value.",
                &node.site()))
    }
}

/// Function responsible for translating a CSS value (i.e.
/// a value of a CSS property) from some s-expression into
/// a valid CSS value.
pub fn css_value(_property : &str, node : &ParseNode)
-> Result<String, GenerationError> {
    // Naïve way (in future consider the type of property,
    //  and take care of special cases):
    convert_value(node)
}

/// # A special-selector / @-rule looks like:
/// S-expr:                          CSS:
/// (@symbol arg)                      -> @symbol arg;
/// (@symbol :attr arg)                -> @symbol (attr: arg);
/// (@symbol (select :prop val))       -> @symbol { select { prop: val; } }
/// (@sym x :attr arg (sel :prop val)) -> @sym x (attr: arg) { sel { prop: val; } }
fn generate_special_selector
    (f: Formatter,
     selector: &str,
     arguments: Iter<ParseNode>) -> Result<(), GenerationError> {
    // Deal with oneline rules quickly.
    if CSS_ONELINE_RULES.contains(&selector) {
        write!(f, "{} ", selector)?;
        for arg in arguments {
            match arg {
                ParseNode::Attribute(attr) => {
                    let kw = &attr.keyword;
                    write!(f, "({}: {}) ", kw, css_value(kw, &*attr.node)?)?;
                },
                _ => write!(f, "{} ", css_value(selector, arg)?)?
            }
        }
        writeln!(f, ";")?;
        return Ok(());
    }
    // @-rules with nested elements!
    write!(f, "{} ", selector)?;

    let mut parsing_rules = false;
    let unexpected_node = |node: &ParseNode, rules: bool| {
        if rules {
            Err(GenerationError::new("CSS",
                "Expected list (i.e. a CSS rule) here!", &node.site()))
        } else {
            Ok(())
        }
    };

    for arg in arguments {
        match arg {
            ParseNode::Attribute(attr) => {
                unexpected_node(&arg, parsing_rules)?;
                let kw = &attr.keyword;
                write!(f, "({}: {}) ", kw, css_value(kw, &*attr.node)?)?;
            },
            ParseNode::List(rule) => {  // Now we parse nested rules!
                if !parsing_rules {
                    writeln!(f, "{{")?;
                }
                parsing_rules = true;
                generate_css_rule(f, &rule)?;
            },
            _ => {
                unexpected_node(&arg, parsing_rules)?;
                write!(f, "{} ", css_value(selector, arg)?)?;
            }
        }
    }
    writeln!(f, "}}")?;
    Ok(())
}

fn generate_css_rule(f: Formatter, list: &[ParseNode]) -> Result<(), GenerationError> {
    let stripped = parser::strip(list, false);
    let mut iter = stripped.iter();
    let mut prop_i = 0; // Index of first property.
    // TODO: Selector functions such as nth-child(...), etc.
    // e.g. (ul li(:nth-child (+ 2n 1))) -> ul li:nth-child(2n + 1).
    let mut selectors = iter.clone()
        .take_while(|n| { prop_i += 1; n.atomic().is_some() })
        .map(|n| n.atomic().unwrap()) // We've checked.
        .peekable();

    // Check we were actually provided with
    // some selectors.
    let head = if let Some(head) = selectors.next() {
        head
    } else {
        return Err(GenerationError::new("CSS",
            "CSS selector(s) missing. \
             Expected a symbol/identifier node, none was found!",
             &list[0].site()));
    };

    // Handle special @-rule selectors.
    if CSS_SPECIAL_SELECTORS.contains(&head.value.as_ref()) {
        iter.next();  //< Throw away the head.
        return generate_special_selector(f, &head.value, iter);
    }

    // Join the selectors togeher.
    write!(f, "{} ", head.value)?;
    for selector in selectors {
        write!(f, "{} ", selector.value)?;
    }
    writeln!(f, "{{")?;

    let properties = iter.skip(prop_i - 1);

    for property in properties {
        if let ParseNode::Attribute(property) = property {
            let value = &property.node;
            writeln!(f, "  {}: {};",
                     &property.keyword,
                     css_value(&property.keyword, value)?)?;
        } else {
            return Err(GenerationError::new("CSS",
                "CSS property-value pairs must be in the \
                 form of attributes, i.e. `:property value`.",
                 &property.site()));
        }
    }
    writeln!(f, "}}")?;

    Ok(())
}

impl MarkupDisplay for CSSFormatter {

    fn document(&self) -> Result<String, GenerationError> {
        let mut doc = String::new();
        if self.tree.is_empty() {
            return Ok(String::from(DEFAULT));
        }
        doc += &self.display()?;
        doc += "\n/* Generated from symbolic-expressions, with SEAM */\n";
        Ok(doc)
    }

    fn generate(&self, f : Formatter)
    -> Result<(), GenerationError> {
        let mut tree_iter = self.tree.iter().peekable();
        while let Some(node) = tree_iter.next() {
            match node {
                ParseNode::List(list) => {
                    generate_css_rule(f, list)?;
                },
                ParseNode::Attribute(attr) => {
                    let site = attr.site.to_owned();
                    return Err(GenerationError::new("CSS",
                        "Attribute not expected here, CSS documents \
                         are supposed to be a series of selectors \
                         and property-value pairs, wrapped in parentheses.",
                         &site));
                },
                ParseNode::Symbol(node)
                | ParseNode::Number(node)
                | ParseNode::String(node) => {
                    let site = node.site.to_owned();
                    if node.value.trim().is_empty() {
                        continue;
                    }
                    return Err(GenerationError::new("CSS",
                        "Symbolic node not expected here, CSS documents \
                         are supposed to be a series of selectors \
                         and property-value pairs, wrapped in parentheses.",
                         &site));
                }
            }
        }
        Ok(())
    }
}