From a6fd12510def5c61f5e13ee9df29cc986e7c3ed4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 27 Nov 2024 17:40:15 +0100 Subject: [PATCH] Improve element building API --- src/element.rs | 163 ++++++++++++++++++++++++++++++++++++------------- src/html.rs | 6 +- src/lib.rs | 65 ++++++++------------ src/mathml.rs | 6 +- src/svg.rs | 6 +- 5 files changed, 154 insertions(+), 92 deletions(-) diff --git a/src/element.rs b/src/element.rs index c5a518f..39c7158 100644 --- a/src/element.rs +++ b/src/element.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; /// #[derive(Clone, Copy, PartialEq, Eq)] @@ -82,73 +82,150 @@ impl Element { Self::new(name, ElementKind::Normal) } - pub fn attr(mut self, name: impl ToString, value: impl ToString) -> Self { - self.attributes - .insert(name.to_string().to_ascii_lowercase(), value.to_string()); - self + pub fn add(&mut self, component: impl ElementComponent) { + component.add_to_element(self); } - pub fn attr_true(self, name: impl ToString) -> Self { - self.attr(name, "") - } - - pub fn data(self, name: impl ToString, value: impl ToString) -> Self { - self.attr(format!("data-{}", name.to_string()), value) - } - - pub fn child(mut self, child: impl Into) -> Self { - self.children.push(child.into()); - self - } - - pub fn children(mut self, children: impl AddChildren) -> Self { - children.add_children(&mut self.children); + pub fn with(mut self, component: impl ElementComponent) -> Self { + self.add(component); self } } -pub trait AddChildren { - fn add_children(self, children: &mut Vec); +pub trait ElementComponent { + fn add_to_element(self, element: &mut Element); } -impl> AddChildren for T { - fn add_children(self, children: &mut Vec) { - children.push(self.into()); +// Attributes + +pub struct Attr { + name: String, + value: String, +} + +impl Attr { + pub fn new(name: impl ToString, value: impl ToString) -> Self { + Self { + name: name.to_string().to_ascii_lowercase(), + value: value.to_string(), + } + } + + pub fn yes(name: impl ToString) -> Self { + Self::new(name, "") + } + + pub fn id(id: impl ToString) -> Self { + Self::new("id", id) + } + + pub fn class(class: impl ToString) -> Self { + Self::new("class", class) + } + + pub fn data(name: impl ToString, value: impl ToString) -> Self { + Self::new(format!("data-{}", name.to_string()), value) } } -impl AddChildren for Vec { - fn add_children(self, children: &mut Vec) { - children.extend(self); +impl ElementComponent for Attr { + fn add_to_element(self, element: &mut Element) { + element.attributes.insert(self.name, self.value); } } -impl AddChildren for [Content; L] { - fn add_children(self, children: &mut Vec) { - children.extend(self); +impl ElementComponent for HashMap { + fn add_to_element(self, element: &mut Element) { + for (name, value) in self { + Attr::new(name, value).add_to_element(element); + } } } -macro_rules! add_children_tuple { +impl ElementComponent for BTreeMap { + fn add_to_element(self, element: &mut Element) { + for (name, value) in self { + Attr::new(name, value).add_to_element(element); + } + } +} + +// Children + +impl> ElementComponent for T { + fn add_to_element(self, element: &mut Element) { + element.children.push(self.into()); + } +} + +// Combining components + +impl ElementComponent for Option { + fn add_to_element(self, element: &mut Element) { + if let Some(component) = self { + component.add_to_element(element) + } + } +} + +impl ElementComponent for Result { + fn add_to_element(self, element: &mut Element) { + match self { + Ok(component) => component.add_to_element(element), + Err(component) => component.add_to_element(element), + } + } +} + +impl ElementComponent for Vec { + fn add_to_element(self, element: &mut Element) { + for component in self { + component.add_to_element(element); + } + } +} + +impl ElementComponent for [T; L] { + fn add_to_element(self, element: &mut Element) { + for component in self { + component.add_to_element(element); + } + } +} + +// Varargs emulation with tuples + +impl ElementComponent for () { + fn add_to_element(self, _element: &mut Element) {} +} + +impl ElementComponent for (C1,) { + fn add_to_element(self, element: &mut Element) { + let (c1,) = self; + c1.add_to_element(element); + } +} + +macro_rules! element_component_tuple { ( $( $t:ident ),* ) => { - impl <$( $t: AddChildren ),*> AddChildren for ($( $t ),*) { - fn add_children(self, children: &mut Vec) { + impl <$( $t: ElementComponent ),*> ElementComponent for ($( $t ),*) { + fn add_to_element(self, element: &mut Element) { #[allow(non_snake_case)] let ($( $t ),*) = self; - $( $t.add_children(children); )* + $( $t.add_to_element(element); )* } } }; } -add_children_tuple!(C1, C2); -add_children_tuple!(C1, C2, C3); -add_children_tuple!(C1, C2, C3, C4); -add_children_tuple!(C1, C2, C3, C4, C5); -add_children_tuple!(C1, C2, C3, C4, C5, C6); -add_children_tuple!(C1, C2, C3, C4, C5, C6, C7); -add_children_tuple!(C1, C2, C3, C4, C5, C6, C7, C8); -add_children_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9); +element_component_tuple!(C1, C2); +element_component_tuple!(C1, C2, C3); +element_component_tuple!(C1, C2, C3, C4); +element_component_tuple!(C1, C2, C3, C4, C5); +element_component_tuple!(C1, C2, C3, C4, C5, C6); +element_component_tuple!(C1, C2, C3, C4, C5, C6, C7); +element_component_tuple!(C1, C2, C3, C4, C5, C6, C7, C8); +element_component_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9); /// An HTML document. /// diff --git a/src/html.rs b/src/html.rs index f518fbe..dcd8400 100644 --- a/src/html.rs +++ b/src/html.rs @@ -2,15 +2,15 @@ //! //! -use crate::{Element, ElementKind}; +use crate::{Element, ElementComponent, ElementKind}; macro_rules! element { ( $name:ident ) => { element!($name, ElementKind::Normal); }; ( $name:ident, $kind:expr ) => { - pub fn $name() -> Element { - Element::new(stringify!($name), $kind) + pub fn $name(component: impl ElementComponent) -> Element { + Element::new(stringify!($name), $kind).with(component) } }; } diff --git a/src/lib.rs b/src/lib.rs index f930167..652d972 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,20 +16,17 @@ pub use self::{element::*, render::*}; #[cfg(test)] mod tests { - use crate::{html::*, Content, Element, Render}; + use crate::{html::*, Attr, Content, Element, Render}; #[test] fn simple_website() { let els = [ Content::doctype(), - html() - .child(head().child(title().child("Hello"))) - .child( - body() - .child(h1().child("Hello")) - .child(p().child("Hello ").child(em().child("world")).child("!")), - ) - .into(), + html(( + head(title("Hello")), + body((h1("Hello"), p(("Hello ", em("world"), "!")))), + )) + .into(), ]; assert_eq!( @@ -46,35 +43,27 @@ mod tests { #[test] fn void_elements() { // Difference between void and non-void - assert_eq!(head().render_to_string().unwrap(), ""); - assert_eq!(input().render_to_string().unwrap(), ""); + assert_eq!(head(()).render_to_string().unwrap(), ""); + assert_eq!(input(()).render_to_string().unwrap(), ""); // Void elements must not contain any children - assert!(input().child(p()).render_to_string().is_err()); + assert!(input(p(())).render_to_string().is_err()); } #[test] fn raw_text_elements() { assert_eq!( - script() - .child("foo ", ); - println!( - "{:?}", - script().child("hello world").render_to_string(), - ); + println!("{:?}", script("hello world").render_to_string(),); - assert!(script() - .child("hello world") - .render_to_string() - .is_err()); + assert!(script("hello world").render_to_string().is_err()); - assert!(script() - .child("hello & bar") - .render_to_string() - .unwrap(), + textarea("foo

