Improve element building API

This commit is contained in:
Joscha 2024-11-27 17:40:15 +01:00
parent c4b3a279cc
commit a6fd12510d
5 changed files with 154 additions and 92 deletions

View file

@ -1,4 +1,4 @@
use std::collections::BTreeMap; use std::collections::{BTreeMap, HashMap};
/// <https://html.spec.whatwg.org/multipage/syntax.html#elements-2> /// <https://html.spec.whatwg.org/multipage/syntax.html#elements-2>
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
@ -82,73 +82,150 @@ impl Element {
Self::new(name, ElementKind::Normal) Self::new(name, ElementKind::Normal)
} }
pub fn attr(mut self, name: impl ToString, value: impl ToString) -> Self { pub fn add(&mut self, component: impl ElementComponent) {
self.attributes component.add_to_element(self);
.insert(name.to_string().to_ascii_lowercase(), value.to_string());
self
} }
pub fn attr_true(self, name: impl ToString) -> Self { pub fn with(mut self, component: impl ElementComponent) -> Self {
self.attr(name, "") self.add(component);
}
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<Content>) -> Self {
self.children.push(child.into());
self
}
pub fn children(mut self, children: impl AddChildren) -> Self {
children.add_children(&mut self.children);
self self
} }
} }
pub trait AddChildren { pub trait ElementComponent {
fn add_children(self, children: &mut Vec<Content>); fn add_to_element(self, element: &mut Element);
} }
impl<T: Into<Content>> AddChildren for T { // Attributes
fn add_children(self, children: &mut Vec<Content>) {
children.push(self.into()); 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<Content> { impl ElementComponent for Attr {
fn add_children(self, children: &mut Vec<Content>) { fn add_to_element(self, element: &mut Element) {
children.extend(self); element.attributes.insert(self.name, self.value);
} }
} }
impl<const L: usize> AddChildren for [Content; L] { impl ElementComponent for HashMap<String, String> {
fn add_children(self, children: &mut Vec<Content>) { fn add_to_element(self, element: &mut Element) {
children.extend(self); for (name, value) in self {
Attr::new(name, value).add_to_element(element);
}
} }
} }
macro_rules! add_children_tuple { impl ElementComponent for BTreeMap<String, String> {
fn add_to_element(self, element: &mut Element) {
for (name, value) in self {
Attr::new(name, value).add_to_element(element);
}
}
}
// Children
impl<T: Into<Content>> ElementComponent for T {
fn add_to_element(self, element: &mut Element) {
element.children.push(self.into());
}
}
// Combining components
impl<T: ElementComponent> ElementComponent for Option<T> {
fn add_to_element(self, element: &mut Element) {
if let Some(component) = self {
component.add_to_element(element)
}
}
}
impl<T: ElementComponent, E: ElementComponent> ElementComponent for Result<T, E> {
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<T: ElementComponent> ElementComponent for Vec<T> {
fn add_to_element(self, element: &mut Element) {
for component in self {
component.add_to_element(element);
}
}
}
impl<const L: usize, T: ElementComponent> 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<C1: ElementComponent> 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 ),* ) => { ( $( $t:ident ),* ) => {
impl <$( $t: AddChildren ),*> AddChildren for ($( $t ),*) { impl <$( $t: ElementComponent ),*> ElementComponent for ($( $t ),*) {
fn add_children(self, children: &mut Vec<Content>) { fn add_to_element(self, element: &mut Element) {
#[allow(non_snake_case)] #[allow(non_snake_case)]
let ($( $t ),*) = self; let ($( $t ),*) = self;
$( $t.add_children(children); )* $( $t.add_to_element(element); )*
} }
} }
}; };
} }
add_children_tuple!(C1, C2); element_component_tuple!(C1, C2);
add_children_tuple!(C1, C2, C3); element_component_tuple!(C1, C2, C3);
add_children_tuple!(C1, C2, C3, C4); element_component_tuple!(C1, C2, C3, C4);
add_children_tuple!(C1, C2, C3, C4, C5); element_component_tuple!(C1, C2, C3, C4, C5);
add_children_tuple!(C1, C2, C3, C4, C5, C6); element_component_tuple!(C1, C2, C3, C4, C5, C6);
add_children_tuple!(C1, C2, C3, C4, C5, C6, C7); element_component_tuple!(C1, C2, C3, C4, C5, C6, C7);
add_children_tuple!(C1, C2, C3, C4, C5, C6, C7, C8); element_component_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, C3, C4, C5, C6, C7, C8, C9);
/// An HTML document. /// An HTML document.
/// ///

View file

@ -2,15 +2,15 @@
//! //!
//! <https://developer.mozilla.org/en-US/docs/Web/HTML/Element> //! <https://developer.mozilla.org/en-US/docs/Web/HTML/Element>
use crate::{Element, ElementKind}; use crate::{Element, ElementComponent, ElementKind};
macro_rules! element { macro_rules! element {
( $name:ident ) => { ( $name:ident ) => {
element!($name, ElementKind::Normal); element!($name, ElementKind::Normal);
}; };
( $name:ident, $kind:expr ) => { ( $name:ident, $kind:expr ) => {
pub fn $name() -> Element { pub fn $name(component: impl ElementComponent) -> Element {
Element::new(stringify!($name), $kind) Element::new(stringify!($name), $kind).with(component)
} }
}; };
} }

View file

