commit 6af58d4965ddd73b9cb4ceb9f459236a8b2d68fc
parent 90b916d51475fec16e8fe421cb70e7351e527cdf
Author: Demonstrandum <moi@knutsen.co>
Date:   Sat,  4 Jul 2020 23:07:42 +0100
Added one macro, allow CSS in HTML, better errors.
 - `%include' macro was added.
 - <style></style> contents are formatted with the CSS generator.
 - Error messages now display filenames.
Diffstat:
13 files changed, 282 insertions(+), 42 deletions(-)
diff --git a/README.md b/README.md
@@ -21,6 +21,15 @@ client.
  - HTML
  - CSS
 
+## TODO
+ - More supported formats (`JSON`, `JS`, `TOML`, &c.).
+ - Add more helpful macros.
+ - Add user defined macros.
+ - Allow for arbitrary embedding of code, that can be run by
+   a LISP interpreter, for example.  (e.g. `(%chez (+ 1 2))` executes
+   `(+ 1 2)` with Chez-Scheme LISP, and places the result in the source
+   (i.e. `3`).
+
 ### Using The Binary
 
 (Providing you have cloned this repo, and `cd`'d into it)
diff --git a/test-css.sex b/samples/css-testing-file.sex
diff --git a/src/assemble/css.rs b/src/assemble/css.rs
@@ -18,19 +18,26 @@ impl CSSFormatter {
 pub const DEFAULT : &str = "\n";
 
 /// All CSS functions, I might have missed a few.
-const CSS_FUNCTIONS : [&str; 53] = [
-    "attr", "blur", "brightness", "calc", "circle", "contrast",
+const CSS_FUNCTIONS : [&str; 56] = [
+    "attr", "blur", "brightness", "calc", "circle", "color", "contrast",
     "counter", "counters", "cubic-bezier", "drop-shadow", "ellipse",
     "grayscale", "hsl", "hsla", "hue-rotate", "hwb", "image", "inset",
-    "invert", "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",
+    "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"
 ];
 
+/// Some CSS functions use commas as an argument delimiter,
+/// some use spaces!  Why not!
+const CSS_COMMA_DELIM : [&str; 2] = [
+    "rgba", "hsla"
+];
+
 /// The only four math operations supported by CSS calc(...),
 /// or at least I think.
 const BINARY_OPERATORS : [&str; 4] = ["+", "-", "*", "/"];
@@ -49,8 +56,12 @@ fn convert_value(node : &ParseNode) -> Result<String, GenerationError> {
                     }
                     let tail = tail_tmp.as_slice();
 
-                    let args = tail.iter()
-                        .fold(String::new(), |acc, s| acc + " " + &s);
+                    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)
@@ -62,7 +73,7 @@ fn convert_value(node : &ParseNode) -> Result<String, GenerationError> {
                         format!("{} {}", head, args)
                     }
                 },
-                _ => String::from("")
+                [] => String::from("")
             };
             Ok(result)
         },
@@ -111,6 +122,7 @@ impl MarkupDisplay for CSSFormatter {
                     let stripped = parser::strip(list, false);
                     let iter = stripped.iter();
                     let mut prop_i = 0; // Index of first property.
+                    // TODO: Selector functions such as nth-child(...), etc.
                     let mut selectors = iter.clone()
                         .take_while(|n| { prop_i += 1; n.atomic().is_some() })
                         .map(|n| n.atomic().unwrap()) // We've checked.
@@ -145,7 +157,7 @@ impl MarkupDisplay for CSSFormatter {
                                  &property.site()));
                         }
                     }
-                    writeln!(f, "}}\n")?;
+                    writeln!(f, "}}")?;
 
                 },
                 ParseNode::Attribute(attr) => {
diff --git a/src/assemble/html.rs b/src/assemble/html.rs
@@ -1,5 +1,7 @@
 //! Assembles an expanded tree into valid HTML.
 use super::{GenerationError, MarkupDisplay};
+use super::css::CSSFormatter;
+
 use crate::parse::parser::{self, ParseNode, ParseTree};
 
 use std::fmt;
@@ -172,12 +174,21 @@ impl MarkupDisplay for HTMLFormatter {
                     }
                     write!(f, ">")?;
 
-                    let html_fmt = HTMLFormatter::new(rest.to_owned());
-                    html_fmt.generate(f)?;
+                    // <style /> tag needs to generate CSS.
+                    if tag == "style" {
+                        writeln!(f, "")?;
+                        let css_fmt = CSSFormatter::new(rest.to_owned());
+                        css_fmt.generate(f)?;
+                    } else {
+                        let html_fmt = HTMLFormatter::new(rest.to_owned());
+                        html_fmt.generate(f)?;
+                    }
                     write!(f, "</{}>", tag)?;
                 },
-                _ => return Err(GenerationError::new("HTML",
-                    "Unknown node encountered.", &node.site()))
+                ParseNode::Attribute(_attr) =>
+                    return Err(GenerationError::new("HTML",
+                        "Unexpected attribute encountered.",
+                        &node.site()))
             }
         }
         Ok(())
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,7 +1,7 @@
 pub mod parse;
 pub mod assemble;
 
-use parse::{parser, lexer};
+use parse::{expander, parser, lexer};
 
 use std::error::Error;
 use std::{fs, path::Path};
@@ -14,7 +14,8 @@ pub fn parse<P: AsRef<Path>>(string : String, source : Option<P>)
     #[cfg(feature="debug")]
     eprintln!("{:#?}", &tokens);
     let tree = parser::parse_stream(tokens)?;
-    Ok(tree)
+    let expanded = expander::expand(tree)?;
+    Ok(expanded)
 }
 
 pub fn parse_file(path : &Path)
diff --git a/src/parse/expander.rs b/src/parse/expander.rs
@@ -0,0 +1,153 @@
+use super::parser::{ParseNode, ParseTree, Node};
+use super::tokens::Site;
+
+use std::{fmt, path::Path, error::Error};
+
+use colored::*;
+
+/// Error type for errors while expanding macros.
+#[derive(Debug, Clone)]
+pub struct ExpansionError(pub String, pub Site);
+
+impl ExpansionError {
+    /// Create a new error given the ML, the message, and the site.
+    pub fn new(msg : &str, site : &Site) -> Self {
+        Self(msg.to_owned(), site.to_owned())
+    }
+}
+
+/// Implement fmt::Display for user-facing error output.
+impl fmt::Display for ExpansionError {
+    fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "[{}] Error Expanding Macro {}: {}",
+            "**".red().bold(), self.1, self.0)
+    }
+}
+
+/// Implements std::error::Error.
+impl Error for ExpansionError { }
+
+use std::collections::HashMap;
+
+#[allow(dead_code)]
+struct Macro {
+    name : String,
+    params : Vec<String>,
+    body : ParseNode
+}
+
+struct ExpansionContext {
+    #[allow(dead_code)] // TODO: User macros!
+    definitions : HashMap<String, Macro>
+}
+
+impl ExpansionContext {
+    pub fn new() -> Self { Self { definitions: HashMap::new() } }
+
+    fn expand_invocation(&mut self, name : &str,
+                         site : &Site,
+                         params : Vec<ParseNode>)
+    -> Result<ParseTree, ExpansionError> {
+        match name {
+            "include" => {
+                let path_node = if let [ p ] = params.as_slice() {
+                    p
+                } else {
+                    return Err(ExpansionError::new(
+                        &format!("Incorrect number of arguments \
+                            to `{}' macro. Got {}, expected {}.",
+                            name, params.len(), 1),
+                        site));
+                };
+
+                let path = if let Some(node) = path_node.atomic() {
+                    node.value
+                } else {
+                    return Err(ExpansionError::new(
+                        &format!("Bad argument to `{}' macro.\n\
+                            Expected a path, but did not get any value
+                            that could be interpreted as a path.", name),
+                        site))
+                };
+
+                // Open file, and parse contents!
+                let tree = match super::parse_file(&Path::new(&path)) {
+                    Ok(tree) => tree,
+                    Err(error) => {
+                        eprintln!("{}", error);
+                        return Err(ExpansionError::new(
+                            &format!("Error parsing file `{}' from \
+                            `include' macro invocation.", path),
+                        site))
+                    }
+                };
+
+                let mut expanded_tree = Vec::with_capacity(tree.len());
+                for branch in tree {
+                    expanded_tree.extend(self.expand_node(branch)?);
+                }
+                Ok(expanded_tree)
+            },
+            _ => Err(ExpansionError::new(
+                    &format!("Macro not found (`{}').", name),
+                    site))
+
+        }
+    }
+
+    pub fn expand_node(&mut self, node : ParseNode)
+    -> Result<ParseTree, ExpansionError> {
+        match node {
+            ParseNode::Symbol(ref _sym) => {
+                // Check if symbol starts with %... and replace it
+                // with it's defined value.
+                Ok(vec![node])
+            },
+            ParseNode::List(list) => {
+                // Check for macro invocation (%... _ _ _).
+                // Recurse over every element.
+                let len = list.len();
+                let mut call = list.into_iter();
+                let head = call.next();
+
+                if let Some(ParseNode::Symbol(ref sym)) = head {
+                    if sym.value.starts_with("%") {
+                        // Rebuild node...
+                        let name = &sym.value[1..];
+                        let Node { site, .. } = sym;
+                        let params = call.collect();
+                        return self.expand_invocation(name, site, params);
+                    }
+                }
+
+                // Rebuild node...
+                let mut expanded_list = Vec::with_capacity(len);
+                expanded_list.extend(self.expand_node(head.unwrap())?);
+                for elem in call {
+                    expanded_list.extend(self.expand_node(elem)?);
+                }
+
+                Ok(vec![ParseNode::List(expanded_list)])
+            },
+            ParseNode::Attribute(mut attr) => {
+                let mut expanded_nodes = self.expand_node(*attr.node)?;
+                attr.node = Box::new(expanded_nodes[0].clone());
+                expanded_nodes[0] = ParseNode::Attribute(attr);
+                Ok(expanded_nodes)
+            },
+            _ => Ok(vec![node])
+        }
+    }
+}
+
+/// Macro-expansion phase.
+/// Macros start with `%...'.
+pub fn expand(tree : ParseTree) -> Result<ParseTree, ExpansionError> {
+    let mut context = ExpansionContext::new();
+
+    let mut expanded = Vec::with_capacity(tree.len());
+    for branch in tree {
+        expanded.extend(context.expand_node(branch)?);
+    }
+    Ok(expanded)
+}
diff --git a/src/parse/lexer.rs b/src/parse/lexer.rs
@@ -1,10 +1,11 @@
-use super::tokens::{self, Token, TokenStream};
+use super::tokens::{self, Site, Token, TokenStream};
 
