fun_html/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(missing_docs)]

//! This crate provides a simple and efficient way to generate HTML using Rust functions,
//! with an intuitive and composable API to create HTML elements.
//!
//! The [`elt`] module contains functions to create common HTML elements. Most of them take 2 arguments:
//! The list of attributes, and the list of children.
//! For example, `div([id("mydiv")], ["hello".into()])` creates a `<div>` with an ID of "mydiv" containing the text "hello".
//!
//! The [`attr`] module contains functions to create common attributes.
//!
//! Text can be inserted by using the [`elt::text`] function or by using one of the `Into<Element>` implementations.//! ```
//! All text and attribute values are automatically escaped to ensure safe and valid HTML output.
//!
//! ```
//! use fun_html::{attr::class, elt::h1};
//!
//! let greeting = h1(
//!   [class(["bold"])], // <-- attributes
//!   ["Hello world!".into()], // <-- children
//! );
//! assert_eq!(greeting.to_string(), r#"<h1 class="bold">Hello world!</h1>"#);
//! ```
//!
//! Because those are simple rust functions, it is easy to leverage rust features like conditions, loops and iterators:
//!
//! ```
//! # use fun_html::{elt::{li,ul,text}};
//!
//! let list_values = true;
//! let element = if list_values {
//!   ul([], (1..=3).map(|n| li([], [n.to_string().into()])))
//! } else {
//!   text("no value")
//! };
//!
//! assert_eq!(element.to_string(), "<ul><li>1</li><li>2</li><li>3</li></ul>")
//! ```
//!
//! ## Escape hatches
//!
//! If necessary, it is possible to define custom elements and attributes with respectively [`Element::new`] and [`Attribute::new`].
//!
//! It is also possible to declare custom attributes on the fly like this: `div([("hx-get", "/values").into()], [])`)
//!
//! It is also possible to inline raw html with:
//! * [`elt::raw`]: inline HTML that is known at compile time, and is therefore considered safe.
//! * [`elt::raw_unsafe`]: can inline HTML built at runtime, and is therefore unsafe.
//!
//!
//! ## Feature flags
//!
//! * `std`: enabled by default. must be disabled to compile to `no_std`
//! * `rocket_v05`: implements the [`Responder` trait from rocket 0.5](https://docs.rs/rocket/0.5/rocket/response/trait.Responder.html) for [`Document`] and [`Element`]

pub mod attr;
pub mod elt;

mod interop {
    #[cfg(feature = "rocket_v05")]
    mod rocket_v05;
}

extern crate alloc;

use alloc::{borrow::Cow, fmt::Display, vec::Vec};

/// An HTML document (`<!DOCTYPE html>`)
///
/// ## Example
///
/// ```
/// # use fun_html::{Document, html, elt::{head, body}};
/// let doc: Document = html([], [head([], []), body([], [])]);
///
/// assert_eq!(doc.to_string(), "<!DOCTYPE html>\n<html><head></head><body></body></html>");
/// ```
#[derive(Debug, Clone)]
pub struct Document(Element);

/// An HTML element
///
/// It can be created via [`Self::new`], [`Self::new_void`]
///
/// ## Example
///
/// ```
/// # use fun_html::{Element, elt::div};
/// let element: Element = div([], []);
///
/// assert_eq!(element.to_string(), "<div></div>");
/// ```    
#[derive(Debug, Clone)]
pub struct Element(ElementInner);

