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...)