+use std::rc::Rc;
 use std::path::Path;
 use std::{fmt, error::Error};
 
 #[derive(Debug, Clone)]
-pub struct LexError(tokens::Site, String);
+pub struct LexError(Site, String);
 
 impl fmt::Display for LexError {
     fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -53,6 +54,9 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>)
     let mut bytes : usize = 0;
     let mut line_bytes : usize = 0;
 
+    let source_str = source.map(
+        |e| Rc::from(e.as_ref().display().to_string()));
+
     let mut accumulator : Vec<u8> = Vec::new();
     let mut tokens : TokenStream = Vec::new();
 
@@ -101,10 +105,9 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>)
             }
 
             if !found_end_quote {
-                let mut site = tokens::Site::from_line(
+                let mut site = Site::from_line(
                     lines, line_bytes, 1);
-                site.source = source
-                    .map(|e| e.as_ref().display().to_string());
+                site.source = source_str.clone();
                 return Err(LexError(site,
                     String::from("Unclosed tripple-quoted string.")));
             }
@@ -115,10 +118,12 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>)
             current_kind = None;
 
             let span = accumulator.len() + 3 + 3;
+            let mut site = Site::from_line(start_line,
+                token_start, span);
+            site.source = source_str.clone();
             tokens.push(Token::new(tokens::Kind::String,
                 String::from_utf8(accumulator).unwrap(),
-                tokens::Site::from_line(start_line,
-                    token_start, span)));
+                site));
             accumulator = Vec::new();
             continue;
         }
