seam

Symbolic-Expressions As Markup.
git clone git://git.knutsen.co/seam
Log | Files | Refs | README | LICENSE

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:
MREADME.md | 9+++++++++
Ctest-css.sex -> samples/css-testing-file.sex | 0
Msrc/assemble/css.rs | 32++++++++++++++++++++++----------
Msrc/assemble/html.rs | 19+++++++++++++++----
Msrc/lib.rs | 5+++--
Msrc/parse/expander.rs | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/parse/lexer.rs | 29++++++++++++++++++-----------
Msrc/parse/mod.rs | 10++++++++++
Msrc/parse/parser.rs | 8+++++++-
Msrc/parse/tokens.rs | 9++++++---
Mtest-css.sex | 24++++++++++++++++--------
Mtest.html | 23+++++++++++++++++++++--
Mtest.sex | 3++-
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...)