commit 623896e7320d121df90ead595159b47e2b2a0a06
parent 67dd231f416eb68e21f012cd0b3e331bd8a4ed63
Author: knutsen <samuel@knutsen.co>
Date: Sun, 21 Mar 2021 22:25:44 +0000
Added CSS special @-selector rules.
Diffstat:
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 {