seam

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

commit 623896e7320d121df90ead595159b47e2b2a0a06
parent 67dd231f416eb68e21f012cd0b3e331bd8a4ed63
Author: knutsen <samuel@knutsen.co>
Date:   Sun, 21 Mar 2021 22:25:44 +0000

Added CSS special @-selector rules.

Diffstat:
MCargo.lock | 22+++++++++++-----------
MCargo.toml | 3++-
MREADME.md | 25+++++++++++++++++++------
AUSAGE.md | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/assemble/css.rs | 194++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/assemble/html.rs | 110+++++++++++++++++++++++++------------------------------------------------------
Msrc/assemble/mod.rs | 11+++++++----
Msrc/assemble/xml.rs | 8+++-----
Msrc/lib.rs | 6+++++-
Msrc/parse/expander.rs | 3++-
Msrc/parse/parser.rs | 81++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
11 files changed, 410 insertions(+), 155 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -45,9 +45,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.14" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" dependencies = [ "libc", ] @@ -60,15 +60,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.71" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" +checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae" [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg", "num-traits", @@ -76,16 +76,16 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", ] [[package]] name = "seam" -version = "0.1.6" +version = "0.1.7" dependencies = [ "chrono", "colored", @@ -110,9 +110,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "winapi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", diff --git a/Cargo.toml b/Cargo.toml @@ -4,7 +4,7 @@ description = "Symbolic Expressions As Markup." keywords = ["markup", "lisp", "macro", "symbolic-expression", "sexp"] license-file = "LICENSE" homepage = "https://git.knutsen.co/seam" -version = "0.1.6" +version = "0.1.7" authors = ["Demonstrandum <moi@knutsen.co>"] edition = "2018" @@ -22,3 +22,4 @@ path = "src/bin.rs" [dependencies] colored = "1.8" chrono = "0.4" + diff --git a/README.md b/README.md @@ -11,9 +11,12 @@ macros, code includes and such. ## Try it out -Mainly this should be used as a library, such as from within a server, +This may be used as a library, such as from within a server, generating HTML (or any other supported markup) before it is served to the -client. +client. Personally, I am currently just using the `seam` binary to statically +generate some personal and project websites. + +Read the [USAGE.md](USAGE.md) file for code examples and documentation. ### Current Formats @@ -65,7 +68,7 @@ seam --html <<< "(p Hello World)" # <head></head> # <body> # <p>Hello World</p> -# <!-- Generated by SEAM, from symbolic-expressions into HTML. --> +# <!-- Generated by SEAM. --> # </body> # </html> ``` @@ -74,18 +77,28 @@ seam --xml <<< '(para Today is a day in (%date "%B, year %Y").)' #stdout: # <?xml version="1.0" encoding="UTF-8" ?> # <para>Today is a day in November, year 2020.</para> -# <!-- Generated by SEAM, from symbolic-expressions into XML. --> +# <!-- Generated by SEAM. --> ``` ## TODO + - Rewrite lexer to only insert whitespace before and after {`(`, `)`} and + next to string-literals. Whitespace should then be added between between + symbols *after* macro expansion, since a macro could expand to any literal. + Variadic macros should preserve whitespace in its arguments entirely (no stripping). + - `%list` macro which expands from `(p (%list a b c))` to `(p a b c)`. + This is essentially an anonymous macro definition, i.e `(%define L a b c)`, + then `%L` is the same as `(%list a b c)`. + - `%for`-loop macro, iterating over `%list`s. + - `%glob` which returns a list of files/directories matching a glob. + - `%markdown` renders markdown given to it. + - `%html`, `%xml`, `%css`, etc. macros which goes into the specific rendering mode. + - Add variadic and keyword macro arguments. - Caching or checking time-stamps as to not regenerate unmodified source files. - HTML object `style="..."` object should handle s-expressions well, (e.g. `(p :style (:color red :border none) Hello World)`) - HTML `<style>` tag should allow for *normal* CSS syntax if just given a string. - Allow for, and handle special `@` syntax in CSS, such as `@import` and `@media`. - Add more supported formats (`JSON`, `JS`, `TOML`, &c.). - Add more helpful/generic macros (e.g. `(%include ...)`, which already exists). - - Add user defined macros system (e.g. `(%define (red-para txt) (p :style "color: red" %txt))`) - - Then add variadic macros. - Allow for arbitrary embedding of code, that can be run by a LISP interpreter (or any other langauge), for example. (e.g. `(%chez (+ 1 2))` executes `(+ 1 2)` with Chez-Scheme LISP, and places the result in the source diff --git a/USAGE.md b/USAGE.md @@ -0,0 +1,102 @@ +# Usage + +## Syntax + +### Overview + +The syntax consists of *symbolic expressions*, which may be any of: +- Symbols (e.g. `p`, `h1` or `hello`). +- Numbers (e.g. `26`, `2em` or `-4px`). +- Strings (e.g. `"hello world"` or `"""Probably a long string"""`). +- Attributes (e.g `:key value`, `:count 6` or `:href "https://crates.io/"`). +- Lists (e.g. `(h1 Big Title!)` or `(p Hello (span :class highlight World))`). + +The most important part of this syntax is *lists*, every level of nesting or +every tag is started with a `(` and ended with a `)` + +### Example + +For example, +```lisp +(h1 :id heading Pound Cake) +(ul (li 1 pound of sugar) + (li 1 pound of butter) + (li 1 pound of (i self-raising) flor) + (li 3 eggs, beaten) + (li 1 teaspoon (a :href "https://wikipedia.org/wiki/Vanilla_extract" vanilla)) + (li 1 teaspoon baking powder)) +``` +would translate to HTML as +```html +<h1 id="heading">Pound Cake</h1> +<ul> + <li>1 pound of sugar</li> + <li>1 pound of butter</li> + <li>1 pound of <i>self-raising</i> flour</li> + <li>3 eggs, beaten</li> + <li>1 teaspoon <a href="https://wikipedia.org/wiki/Vanilla_extractr">vanilla</a></li> + <li>1 teaspoin baking powder</li> +</ul> +``` + +## Macros + +In addition to the basic syntax, *symbols* prepended with a ‘`%`’ character, +will be treated as macros. + +### Built-in Macros +- `%define` allows for user defined macros, e.g. `(%define name "Samuel")`, + ‘function’-like macros may also be defined to allow arguments, say + `(%define (greet name) (p Hello there, %name))`. +- `%ifdef` checks if a symbol is defined, e.g. `(ifdef username (span Hello %username))`, + or with an else-clause: `(ifdef title (title My Blog - %title) (title My Blog))`. +- `%include` will include an entire file's contents into another, e.g. + `(%include "components/footer.sex")`. +- `%date` standard `strftime` function, works of current system time, e.g. + `(span :id timestamp Uploaded on (%date "%Y-%m-%d"))`. +- `%log` will simply log any information to STDERR, e.g. `(%log Hello, World!)`. + +### Example + +*File `head.sex`:* +```lisp +(meta :name viewport :content "width=device-width, initial-scale=1.0") +(%ifdef title + (title %title" -- My Website") + (title "My Website")) +(style (%include "styles/main.sex")) +``` + +*File `styles/main.sex`:* +```lisp +(body :font-size 16px + :margin (0 auto) + :background #fff) +(.important + :color red) +``` + +*File `index.sex`:* +```lisp +(!doctype html) +(html + (head (%define title "Index page") + (%include "head.sex")) + (body + (div :id container + (%include "components/navbar.sex") + (h1 Hello, World!) + (p :class important Enjoy your stay!)))) +``` + +*File `components/navbar.sex`:* +```lisp +(nav + (ul (li (a :href "/" Home)) + (li (a :href "/about" About Me)) + (li (a :href "/donate" Donate!)))) +``` + +This way you can easily construct websites with reusable components, +cut down on copy-pasting, and generate it statically. Ideal for a blog, +especially using the `(%markdown ...)` macro. diff --git a/src/assemble/css.rs b/src/assemble/css.rs @@ -1,8 +1,8 @@ //! Assembles an expanded tree into valid CSS. -use super::{GenerationError, MarkupDisplay}; +use super::{GenerationError, MarkupDisplay, Formatter}; use crate::parse::parser::{self, ParseNode, ParseTree}; -use std::fmt; +use std::slice::Iter; #[derive(Debug, Clone)] pub struct CSSFormatter { @@ -18,7 +18,7 @@ impl CSSFormatter { pub const DEFAULT : &str = "\n"; /// All CSS functions, I might have missed a few. -const CSS_FUNCTIONS : [&str; 56] = [ +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", @@ -29,14 +29,29 @@ const CSS_FUNCTIONS : [&str; 56] = [ "rotateY", "rotateZ", "saturate", "sepia", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "skew", "skewX", "skewY", "symbols", "translate", "translate3d", "translateX", "translateY", "translateZ", - "url", "var" + "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" -]; +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. @@ -102,63 +117,142 @@ pub fn css_value(_property : &str, node : &ParseNode) 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)); } - self.generate(&mut doc)?; - doc += "/* Generated from symbolic-expressions, with SEAM */\n"; + doc += &self.display()?; + doc += "\n/* Generated from symbolic-expressions, with SEAM */\n"; Ok(doc) } - fn generate(&self, f : &mut dyn fmt::Write) + 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) => { - 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. - .peekable(); - - // Check we were actually provided with - // some selectors. - if selectors.peek().is_none() { - return Err(GenerationError::new("CSS", - "CSS selector(s) missing. \ - Expected a symbol node, none was found!", - &node.site())); - } - // Join the selectors togeher. - 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, "}}")?; - + generate_css_rule(f, list)?; }, ParseNode::Attribute(attr) => { let site = attr.site.to_owned(); diff --git a/src/assemble/html.rs b/src/assemble/html.rs @@ -1,10 +1,8 @@ //! Assembles an expanded tree into valid HTML. -use super::{GenerationError, MarkupDisplay}; +use super::{GenerationError, MarkupDisplay, Formatter}; use super::css::CSSFormatter; -use crate::parse::parser::{self, ParseNode, ParseTree}; - -use std::fmt; +use crate::parse::parser::{ParseNode, ParseTree, SearchTree}; #[derive(Debug, Clone)] pub struct HTMLFormatter { @@ -32,87 +30,49 @@ impl MarkupDisplay for HTMLFormatter { if self.tree.is_empty() { return Ok(String::from(DEFAULT)); } - let stripped = parser::strip(&self.tree, true); - let mut current_node = stripped.get(0); - - // Check if <!DOCTYPE html> exists. - let mut has_declaration = false; - if let Some(ParseNode::List(list)) = current_node.as_ref() { - if let Some(ParseNode::Symbol(declaration)) = list.get(0) { - if declaration.value.to_lowercase() == "!doctype" { - has_declaration = true; - } - } - } - - if has_declaration { - current_node = stripped.get(1); - } else { - doc += "<!DOCTYPE html>\n" - } - // Check if <html></html> root object exists. - let mut html_tag = false; - if let Some(ParseNode::List(list)) = current_node.as_ref() { - if let Some(ParseNode::Symbol(root_tag)) = &list.get(0) { - if root_tag.value.to_lowercase() == "html" { - html_tag = true; + // Check if top-level <!DOCTYPE html> exists. + let doctype_tag + = self.tree.search_node(ParseNode::List, "!DOCTYPE", true, 1); + // Check if top-level <html></html> root object exists. + let html_tag + = self.tree.search_node(ParseNode::List, "html", true, 1); + // Check if <head></head> tag object exists. + let head_tag + = self.tree.search_node(ParseNode::List, "head", true, 2); + // Check if <body></body> tag object exists. + let body_tag + = self.tree.search_node(ParseNode::List, "body", true, 2); + + if doctype_tag.is_none() { + doc += "<!DOCTYPE html>\n"; + if html_tag.is_none() { + doc += "<html>\n"; + if head_tag.is_none() { + doc += "<head></head>\n" } - } - } - - if !html_tag { - doc += "<html>\n"; - } - - // Check if <head></head> exists. - let mut head_tag = false; - if let Some(ParseNode::List(list)) = current_node.as_ref() { - if let Some(ParseNode::List(head_list)) = &list.get(1) { - if let Some(ParseNode::Symbol(head)) = &head_list.get(0) { - if head.value.to_lowercase() == "head" { - head_tag = true; - } + if body_tag.is_none() { + doc += "<body>\n" } } } + // Populate. + doc += &self.display()?; + doc += "\n<!-- Generated by SEAM. -->\n"; - if !head_tag { - doc += "<head></head>\n"; - } - - // Check if body exists, if not, make it, and populate it. - let mut body_tag = false; - if let Some(ParseNode::List(list)) = current_node.as_ref() { - if let Some(ParseNode::List(body_list)) = &list.get(2) { - if let Some(ParseNode::Symbol(body)) = &body_list.get(0) { - if body.value.to_lowercase() == "body" { - body_tag = true; - } + if doctype_tag.is_none() { + if html_tag.is_none() { + if body_tag.is_none() { + doc += "</body>\n" } + doc += "</html>\n" } } - if !body_tag { - doc += "<body>\n"; - } - - // Populate. - self.generate(&mut doc)?; - doc += "<!-- Generated by SEAM, from symbolic-expressions \ - into HTML. -->\n"; - // Cloes all new tags. - if !body_tag { - doc += "</body>\n"; - } - if !html_tag { - doc += "</html>\n"; - } - Ok(doc) } - fn generate(&self, f : &mut dyn fmt::Write) + fn generate(&self, f : Formatter) -> Result<(), GenerationError> { let mut tree_iter = self.tree.iter().peekable(); while let Some(node) = tree_iter.next() { @@ -137,10 +97,10 @@ impl MarkupDisplay for HTMLFormatter { tag = &head_symbol.value; write!(f, "<{}", tag)?; } else { - // Error, tags can only have symbol values. + // TODO: Error, tags can only have symbol values. } } else { - // Error, empty tags not supported. + // TODO: Error, empty tags not supported. } let mut rest = &list[1..]; @@ -175,7 +135,7 @@ impl MarkupDisplay for HTMLFormatter { write!(f, ">")?; // <style /> tag needs to generate CSS. - if tag == "style" { + if tag == "style" { // TODO: If just a string, don't convert. writeln!(f, "")?; let css_fmt = CSSFormatter::new(rest.to_owned()); css_fmt.generate(f)?; diff --git a/src/assemble/mod.rs b/src/assemble/mod.rs @@ -59,6 +59,8 @@ impl convert::From<fmt::Error> for GenerationError { } } +pub type Formatter<'a> = &'a mut dyn fmt::Write; + /// Trait for all structs that can generate specific markup /// for the s-expression tree. pub trait MarkupDisplay { @@ -66,7 +68,7 @@ pub trait MarkupDisplay { /// Similar to fmt in Display/Debug traits, takes in a /// mutable writable buffer, returns success or a specifc /// error while generating the markup. - fn generate(&self, buf : &mut dyn fmt::Write) + fn generate(&self, buf : Formatter) -> Result<(), GenerationError>; /// Documentises the input, that's to say, it adds any /// extra meta-information to the generated markup, if @@ -77,15 +79,16 @@ pub trait MarkupDisplay { /// Directly converts the s-expressions into a string /// containing the markup, unless there was an error. fn display(&self) -> Result<String, GenerationError> { - let mut s_buf = String::new(); - self.generate(&mut s_buf).map(|_| s_buf) + let mut buf = String::new(); + self.generate(&mut buf)?; + Ok(buf) } } /// Automatically implement fmt::Display as a wrapper around /// MarkupDisplay::generate, but throws away the useful error message. impl fmt::Display for dyn MarkupDisplay { - fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f : &mut fmt::Formatter) -> fmt::Result { self.generate(f).map_err(|_| fmt::Error) } } diff --git a/src/assemble/xml.rs b/src/assemble/xml.rs @@ -1,9 +1,7 @@ //! Assembles an expanded tree into valid XML. -use super::{MarkupDisplay, GenerationError}; +use super::{MarkupDisplay, GenerationError, Formatter}; use crate::parse::parser::{self, ParseNode, ParseTree}; -use std::fmt; - #[derive(Debug, Clone)] pub struct XMLFormatter { pub tree : ParseTree @@ -54,13 +52,13 @@ impl MarkupDisplay for XMLFormatter { } // Populate. - self.generate(&mut doc)?; + doc += &self.display()?; doc += "<!-- Generated by SEAM, from symbolic-expressions \ into XML. -->\n"; Ok(doc) } - fn generate(&self, f : &mut dyn fmt::Write) + fn generate(&self, f : Formatter) -> Result<(), GenerationError> { let mut tree_iter = self.tree.iter().peekable(); while let Some(node) = tree_iter.next() { diff --git a/src/lib.rs b/src/lib.rs @@ -1,3 +1,7 @@ +#![allow(incomplete_features)] +#![feature(associated_type_defaults)] +#![feature(generic_associated_types)] + pub mod parse; pub mod assemble; @@ -6,7 +10,7 @@ use parse::{expander, parser, lexer}; use std::error::Error; use std::{fs, io, path::Path}; -pub const VERSION : (u8, u8, u8) = (0, 1, 6); +pub const VERSION : (u8, u8, u8) = (0, 1, 7); pub fn parse<P: AsRef<Path>>(string : String, source : Option<P>) -> Result<parser::ParseTree, Box<dyn Error>> { diff --git a/src/parse/expander.rs b/src/parse/expander.rs @@ -310,7 +310,8 @@ impl ExpansionContext { let name = &sym.value[1..]; let Node { site, .. } = sym; // Clean macro arguments from whitespace tokens. - let params = parser::strip(&call.collect(), false); + let call_vec: ParseTree = call.collect(); + let params = parser::strip(&call_vec, false); return self.expand_invocation(name, site, params); } } diff --git a/src/parse/parser.rs b/src/parse/parser.rs @@ -67,6 +67,85 @@ impl ParseNode { pub type ParseTree = Vec<ParseNode>; +pub trait SearchTree { + type Constructor<T> = fn(T) -> ParseNode; + /// Search the parse-tree for a specific node with a specific value. + /// if `kind == ParseNode::List`, then check if the list has + /// first value (head/caller) equal to `value`, etc. + fn search_node<T>(&self, kind : Self::Constructor<T>, + value : &str, + case_insensitive : bool, + level : usize) -> Option<ParseNode>; +} + +impl SearchTree for ParseTree { + fn search_node<T>(&self, kind : Self::Constructor<T>, value : &str, + insensitive : bool, level: usize) -> Option<ParseNode> { + if level == 0 { + return None; + } + + let fnptr = kind as usize; + + let is_equal = |string: &str| if insensitive { + string.to_lowercase() == value.to_lowercase() + } else { + string == value + }; + + for node in self { + let found = match node { + ParseNode::List(nodes) => { + if fnptr == ParseNode::List as usize { + if let Some(Some(caller)) = nodes.get(0).map(ParseNode::atomic) { + if is_equal(&caller.value) { + return Some(node.clone()); + } + } + } + nodes.search_node(kind, value, insensitive, level - 1) + }, + ParseNode::Symbol(name) => { + if fnptr == ParseNode::Symbol as usize && is_equal(&name.value) { + Some(node.clone()) + } else { + None + } + }, + ParseNode::String(name) => { + if fnptr == ParseNode::String as usize && is_equal(&name.value) { + Some(node.clone()) + } else { + None + } + }, + ParseNode::Number(name) => { + if fnptr == ParseNode::Number as usize && is_equal(&name.value) { + Some(node.clone()) + } else { + None + } + }, + ParseNode::Attribute(attr) => { + if kind as usize == ParseNode::Attribute as usize { + if is_equal(&attr.keyword) { + return Some(node.clone()); + } + } + let singleton : ParseTree = vec![*attr.node.clone()]; + singleton.search_node(kind, value, insensitive, level - 1) + } + }; + + if found.is_some() { + return found; + } + } + + None + } +} + #[derive(Debug, Clone)] pub struct ParseError(pub String, pub Site); @@ -181,7 +260,7 @@ pub fn parse_stream(tokens: tokens::TokenStream) } /// Strip any pure whitespace (and annotation) nodes from the tree. -pub fn strip(tree : &ParseTree, strip_attributes : bool) -> ParseTree { +pub fn strip(tree : &[ParseNode], strip_attributes : bool) -> ParseTree { let mut stripped = tree.to_owned(); stripped.retain(|branch| { match branch {