add: plugins

This commit is contained in:
trisua 2025-05-31 12:42:23 -04:00
parent 149025f9e4
commit d71dc7e7ca
7 changed files with 161 additions and 47 deletions

2
Cargo.lock generated
View file

@ -4,4 +4,4 @@ version = 4
[[package]] [[package]]
name = "bberry" name = "bberry"
version = "0.1.2" version = "0.2.0"

View file

@ -1,7 +1,7 @@
[package] [package]
name = "bberry" name = "bberry"
description = "lisp-like dsl which \"compiles\" into html" description = "lisp-like dsl which \"compiles\" into html"
version = "0.1.2" version = "0.2.0"
edition = "2024" edition = "2024"
authors = ["trisuaso"] authors = ["trisuaso"]
repository = "https://trisua.com/t/bberry.git" repository = "https://trisua.com/t/bberry.git"

4
examples/plugins.lisp Normal file
View file

@ -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

View file

@ -1,8 +1,14 @@
use std::collections::HashMap; use std::collections::HashMap;
pub trait Render { pub trait Render<'a> {
/// Render into HTML. /// Render into HTML.
fn render(&self) -> String; fn render(
self,
plugins: &mut HashMap<String, Box<dyn FnMut(Element) -> 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"]; pub const SELF_CLOSING_TAGS: &[&str] = &["img", "br", "hr", "input", "meta", "link"];
@ -16,8 +22,11 @@ pub struct Element {
pub children: Vec<Element>, pub children: Vec<Element>,
} }
impl Render for Element { impl<'a> Render<'a> for Element {
fn render(&self) -> String { fn render(
self,
plugins: &mut HashMap<String, Box<dyn FnMut(Element) -> Element + 'a>>,
) -> String {
if self.tag == "text" { if self.tag == "text" {
return self.attrs.get("content").unwrap().to_owned(); return self.attrs.get("content").unwrap().to_owned();
} else if self.tag == "v" { } else if self.tag == "v" {
@ -27,10 +36,12 @@ impl Render for Element {
let mut inner: String = String::new(); let mut inner: String = String::new();
for element in &self.children { for element in &self.children {
inner.push_str(&element.render()); inner.push_str(&element.clone().render(plugins));
} }
return inner; return inner;
} else if let Some(f) = plugins.get_mut(&self.tag) {
return f(self).render(plugins);
} }
let closing = format!("</{}>", self.tag); let closing = format!("</{}>", self.tag);
@ -59,7 +70,7 @@ impl Render for Element {
let mut inner: String = String::new(); let mut inner: String = String::new();
for element in &self.children { for element in &self.children {
inner.push_str(&element.render()); inner.push_str(&element.clone().render(plugins));
} }
inner 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
})
}
} }

View file

@ -101,34 +101,7 @@ pub fn expr_parser(buf: &str) -> Element {
} }
// special elements // special elements
if element.tag == "#" { if (element.tag == "attr") | (element.tag == ":") | (element.tag.is_empty()) {
// parse as tuple
// tuples can only contain strings
let len = buf.matches("s\"").collect::<Vec<&str>>().len();
let mut values: Vec<String> = 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()) {
let mut chars = (&buf[i..buf.len()]).to_string(); let mut chars = (&buf[i..buf.len()]).to_string();
if element.tag.is_empty() { if element.tag.is_empty() {
@ -264,6 +237,6 @@ pub fn element_parser(value: &str) -> (Element, usize) {
} }
/// Parse a full document. /// Parse a full document.
pub fn document(value: &str) -> (Element, usize) { pub fn document(value: &str) -> Element {
element_parser(&format!("(null? [{value}])")) element_parser(&format!("(null? [{value}])")).0
} }

View file

@ -1,17 +1,24 @@
pub mod core; pub mod core;
pub mod macros;
pub use core::parser::document as parse; pub use core::parser::document as parse;
#[cfg(test)] #[cfg(test)]
mod 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] #[test]
fn string_escape() { fn string_escape() {
std::fs::write( std::fs::write(
"string_escape.html", "string_escape.html",
parse(&std::fs::read_to_string("examples/string_escape.lisp").unwrap()) parse(&std::fs::read_to_string("examples/string_escape.lisp").unwrap()).render_safe(),
.0
.render(),
) )
.unwrap(); .unwrap();
@ -22,13 +29,70 @@ mod test {
} }
#[test] #[test]
fn boilerplate() { fn plugins() {
std::fs::write( std::fs::write(
"boilerplate.html", "plugins.html",
parse(&std::fs::read_to_string("examples/boilerplate.lisp").unwrap()) parse(&std::fs::read_to_string("examples/plugins.lisp").unwrap()).render(&mut {
.0 let mut h = HashMap::new();
.render(),
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(); .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))
}
} }

51
src/macros.rs Normal file
View file

@ -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()
};
}