Improve element building API
This commit is contained in:
parent
c4b3a279cc
commit
a6fd12510d
5 changed files with 154 additions and 92 deletions
163
src/element.rs
163
src/element.rs
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
src/lib.rs
65
src/lib.rs
|
|
@ -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 <p> & bar</textarea>",
|
"<textarea>foo <p> & 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>"#,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue