diff --git a/Cargo.lock b/Cargo.lock index 96211c0..628dd95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "bberry" -version = "0.1.2" +version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 10b61e7..13f2005 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bberry" description = "lisp-like dsl which \"compiles\" into html" -version = "0.1.2" +version = "0.2.0" edition = "2024" authors = ["trisuaso"] repository = "https://trisua.com/t/bberry.git" diff --git a/examples/plugins.lisp b/examples/plugins.lisp new file mode 100644 index 0000000..b493f0f --- /dev/null +++ b/examples/plugins.lisp @@ -0,0 +1,4 @@ +(h1 (say_hi)) ; plugins with no parameters +(say_hi_with_params (text "jeff")) ; plugins with parameters +(pre (say_hi_multiple (text "abcd") (text "xyz"))) ; mutliple parameters in plugins +(pre (code (read_file (text ".gitignore")))) ; slightly more advanced plugins diff --git a/src/core/element.rs b/src/core/element.rs index a9f45eb..22eaaf2 100644 --- a/src/core/element.rs +++ b/src/core/element.rs @@ -1,8 +1,14 @@ use std::collections::HashMap; -pub trait Render { +pub trait Render<'a> { /// Render into HTML. - fn render(&self) -> String; + fn render( + self, + plugins: &mut HashMap Element + 'a>>, + ) -> String; + + /// Render into HTML with no plugins. + fn render_safe(self) -> String; } pub const SELF_CLOSING_TAGS: &[&str] = &["img", "br", "hr", "input", "meta", "link"]; @@ -16,8 +22,11 @@ pub struct Element { pub children: Vec, } -impl Render for Element { - fn render(&self) -> String { +impl<'a> Render<'a> for Element { + fn render( + self, + plugins: &mut HashMap Element + 'a>>, + ) -> String { if self.tag == "text" { return self.attrs.get("content").unwrap().to_owned(); } else if self.tag == "v" { @@ -27,10 +36,12 @@ impl Render for Element { let mut inner: String = String::new(); for element in &self.children { - inner.push_str(&element.render()); + inner.push_str(&element.clone().render(plugins)); } return inner; + } else if let Some(f) = plugins.get_mut(&self.tag) { + return f(self).render(plugins); } let closing = format!("", self.tag); @@ -59,7 +70,7 @@ impl Render for Element { let mut inner: String = String::new(); for element in &self.children { - inner.push_str(&element.render()); + inner.push_str(&element.clone().render(plugins)); } inner @@ -71,4 +82,15 @@ impl Render for Element { } ) } + + fn render_safe(self) -> String { + self.render(&mut { + let mut h = HashMap::new(); + h.insert( + "#null".to_string(), + Box::new(|x: Element| x.to_owned()) as _, + ); + h + }) + } } diff --git a/src/core/parser.rs b/src/core/parser.rs index 881c724..97662a7 100644 --- a/src/core/parser.rs +++ b/src/core/parser.rs @@ -101,34 +101,7 @@ pub fn expr_parser(buf: &str) -> Element { } // special elements - if element.tag == "#" { - // parse as tuple - // tuples can only contain strings - let len = buf.matches("s\"").collect::>().len(); - let mut values: Vec = Vec::new(); - let mut last_len: usize = 0; - - for _ in 0..len { - let mut buffer: String = String::new(); - - string_parser( - buf.replace("# ", "") - .replace("s\"", "\"") - .chars() - .skip(last_len), - &mut buffer, - ); - last_len = buffer.len() + 2; - - values.push(buffer); - } - - for (i, v) in values.iter().enumerate() { - element.attrs.insert(i.to_string(), v.to_owned()); - } - - return element; - } else if (element.tag == "attr") | (element.tag == ":") | (element.tag.is_empty()) { + if (element.tag == "attr") | (element.tag == ":") | (element.tag.is_empty()) { let mut chars = (&buf[i..buf.len()]).to_string(); if element.tag.is_empty() { @@ -264,6 +237,6 @@ pub fn element_parser(value: &str) -> (Element, usize) { } /// Parse a full document. -pub fn document(value: &str) -> (Element, usize) { - element_parser(&format!("(null? [{value}])")) +pub fn document(value: &str) -> Element { + element_parser(&format!("(null? [{value}])")).0 } diff --git a/src/lib.rs b/src/lib.rs index 097f7a0..d1b74ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,24 @@ pub mod core; +pub mod macros; pub use core::parser::document as parse; #[cfg(test)] mod test { - use crate::{core::element::Render, parse}; + use std::{ + collections::HashMap, + time::{Duration, SystemTime}, + }; + + use crate::{ + core::element::{Element, Render}, + parse, read_param, text, + }; #[test] fn string_escape() { std::fs::write( "string_escape.html", - parse(&std::fs::read_to_string("examples/string_escape.lisp").unwrap()) - .0 - .render(), + parse(&std::fs::read_to_string("examples/string_escape.lisp").unwrap()).render_safe(), ) .unwrap(); @@ -22,13 +29,70 @@ mod test { } #[test] - fn boilerplate() { + fn plugins() { std::fs::write( - "boilerplate.html", - parse(&std::fs::read_to_string("examples/boilerplate.lisp").unwrap()) - .0 - .render(), + "plugins.html", + parse(&std::fs::read_to_string("examples/plugins.lisp").unwrap()).render(&mut { + let mut h = HashMap::new(); + + h.insert( + "say_hi".to_string(), + Box::new(|_| text!("Hello, world!")) as _, + ); + + h.insert( + "say_hi_with_params".to_string(), + Box::new(|e: Element| text!(read_param!(e, 0))) as _, + ); + + h.insert( + "say_hi_multiple".to_string(), + Box::new(|e: Element| { + let mut content: String = String::new(); + + for child in e.children { + let text = child.attrs.get("content").unwrap(); + content += &format!("{text}, "); + } + + text!(content) + }) as _, + ); + + h.insert( + "read_file".to_string(), + Box::new(|e: Element| { + text!(std::fs::read_to_string(read_param!(e, 0)).unwrap()) + }) as _, + ); + + h + }), ) .unwrap(); } + + #[test] + fn boilerplate() { + std::fs::write( + "boilerplate.html", + parse(&std::fs::read_to_string("examples/boilerplate.lisp").unwrap()).render_safe(), + ) + .unwrap(); + } + + #[test] + fn boilerplate_speed() { + let start = SystemTime::now(); + + std::fs::write( + "boilerplate.html", + parse(&std::fs::read_to_string("examples/boilerplate.lisp").unwrap()).render_safe(), + ) + .unwrap(); + + let duration = start.elapsed().unwrap(); + println!("took: {}μs", duration.as_micros()); + assert!(duration < Duration::from_micros(500)) + } } diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..4bace11 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,51 @@ +#[macro_export] +macro_rules! text { + ($t:literal) => { + $crate::core::element::Element { + tag: "text".to_string(), + children: Vec::new(), + attrs: { + let mut a = HashMap::new(); + a.insert("content".to_string(), $t.to_string()); + a + }, + } + }; + + ($t:ident) => { + $crate::core::element::Element { + tag: "text".to_string(), + children: Vec::new(), + attrs: { + let mut a = HashMap::new(); + a.insert("content".to_string(), $t.to_string()); + a + }, + } + }; + + ($t:expr) => { + $crate::core::element::Element { + tag: "text".to_string(), + children: Vec::new(), + attrs: { + let mut a = HashMap::new(); + a.insert("content".to_string(), ($t).to_string()); + a + }, + } + }; +} + +#[macro_export] +macro_rules! read_param { + ($element:ident, $id:literal) => { + $element + .children + .get($id) + .unwrap() + .attrs + .get("content") + .unwrap() + }; +}