@@ -215,7 +220,8 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>)
                          || token.kind == tokens::Kind::String
                          || token.kind == tokens::Kind::RParen) => {
                 let kind = tokens::Kind::Whitespace;
-                let site = tokens::Site::from_line(lines, line_bytes, 1);
+                let mut site = Site::from_line(lines, line_bytes, 1);
+                site.source = source_str.clone();
                 let value = character.to_string();
                 tokens.push(Token::new(kind, value, site));
             },
@@ -244,7 +250,8 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>)
                 }
 
                 let value = String::from_utf8(accumulator).unwrap();
-                let site = tokens::Site::from_line(lines, token_start, span);
+                let mut site = Site::from_line(lines, token_start, span);
+                site.source = source_str.clone();
                 tokens.push(Token::new(kind, value, site));
                 accumulator = Vec::new();
 
@@ -273,8 +280,8 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>)
         escaped = false;
     }
     if string_open {
-        let mut site = tokens::Site::from_line(lines, line_bytes, 1);
-        site.source = source.map(|p| p.as_ref().display().to_string());
+        let mut site = Site::from_line(lines, line_bytes, 1);
+        site.source = source_str.clone();
         return Err(LexError(site,
             "Unclosed double-quoted string.".to_string()))
     }
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
@@ -4,4 +4,14 @@ pub mod lexer;
 
 pub mod parser;
 