& bar").render_to_string().unwrap(), "", ); - assert!(textarea().child(p()).render_to_string().is_err()); + assert!(textarea(p(())).render_to_string().is_err()); } #[test] fn attributes() { assert_eq!( - input() - .attr("name", "tentacles") - .attr("type", "number") - .attr("min", 10) - .attr("max", 100) - .render_to_string() - .unwrap(), + input(( + Attr::new("name", "tentacles"), + Attr::new("type", "number"), + Attr::new("min", 10), + Attr::new("max", 100), + )) + .render_to_string() + .unwrap(), r#""#, ); assert_eq!( - input() - .attr("name", "horns") - .attr_true("checked") + input((Attr::new("name", "horns"), Attr::yes("checked"))) .render_to_string() .unwrap(), r#""#, @@ -119,7 +104,7 @@ mod tests { fn always_lowercase() { assert_eq!( Element::normal("HTML") - .attr("LANG", "EN") + .with(Attr::new("LANG", "EN")) .render_to_string() .unwrap(), r#""#, diff --git a/src/mathml.rs b/src/mathml.rs index f9c091e..794e561 100644 --- a/src/mathml.rs +++ b/src/mathml.rs @@ -2,15 +2,15 @@ //! //! -use crate::{Element, ElementKind}; +use crate::{Element, ElementComponent, ElementKind}; macro_rules! element { ( $name:ident ) => { element!($name, stringify!($name)); }; ( $name:ident, $tag:expr ) => { - pub fn $name() -> Element { - Element::new($tag, ElementKind::Foreign) + pub fn $name(component: impl ElementComponent) -> Element { + Element::new($tag, ElementKind::Foreign).with(component) } }; } diff --git a/src/svg.rs b/src/svg.rs index bf450fe..f34732a 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -2,15 +2,15 @@ //! //! -use crate::{Element, ElementKind}; +use crate::{Element, ElementComponent, ElementKind}; macro_rules! element { ( $name:ident ) => { element!($name, stringify!($name)); }; ( $name:ident, $tag:expr ) => { - pub fn $name() -> Element { - Element::new($tag, ElementKind::Foreign) + pub fn $name(component: impl ElementComponent) -> Element { + Element::new($tag, ElementKind::Foreign).with(component) } }; }