fun_html/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![cfg_attr(docsrs, feature(doc_auto_cfg))]
3#![warn(missing_docs)]
4
5//! This crate provides a simple and efficient way to generate HTML using Rust functions,
6//! with an intuitive and composable API to create HTML elements.
7//!
8//! The [`elt`] module contains functions to create common HTML elements. Most of them take 2 arguments:
9//! The list of attributes, and the list of children.
10//! For example, `div([id("mydiv")], ["hello".into()])` creates a `<div>` with an ID of "mydiv" containing the text "hello".
11//!
12//! The [`attr`] module contains functions to create common attributes.
13//!
14//! Text can be inserted by using the [`elt::text`] function or by using one of the `Into<Element>` implementations.
15//! All text and attribute values are automatically escaped to ensure safe and valid HTML output.
16//!
17//! ```
18//! use fun_html::{attr, elt};
19//!
20//! let greeting = elt::h1(
21//!   [attr::class(["bold"])], // <-- attributes
22//!   ["Hello world!".into()], // <-- children
23//! );
24//! assert_eq!(greeting.to_string(), r#"<h1 class="bold">Hello world!</h1>"#);
25//! ```
26//!
27//! Because those are simple rust functions, it is easy to leverage rust features like conditions, loops and iterators:
28//!
29//! ```
30//! use fun_html::{elt, Element};
31//!
32//! fn list_view(items: Vec<i32>) -> Element {
33//!   if !items.is_empty(){
34//!     elt::ul([], items.into_iter().map(|item| elt::li([], [item.to_string().into()])))
35//!   } else {
36//!     elt::text("no item")
37//!   }
38//! };
39//!
40//! assert_eq!(
41//!   list_view((1..=3).collect()).to_string(),
42//!   "<ul><li>1</li><li>2</li><li>3</li></ul>"
43//! );
44//! ```
45//!
46//! ## Escape hatches
47//!
48//! If necessary, it is possible to define custom elements and attributes with respectively [`Element::new`] and [`Attribute::new`].
49//!
50//! It is also possible to declare custom attributes on the fly like this: `div([("hx-get", "/values").into()], [])`)
51//!
52//! It is also possible to inline raw html with:
53//! * [`elt::raw`]: inline HTML that is known at compile time, and is therefore considered safe.
54//! * [`elt::raw_unsafe`]: can inline HTML built at runtime, and is therefore unsafe.
55//!
56//!
57//! ## Feature flags
58//!
59//! * `std`: enabled by default. must be disabled to compile to `no_std`
60//! * `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`]
61//! * `salvo_v074`: implements the [`Scribe` trait from salvo 0.74](https://docs.rs/salvo/0.74/salvo/trait.Scribe.html) for [`Document`] and [`Element`]
62//! * `salvo_v076`: implements the [`Scribe` trait from salvo 0.76](https://docs.rs/salvo/0.76/salvo/trait.Scribe.html) for [`Document`] and [`Element`]
63//! * `maud_v026`: implements `Render` add provide conversion from/to `Markup` for [maud 0.26](https://docs.rs/maud/0.26)
64
65pub mod attr;
66pub mod elt;
67
68mod interop {
69    #[cfg(feature = "maud_v026")]
70    mod maud_v026;
71    #[cfg(feature = "rocket_v05")]
72    mod rocket_v05;
73    #[cfg(feature = "salvo_v074")]
74    mod salvo_v074;
75    #[cfg(feature = "salvo_v076")]
76    mod salvo_v076;
77}
78
79extern crate alloc;
80
81use alloc::{borrow::Cow, fmt::Display, vec::Vec};
82
83/// An HTML document (`<!DOCTYPE html>`)
84///
85/// ## Example
86///
87/// ```
88/// # use fun_html::{Document, html, elt::{head, body}};
89/// let doc: Document = html([], [head([], []), body([], [])]);
90///
91/// assert_eq!(doc.to_string(), "<!DOCTYPE html>\n<html><head></head><body></body></html>");
92/// ```
93#[derive(Debug, Clone)]
94pub struct Document(Element);
95
96/// An HTML element
97///
98/// It can be created via [`Self::new`], [`Self::new_void`]
99///
100/// ## Example
101///
102/// ```
103/// # use fun_html::{Element, elt::div};
104/// let element: Element = div([], []);
105///
106/// assert_eq!(element.to_string(), "<div></div>");
107/// ```    
108#[derive(Debug, Clone, Default)]
109pub struct Element(ElementInner);
110
111#[derive(Debug, Clone)]
112enum ElementInner {
113    Parent {
114        tag: &'static str,
115        attributes: Vec<Attribute>,
116        children: Vec<Element>,
117    },
118    Void {
119        tag: &'static str,
120        attributes: Vec<Attribute>,
121    },
122    Text(Cow<'static, str>),
123    Script(Cow<'static, str>),
124    Raw(Cow<'static, str>),
125    Multiple(Vec<Element>),
126    None,
127}
128
129impl Default for ElementInner {
130    fn default() -> Self {
131        Self::None
132    }
133}
134
135/// An attribute
136///
137/// It can be created via [`Self::new`], [`Self::new_flag`]
138/// or by converting from ither `(&'static str, &'static str)` or `(&'static str, String)`.
139///
140/// See [`attr`] for a collection of common attributes
141///
142/// ## Example
143///
144/// ```
145/// # use fun_html::attr::id;
146/// assert_eq!(
147///   id("foo").to_string(),
148///   r#"id="foo""#,
149/// )
150/// ```    
151#[derive(Debug, Clone, Default)]
152pub struct Attribute(AttributeInner);
153
154#[derive(Debug, Clone)]
155enum AttributeInner {
156    KeyValue(Cow<'static, str>, Cow<'static, str>),
157    KeyValueInt(Cow<'static, str>, i32),
158    Flag(Cow<'static, str>),
159    None,
160}
161
162impl Default for AttributeInner {
163    fn default() -> Self {
164        Self::None
165    }
166}
167
168impl Default for Document {
169    fn default() -> Self {
170        Self(Element::new(
171            "html",
172            [],
173            [Element::new("head", [], []), Element::new("body", [], [])],
174        ))
175    }
176}
177
178impl Display for Document {
179    fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
180        write!(f, "<!DOCTYPE html>\n{}", self.0)
181    }
182}
183
184impl Element {
185    /// Create a new HTML element from its tag, attributes, and children
186    pub fn new(
187        tag: &'static str,
188        attributes: impl IntoIterator<Item = Attribute>,
189        children: impl IntoIterator<Item = Element>,
190    ) -> Self {
191        assert_valid_tag_name(tag);
192        Self(ElementInner::Parent {
193            tag,
194            attributes: attributes.into_iter().map(Into::into).collect(),
195            children: children.into_iter().collect(),
196        })
197    }
198
199    /// Create a new [void] HTML element from its tag and attributes
200    ///
201    /// ("void" element cannot have children and do not need a closing tag)
202    ///
203    /// [void]: https://developer.mozilla.org/en-US/docs/Glossary/Void_element
204    pub fn new_void(tag: &'static str, attributes: impl IntoIterator<Item = Attribute>) -> Self {
205        assert_valid_tag_name(tag);
206        Self(ElementInner::Void {
207            tag,
208            attributes: attributes.into_iter().collect(),
209        })
210    }
211}
212
213fn assert_valid_tag_name(tag: &str) {
214    debug_assert!(
215        !tag.is_empty() && tag.chars().all(|c| !c.is_whitespace()),
216        "invalid tag name: '{tag}'"
217    );
218}
219
220impl From<ElementInner> for Element {
221    fn from(value: ElementInner) -> Self {
222        Self(value)
223    }
224}
225
226impl Display for Element {
227    fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
228        match &self.0 {
229            ElementInner::Parent {
230                tag,
231                attributes,
232                children,
233            } => {
234                write!(f, "<{tag}")?;
235                write_attributes(f, attributes)?;
236                write!(f, ">")?;
237                for child in children {
238                    write!(f, "{child}")?;
239                }
240                write!(f, "</{tag}>")?;
241            }
242            ElementInner::Void { tag, attributes } => {
243                write!(f, "<{tag}")?;
244                write_attributes(f, attributes)?;
245                write!(f, ">")?;
246            }
247            ElementInner::Text(text) => write!(f, "{}", html_escape::encode_text(text))?,
248            ElementInner::Script(text) => write!(f, "{}", html_escape::encode_script(text))?,
249            ElementInner::Raw(raw) => write!(f, "{raw}")?,
250            ElementInner::Multiple(elems) => {
251                for elt in elems {
252                    write!(f, "{elt}")?;
253                }
254            }
255            ElementInner::None => (),
256        }
257        Ok(())
258    }
259}
260
261fn write_attributes(
262    f: &mut alloc::fmt::Formatter<'_>,
263    attributes: &[Attribute],
264) -> Result<(), alloc::fmt::Error> {
265    for attribute in attributes
266        .iter()
267        .filter(|a| !matches!(&a.0, AttributeInner::None))
268    {
269        write!(f, " {attribute}")?;
270    }
271    Ok(())
272}
273
274impl Display for Attribute {
275    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
276        match &self.0 {
277            AttributeInner::KeyValue(key, value) => {
278                write!(
279                    f,
280                    "{}=\"{}\"",
281                    key,
282                    html_escape::encode_double_quoted_attribute(&value)
283                )
284            }
285            AttributeInner::KeyValueInt(key, value) => {
286                write!(f, "{key}=\"{value}\"")
287            }
288            AttributeInner::Flag(key) => write!(f, "{key}"),
289            AttributeInner::None => Ok(()),
290        }
291    }
292}
293
294impl Attribute {
295    /// Create a new attribute
296    pub fn new(name: &'static str, value: impl Into<Cow<'static, str>>) -> Self {
297        assert_valid_attribute_name(name);
298        Self(AttributeInner::KeyValue(name.into(), value.into()))
299    }
300
301    /// Create attribute with an integer value
302    pub fn new_int(name: &'static str, value: i32) -> Self {
303        assert_valid_attribute_name(name);
304        Self(AttributeInner::KeyValueInt(name.into(), value))
305    }
306
307    /// Create a new flag attribute (that doesn't take a value)
308    pub fn new_flag(name: &'static str) -> Self {
309        assert_valid_attribute_name(name);
310        Self(AttributeInner::Flag(name.into()))
311    }
312
313    /// Create a new attribute with a name generated at runtime
314    ///
315    /// This **IS NOT SAFE** as the attribute name is not escaped.
316    /// It is necessary to control or validate the attribute name to avoid being vulnerable to XSS attacks.
317    ///
318    /// The attribute value is escaped normally.
319    pub fn new_unsafe_name(
320        name: impl Into<Cow<'static, str>>,
321        value: impl Into<Cow<'static, str>>,
322    ) -> Self {
323        Self(AttributeInner::KeyValue(name.into(), value.into()))
324    }
325}
326
327fn assert_valid_attribute_name(name: &str) {
328    debug_assert!(
329        !name.is_empty() && name.chars().all(|c| !c.is_whitespace()),
330        "invalid attribute name: '{name}'"
331    );
332}
333
334impl IntoIterator for Element {
335    type Item = Self;
336    type IntoIter = core::iter::Once<Self>;
337    fn into_iter(self) -> Self::IntoIter {
338        core::iter::once(self)
339    }
340}
341
342impl<const N: usize> From<[Element; N]> for Element {
343    fn from(value: [Element; N]) -> Self {
344        Vec::from(value).into()
345    }
346}
347
348impl From<Vec<Element>> for Element {
349    fn from(value: Vec<Element>) -> Self {
350        Self(ElementInner::Multiple(value))
351    }
352}
353
354/// Create an HTML [`Document`]
355///
356/// You must pass the [`elt::head`] and [`elt::body`] element as you would with any other element.
357///
358/// ## Example
359///
360/// ```
361/// # use fun_html::{html, elt::{head, body, title, h1}, attr::{lang}};
362/// let document = html([lang("en")], [
363///     head([], [title([], "Greetings")]),
364///     body([], [h1([], ["Hello world!".into()])]),
365/// ]);
366/// ```
367pub fn html(
368    attributes: impl IntoIterator<Item = Attribute>,
369    children: impl IntoIterator<Item = Element>,
370) -> Document {
371    Document(Element::new("html", attributes, children))
372}