@ -16,20 +16,17 @@ pub use self::{element::*, render::*};
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{html::*, Content, Element, Render}; use crate::{html::*, Attr, Content, Element, Render};
#[test] #[test]
fn simple_website() { fn simple_website() {
let els = [ let els = [
Content::doctype(), Content::doctype(),
html() html((
.child(head().child(title().child("Hello"))) head(title("Hello")),
.child( body((h1("Hello"), p(("Hello ", em("world"), "!")))),
body() ))
.child(h1().child("Hello")) .into(),
.child(p().child("Hello ").child(em().child("world")).child("!")),
)
.into(),
]; ];
assert_eq!( assert_eq!(
@ -46,35 +43,27 @@ mod tests {
#[test] #[test]
fn void_elements() { fn void_elements() {
// Difference between void and non-void // Difference between void and non-void
assert_eq!(head().render_to_string().unwrap(), "<head></head>"); assert_eq!(head(()).render_to_string().unwrap(), "<head></head>");
assert_eq!(input().render_to_string().unwrap(), "<input>"); assert_eq!(input(()).render_to_string().unwrap(), "<input>");
// Void elements must not contain any children // 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] #[test]
fn raw_text_elements() { fn raw_text_elements() {
assert_eq!( assert_eq!(
script() script("foo <script> & </style> bar")
.child("foo <script> & </style> bar")
.render_to_string() .render_to_string()
.unwrap(), .unwrap(),
"<script>foo <script> & </style> bar</script>", "<script>foo <script> & </style> bar</script>",
); );
println!( println!("{:?}", script("hello </script> world").render_to_string(),);
"{:?}",
script().child("hello </script> world").render_to_string(),
);
assert!(script() assert!(script("hello </script> world").render_to_string().is_err());
.child("hello </script> world")
.render_to_string()
.is_err());
assert!(script() assert!(script("hello </ScRiPt ... world")
.child("hello </ScRiPt ... world")
.render_to_string() .render_to_string()
.is_err()); .is_err());
} }
@ -82,33 +71,29 @@ mod tests {
#[test] #[test]
fn escaped_text_elements() { fn escaped_text_elements() {
assert_eq!( assert_eq!(
textarea() textarea("foo <p> & bar").render_to_string().unwrap(),
.child("foo <p> & bar")
.render_to_string()
.unwrap(),
"<textarea>foo &lt;p&gt; &amp; bar</textarea>", "<textarea>foo &lt;p&gt; &amp; bar</textarea>",
); );
assert!(textarea().child(p()).render_to_string().is_err()); assert!(textarea(p(())).render_to_string().is_err());
} }
#[test] #[test]
fn attributes() { fn attributes() {
assert_eq!( assert_eq!(
input() input((
.attr("name", "tentacles") Attr::new("name", "tentacles"),
.attr("type", "number") Attr::new("type", "number"),
.attr("min", 10) Attr::new("min", 10),
.attr("max", 100) Attr::new("max", 100),
.render_to_string() ))
.unwrap(), .render_to_string()
.unwrap(),
r#"<input max="100" min="10" name="tentacles" type="number">"#, r#"<input max="100" min="10" name="tentacles" type="number">"#,
); );
assert_eq!( assert_eq!(
input() input((Attr::new("name", "horns"), Attr::yes("checked")))
.attr("name", "horns")
.attr_true("checked")
.render_to_string() .render_to_string()
.unwrap(), .unwrap(),
r#"<input checked name="horns">"#, r#"<input checked name="horns">"#,
@ -119,7 +104,7 @@ mod tests {
fn always_lowercase() { fn always_lowercase() {
assert_eq!( assert_eq!(
Element::normal("HTML") Element::normal("HTML")
.attr("LANG", "EN") .with(Attr::new("LANG", "EN"))
.render_to_string() .render_to_string()
.unwrap(), .unwrap(),
r#"<html lang="EN"></html>"#, r#"<html lang="EN"></html>"#,

View file

@ -2,15 +2,15 @@
//! //!
//! <https://developer.mozilla.org/en-US/docs/Web/MathML/Element> //! <https://developer.mozilla.org/en-US/docs/Web/MathML/Element>
use crate::{Element, ElementKind}; use crate::{Element, ElementComponent, ElementKind};
macro_rules! element { macro_rules! element {
( $name:ident ) => { ( $name:ident ) => {
element!($name, stringify!($name)); element!($name, stringify!($name));
}; };
( $name:ident, $tag:expr ) => { ( $name:ident, $tag:expr ) => {
pub fn $name() -> Element { pub fn $name(component: impl ElementComponent) -> Element {
Element::new($tag, ElementKind::Foreign) Element::new($tag, ElementKind::Foreign).with(component)
} }
}; };
} }

View file

@ -2,15 +2,15 @@
//! //!
//! <https://developer.mozilla.org/en-US/docs/Web/SVG/Element> //! <https://developer.mozilla.org/en-US/docs/Web/SVG/Element>
use crate::{Element, ElementKind}; use crate::{Element, ElementComponent, ElementKind};
macro_rules! element { macro_rules! element {
( $name:ident ) => { ( $name:ident ) => {
element!($name, stringify!($name)); element!($name, stringify!($name));
}; };
( $name:ident, $tag:expr ) => { ( $name:ident, $tag:expr ) => {
pub fn $name() -> Element { pub fn $name(component: impl ElementComponent) -> Element {
Element::new($tag, ElementKind::Foreign) Element::new($tag, ElementKind::Foreign).with(component)
} }
}; };
} }