#[derive(Debug, Clone)]
enum ElementInner {
    Parent {
        tag: &'static str,
        attributes: Vec<Attribute>,
        children: Vec<Element>,
    },
    Void {
        tag: &'static str,
        attributes: Vec<Attribute>,
    },
    Text(Cow<'static, str>),
    Script(Cow<'static, str>),
    Raw(Cow<'static, str>),
    Multiple(Vec<Element>),
    None,
}

/// An attribute
///
/// It can be created via [`Self::new`], [`Self::new_flag`]
/// or by converting from ither `(&'static str, &'static str)` or `(&'static str, String)`.
///
/// See [`attr`] for a collection of common attributes
///
/// ## Example
///
/// ```
/// # use fun_html::attr::id;
/// assert_eq!(
///   id("foo").to_string(),
///   r#"id="foo""#,
/// )
/// ```    
#[derive(Debug, Clone)]
pub struct Attribute {
    name: &'static str,
    value: AttributeValue,
}

#[derive(Debug, Clone)]
enum AttributeValue {
    String(Cow<'static, str>),
    Int(i32),
    None,
}

impl Default for Document {
    fn default() -> Self {
        Self(Element::new(
            "html",
            [],
            [Element::new("head", [], []), Element::new("body", [], [])],
        ))
    }
}

impl Display for Document {
    fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
        write!(f, "<!DOCTYPE html>\n{}", self.0)
    }
}

impl Element {
    /// Create a new HTML element from its tag, attributes, and children
    pub fn new(
        tag: &'static str,
        attributes: impl IntoIterator<Item = Attribute>,
        children: impl IntoIterator<Item = Element>,
    ) -> Self {
        assert_valid_tag_name(tag);
        Self(ElementInner::Parent {
            tag,
            attributes: attributes.into_iter().map(Into::into).collect(),
            children: children.into_iter().collect(),
        })
    }

    /// Create a new [void] HTML element from its tag and attributes
    ///
    /// ("void" element cannot have children and do not need a closing tag)
    ///
    /// [void]: https://developer.mozilla.org/en-US/docs/Glossary/Void_element
    pub fn new_void(tag: &'static str, attributes: impl IntoIterator<Item = Attribute>) -> Self {
        assert_valid_tag_name(tag);
        Self(ElementInner::Void {
            tag,
            attributes: attributes.into_iter().collect(),
        })
    }
}

fn assert_valid_tag_name(tag: &str) {
    debug_assert!(
        !tag.is_empty() && tag.chars().all(|c| !c.is_whitespace()),
        "invalid tag name: '{tag}'"
    );
}

impl From<ElementInner> for Element {
    fn from(value: ElementInner) -> Self {
        Self(value)
    }
}

impl Display for Element {
    fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
        match &self.0 {
            ElementInner::Parent {
                tag,
                attributes,
                children,
            } => {
                write!(f, "<{tag}")?;
                write_attributes(f, attributes)?;
                write!(f, ">")?;
                for child in children {
                    write!(f, "{child}")?;
                }
                write!(f, "</{tag}>")?;
            }
            ElementInner::Void { tag, attributes } => {
                write!(f, "<{tag}")?;
                write_attributes(f, attributes)?;
                write!(f, ">")?;
            }
            ElementInner::Text(text) => write!(f, "{}", html_escape::encode_text(text))?,
            ElementInner::Script(text) => write!(f, "{}", html_escape::encode_script(text))?,
            ElementInner::Raw(raw) => write!(f, "{raw}")?,
            ElementInner::Multiple(elems) => {
                for elt in elems {
                    write!(f, "{elt}")?;
                }
            }
            ElementInner::None => (),
        }
        Ok(())
    }
}

fn write_attributes(
    f: &mut alloc::fmt::Formatter<'_>,
    attributes: &Vec<Attribute>,
) -> Result<(), alloc::fmt::Error> {
    for attribute in attributes {
        write!(f, " {attribute}")?;
    }
    Ok(())
}

impl Display for Attribute {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}", self.name)?;
        match &self.value {
            AttributeValue::String(value) => {
                write!(
                    f,
                    "=\"{}\"",
                    html_escape::encode_double_quoted_attribute(value)
                )?;
            }
            AttributeValue::Int(int) => {
                write!(f, "=\"{int}\"",)?;
            }
            AttributeValue::None => (),
        }
        Ok(())
    }
}

impl Attribute {
    /// Create a new attribute
    pub fn new(name: &'static str, value: impl Into<Cow<'static, str>>) -> Self {
        assert_valid_attribute_name(name);
        Self {
            name,
            value: AttributeValue::String(value.into()),
        }
    }

    /// Create attribute with an integer value
    pub fn new_int(name: &'static str, value: i32) -> Self {
        assert_valid_attribute_name(name);
        Self {
            name,
            value: AttributeValue::Int(value),
        }
    }

    /// Create a new flag attribute (that doesn't take a value)
    pub fn new_flag(name: &'static str) -> Self {
        assert_valid_attribute_name(name);
        Self {
            name,
            value: AttributeValue::None,
        }
    }
}

fn assert_valid_attribute_name(name: &str) {
    debug_assert!(
        !name.is_empty() && name.chars().all(|c| !c.is_whitespace()),
        "invalid attribute name: '{name}'"
    );
}

impl IntoIterator for Element {
    type Item = Self;
    type IntoIter = core::iter::Once<Self>;
    fn into_iter(self) -> Self::IntoIter {
        core::iter::once(self)
    }
}

impl<const N: usize> From<[Element; N]> for Element {
    fn from(value: [Element; N]) -> Self {
        Vec::from(value).into()
    }
}

impl From<Vec<Element>> for Element {
    fn from(value: Vec<Element>) -> Self {
        Self(ElementInner::Multiple(value))
    }
}

/// Create an HTML [`Document`]
///
/// You must pass the [`elt::head`] and [`elt::body`] element as you would with any other element.
///
/// ## Example
///
/// ```
/// # use fun_html::{html, elt::{head, body, title, h1}, attr::{lang}};
/// let document = html([lang("en")], [
///     head([], [title([], "Greetings")]),
///     body([], [h1([], ["Hello world!".into()])]),
/// ]);
/// ```
pub fn html(
    attributes: impl IntoIterator<Item = Attribute>,
    children: impl IntoIterator<Item = Element>,
) -> Document {
    Document(Element::new("html", attributes, children))
}