+use parser::ParseTree;
+use std::{fs, path::Path, error::Error};
+
+pub fn parse_file(path : &Path) -> Result<ParseTree, Box<dyn Error>> {
+    let contents = fs::read_to_string(&path)?;
+    let tokens = lexer::lex(contents, Some(path))?;
+    let tree = parser::parse_stream(tokens)?;
+    Ok(tree)
+}
+
 pub mod expander;
diff --git a/src/parse/parser.rs b/src/parse/parser.rs
@@ -214,13 +214,19 @@ impl fmt::Display for ParseNode {
             },
             ParseNode::Attribute(attr) => write!(f, ":{} {}",
                 &attr.keyword, &*attr.node),
-            ParseNode::List(list) => write!(f, "({}{})", &list[0],
+            ParseNode::List(list) => if list.len() == 0 {
+                write!(f, "()")
+            } else if let [ single ] = list.as_slice() {
+                write!(f, "({})", single)
+            } else {
+                write!(f, "({}{})", &list[0],
                 list[1..].iter().fold(String::new(), |acc, elem| {
                     let nested = elem.to_string().split('\n')
                         .fold(String::new(), |acc, e|
                             acc + "\n  " + &e);
                     acc + &nested
                 }))
+            }
         }
     }
 }
diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs
@@ -1,15 +1,16 @@
+use std::rc::Rc;
 use std::fmt::{self, Display};
 
 #[derive(Debug, Clone)]
 pub struct Site {
-    pub source : Option<String>,
+    pub source : Option<Rc<str>>,
     pub line : usize,
     pub bytes_from_start : usize,
     pub bytes_span : usize,
 }
 
 impl Site {
-    pub fn new(source : String, line : usize,
+    pub fn new(source : Rc<str>, line : usize,
             bytes_from_start : usize,
             bytes_span : usize) -> Self {
         Self {
@@ -43,7 +44,9 @@ impl Display for Site {
     fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(f, "(")?;
         if let Some(source) = &self.source {
-            write!(f, "`{}':", source)?;
+            write!(f, "{}:", source)?;
+        } else {
+            write!(f, "no-file:")?;
         }
         write!(f, "{}:{})", self.line, self.bytes_from_start + 1)
     }
diff --git a/test-css.sex b/test-css.sex
@@ -1,8 +1,16 @@
-(a 2b c2
-   :background-image (url dog.png)
-   :background-position ((calc (- 100% 50px)) (calc (- 100% 20px)))
-   :width 2
-   :heigh 3)
-
-(x #y .z
-   :color (rgb 2 (calc (+ 3 (* 7 3) 1)) 4))
+(html
+  :width 100%
+  :height 100%)
+
+(html , body
+  :margin 0
+  :padding 0)
+
+(body
+  :padding (4em 6em))
+
+(#hello
+   :color (rgb 24 (calc (+ 3 (* 7 3) 1)) 4)
+   :font-family sans-serif)
+
+(img :border-radius 5px)
diff --git a/test.html b/test.html
@@ -1,5 +1,25 @@
 <!DOCTYPE html>
-<html lang="en"><head><title>Example HTML Document</title></head>
+<html lang="en"><head><title>Example HTML Document</title>
+<style>
+html {
+  width: 100%;
+  height: 100%;
+}
+html , body {
+  margin: 0;
+  padding: 0;
+}
+body {
+  padding: 4em 6em;
+}
+#hello {
+  color: rgb(24 calc((3 + (7 * 3) + 1)) 4);
+  font-family: sans-serif;
+}
+img {
+  border-radius: 5px;
+}
+</style></head>
 <body><p id="hello">Hello, World!</p>
 <p>something something text...</p>
 <h1>A (big) Header!</h1>
@@ -7,4 +27,3 @@
 <p>Hello<span style="color: green">World</span>!</p>
 <img alt="Cute cat" src="https://static.insider.com/image/5d24d6b921a861093e71fef3.jpg" width="300"></img></body></html>
 <!-- Generated by SEAM, from symbolic-expressions into HTML. -->
-
diff --git a/test.sex b/test.sex
@@ -1,7 +1,8 @@
 (!DOCTYPE html)
 (html :lang en
   (head
-    (title Example HTML Document))
+    (title Example HTML Document)
+    (style (%include "./test-css.sex")))
   (body
     (p :id hello Hello, World!)
     (p something something text...)