Document library
This commit is contained in:
parent
cb2a6da25c
commit
cfd83df713
4 changed files with 325 additions and 27 deletions
30
README.md
Normal file
30
README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# el
|
||||||
|
|
||||||
|
`el` is a Rust library for writing, modifying, and safely rendering HTML
|
||||||
|
elements as simple data structures. It is inspired by [hiccup] and named after a
|
||||||
|
small helper function I once wrote in JS.
|
||||||
|
|
||||||
|
[hiccup]: https://github.com/weavejester/hiccup
|
||||||
|
|
||||||
|
## Usage example
|
||||||
|
|
||||||
|
```rs
|
||||||
|
use el::{Attr, Render, html::*};
|
||||||
|
|
||||||
|
let page: String = html((
|
||||||
|
head((
|
||||||
|
meta(Attr::new("charset", "utf-8")),
|
||||||
|
meta((
|
||||||
|
Attr::new("name", "viewport"),
|
||||||
|
Attr::new("content", "width=device-width, initial-scale=1"),
|
||||||
|
)),
|
||||||
|
title("Example page"),
|
||||||
|
)),
|
||||||
|
body((
|
||||||
|
h1((Attr::id("heading"), "Example page")),
|
||||||
|
p(("This is an example for a ", em("simple"), " web page.")),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.render_to_string()
|
||||||
|
.unwrap();
|
||||||
|
```
|
||||||
195
src/element.rs
195
src/element.rs
|
|
@ -1,7 +1,11 @@
|
||||||
use std::collections::{btree_map::Entry, BTreeMap, HashMap};
|
use std::collections::{btree_map::Entry, BTreeMap, HashMap};
|
||||||
|
|
||||||
/// <https://html.spec.whatwg.org/multipage/syntax.html#elements-2>
|
/// The kind of an element.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
///
|
||||||
|
/// Follows the [definitions from the HTML standard][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://html.spec.whatwg.org/multipage/syntax.html#elements-2
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ElementKind {
|
pub enum ElementKind {
|
||||||
Void,
|
Void,
|
||||||
Template,
|
Template,
|
||||||
|
|
@ -11,27 +15,71 @@ pub enum ElementKind {
|
||||||
Normal,
|
Normal,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
/// A single bit of [`Element`] content.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Content {
|
pub enum Content {
|
||||||
|
/// A raw string to be rendered without any checks.
|
||||||
|
///
|
||||||
|
/// Can also be constructed using [`Self::raw`].
|
||||||
|
///
|
||||||
|
/// # Warning
|
||||||
|
///
|
||||||
|
/// This is an escape hatch for including arbitrary text. Using it
|
||||||
|
/// incorrectly may result in security vulnerabilities in the rendered HTML.
|
||||||
Raw(String),
|
Raw(String),
|
||||||
|
/// Plain text.
|
||||||
|
///
|
||||||
|
/// Can also be constructed using [`Self::text`].
|
||||||
Text(String),
|
Text(String),
|
||||||
|
/// An HTML comment (`<!-- ... -->`).
|
||||||
|
///
|
||||||
|
/// Can also be constructed using [`Self::comment`].
|
||||||
Comment(String),
|
Comment(String),
|
||||||
|
/// A child [`Element`].
|
||||||
|
///
|
||||||
|
/// Can also be constructed using [`Self::element`].
|
||||||
Element(Element),
|
Element(Element),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Content {
|
impl Content {
|
||||||
|
/// Construct [`Content::Raw`], a raw string to be rendered without any
|
||||||
|
/// checks.
|
||||||
|
///
|
||||||
|
/// # Warning
|
||||||
|
///
|
||||||
|
/// This is an escape hatch for including arbitrary text. Using it
|
||||||
|
/// incorrectly may result in security vulnerabilities in the rendered HTML.
|
||||||
pub fn raw(str: impl ToString) -> Self {
|
pub fn raw(str: impl ToString) -> Self {
|
||||||
Self::Raw(str.to_string())
|
Self::Raw(str.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct [`Content::Text`], plain text.
|
||||||
pub fn text(str: impl ToString) -> Self {
|
pub fn text(str: impl ToString) -> Self {
|
||||||
Self::Text(str.to_string())
|
Self::Text(str.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct [`Content::Comment`], an HTML comment (`<!-- ... -->`).
|
||||||
pub fn comment(str: impl ToString) -> Self {
|
pub fn comment(str: impl ToString) -> Self {
|
||||||
Self::Comment(str.to_string())
|
Self::Comment(str.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct [`Content::Element`], a child [`Element`].
|
||||||
|
///
|
||||||
|
/// Instead of calling `Content::element(foo)`, you can also use
|
||||||
|
/// `foo.into()`.
|
||||||
|
pub fn element(e: impl Into<Element>) -> Self {
|
||||||
|
Self::Element(e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a doctype of the form `<!DOCTYPE html>`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use el::Content;
|
||||||
|
/// let doctype = Content::doctype();
|
||||||
|
/// assert_eq!(doctype, Content::raw("<!DOCTYPE html>"));
|
||||||
|
/// ```
|
||||||
pub fn doctype() -> Self {
|
pub fn doctype() -> Self {
|
||||||
Self::raw("<!DOCTYPE html>")
|
Self::raw("<!DOCTYPE html>")
|
||||||
}
|
}
|
||||||
|
|
@ -55,15 +103,61 @@ impl From<Element> for Content {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
/// An HTML element.
|
||||||
|
///
|
||||||
|
/// SVG and MathML elements are also modelled using this type.
|
||||||
|
///
|
||||||
|
/// Errors (e.g. illegal characters or an element of [`ElementKind::Void`]
|
||||||
|
/// having children) are deferred until rendering and are not checked during
|
||||||
|
/// element construction. See also [`crate::Render`] and [`crate::Error`].
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Element {
|
pub struct Element {
|
||||||
|
/// The tag name of the element.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// What kind of element this is.
|
||||||
|
///
|
||||||
|
/// # Warning
|
||||||
|
///
|
||||||
|
/// The element kind affects the correctness of the rendered output.
|
||||||
|
/// Choosing an incorrect kind may result in security vulnerabilities in the
|
||||||
|
/// rendered HTML. See [`ElementKind`] for more details.
|
||||||
pub kind: ElementKind,
|
pub kind: ElementKind,
|
||||||
|
/// The attributes (e.g. `id` or `class`) of the element.
|
||||||
|
///
|
||||||
|
/// This map does not take into account case insensitivity of attributes.
|
||||||
|
/// Any attributes contained in the map will appear in the rendered output.
|
||||||
pub attributes: BTreeMap<String, String>,
|
pub attributes: BTreeMap<String, String>,
|
||||||
|
/// The children of the element.
|
||||||
pub children: Vec<Content>,
|
pub children: Vec<Content>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element {
|
impl Element {
|
||||||
|
/// Create a new element of a specific [`ElementKind`].
|
||||||
|
///
|
||||||
|
/// See also [`Self::normal`] to create elements of kind
|
||||||
|
/// [`ElementKind::Normal`].
|
||||||
|
///
|
||||||
|
/// # Warning
|
||||||
|
///
|
||||||
|
/// The element kind affects the correctness of the rendered output.
|
||||||
|
/// Choosing an incorrect kind may result in security vulnerabilities in the
|
||||||
|
/// rendered HTML. See [`ElementKind`] for more details.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use el::{Element, ElementKind, html, svg};
|
||||||
|
///
|
||||||
|
/// let p = Element::new("p", ElementKind::Normal);
|
||||||
|
/// let link = Element::new("link", ElementKind::Void);
|
||||||
|
/// let script = Element::new("script", ElementKind::RawText);
|
||||||
|
/// let svg = Element::new("svg", ElementKind::Foreign);
|
||||||
|
///
|
||||||
|
/// assert_eq!(p, html::p(()));
|
||||||
|
/// assert_eq!(link, html::link(()));
|
||||||
|
/// assert_eq!(script, html::script(()));
|
||||||
|
/// assert_eq!(svg, svg::svg(()));
|
||||||
|
/// ```
|
||||||
pub fn new(name: impl ToString, kind: ElementKind) -> Self {
|
pub fn new(name: impl ToString, kind: ElementKind) -> Self {
|
||||||
let mut name = name.to_string();
|
let mut name = name.to_string();
|
||||||
if kind != ElementKind::Foreign {
|
if kind != ElementKind::Foreign {
|
||||||
|
|
@ -78,26 +172,109 @@ impl Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new element of the kind [`ElementKind::Normal`].
|
||||||
|
///
|
||||||
|
/// `Element::normal(foo)` is equivalent to calling `Element::new(foo,
|
||||||
|
/// ElementKind::Normal)`.
|
||||||
|
///
|
||||||
|
/// # Warning
|
||||||
|
///
|
||||||
|
/// The element kind affects the correctness of the rendered output.
|
||||||
|
/// Choosing an incorrect kind may result in security vulnerabilities in the
|
||||||
|
/// rendered HTML. See [`ElementKind`] for more details.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use el::{Element, ElementKind};
|
||||||
|
/// let element = Element::normal("custom");
|
||||||
|
/// assert_eq!(element.kind, ElementKind::Normal);
|
||||||
|
/// ```
|
||||||
pub fn normal(name: impl ToString) -> Self {
|
pub fn normal(name: impl ToString) -> Self {
|
||||||
Self::new(name, ElementKind::Normal)
|
Self::new(name, ElementKind::Normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add components to the element in-place.
|
||||||
|
///
|
||||||
|
/// To add multiple components, either call this function repeatedly or use
|
||||||
|
/// a type like tuples, arrays, [`Vec`], [`Option`], [`Result`] to combine
|
||||||
|
/// multiple components. See [`ElementComponent`] for more info.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use el::{Attr, html::*};
|
||||||
|
/// let mut element = p(());
|
||||||
|
///
|
||||||
|
/// // Adding single components
|
||||||
|
/// element.add("some text");
|
||||||
|
/// element.add(Attr::class("foo"));
|
||||||
|
///
|
||||||
|
/// // Adding multiple components
|
||||||
|
/// element.add((Attr::id("bar"), " ", em("and"), " some more text"));
|
||||||
|
/// ```
|
||||||
pub fn add(&mut self, c: impl ElementComponent) {
|
pub fn add(&mut self, c: impl ElementComponent) {
|
||||||
c.add_to_element(self);
|
c.add_to_element(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A more builder-pattern-like version of [`Self::add`].
|
||||||
|
///
|
||||||
|
/// Instead of a mutable reference, this function takes ownership of the
|
||||||
|
/// element before returning it again. This can be more ergonomic in some
|
||||||
|
/// cases.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use el::{Attr, html::*};
|
||||||
|
///
|
||||||
|
/// let element = p(())
|
||||||
|
/// // Adding single components
|
||||||
|
/// .with("some text")
|
||||||
|
/// .with(Attr::class("foo"))
|
||||||
|
/// // Adding multiple components
|
||||||
|
/// .with((Attr::id("bar"), " ", em("and"), " some more text"));
|
||||||
|
/// ```
|
||||||
pub fn with(mut self, c: impl ElementComponent) -> Self {
|
pub fn with(mut self, c: impl ElementComponent) -> Self {
|
||||||
self.add(c);
|
self.add(c);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A component can add itself to an [`Element`] by modifying it.
|
||||||
|
///
|
||||||
|
/// A component usually represents either a bit of content or an attribute for
|
||||||
|
/// the element it is being added to. Some components (e.g. tuples, arrays,
|
||||||
|
/// [`Vec`], [`Option`], [`Result`]) consist of further components. This creates
|
||||||
|
/// a flexible API for building [`Element`]s:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use el::{Attr, Render, html::*};
|
||||||
|
/// let p = p((
|
||||||
|
/// Attr::id("foo"),
|
||||||
|
/// Attr::class("bar"),
|
||||||
|
/// Attr::class("baz"),
|
||||||
|
/// "Hello ", em("world"), "!",
|
||||||
|
/// ));
|
||||||
|
/// assert_eq!(
|
||||||
|
/// p.render_to_string().unwrap(),
|
||||||
|
/// r#"<p class="bar baz" id="foo">Hello <em>world</em>!</p>"#,
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
pub trait ElementComponent {
|
pub trait ElementComponent {
|
||||||
|
/// Add a component to an element, consuming the component in the process.
|
||||||
fn add_to_element(self, element: &mut Element);
|
fn add_to_element(self, element: &mut Element);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attributes
|
/// An element attribute, used during [`Element`] construction.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use el::{Attr, html::*};
|
||||||
|
/// let p = p(Attr::class("foo"));
|
||||||
|
/// assert_eq!(p.attributes["class"], "foo");
|
||||||
|
/// ```
|
||||||
pub struct Attr {
|
pub struct Attr {
|
||||||
name: String,
|
name: String,
|
||||||
value: String,
|
value: String,
|
||||||
|
|
@ -292,11 +469,11 @@ 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);
|
||||||
element_component_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.
|
/// A full HTML document including doctype.
|
||||||
///
|
///
|
||||||
/// A `Document(el)` is basically the same as `[Content::doctype(), el.into()]`
|
/// A `Document(el)` is basically the same as `[Content::doctype(), el.into()]`
|
||||||
/// for the purposes of the [`crate::Render`] trait.
|
/// for the purposes of the [`Render`][crate::Render] trait.
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Document(pub Element);
|
pub struct Document(pub Element);
|
||||||
|
|
||||||
impl From<Element> for Document {
|
impl From<Element> for Document {
|
||||||
|
|
|
||||||
68
src/lib.rs
68
src/lib.rs
|
|
@ -1,7 +1,71 @@
|
||||||
//! Create HTML by manipulating elements as structured data. Inspired by the
|
//! # el
|
||||||
//! clojure library [hiccup][hiccup].
|
//!
|
||||||
|
//! Write, modify, and safely render HTML elements as simple data structures.
|
||||||
|
//!
|
||||||
|
//! This library is inspired by [hiccup] and named after a small helper function
|
||||||
|
//! I once wrote in JS.
|
||||||
//!
|
//!
|
||||||
//! [hiccup]: https://github.com/weavejester/hiccup
|
//! [hiccup]: https://github.com/weavejester/hiccup
|
||||||
|
//!
|
||||||
|
//! ## Library overview
|
||||||
|
//!
|
||||||
|
//! The basic data structure is the [`Element`], which can be rendered to a
|
||||||
|
//! [`String`] using the [`Render`] trait. Custom elements can be constructed
|
||||||
|
//! using [`Element::normal`] or [`Element::new`]. Once constructed, elements
|
||||||
|
//! can be modified by accessing their fields or using the [`Element::add`] or
|
||||||
|
//! [`Element::with`] methods, though this is usually not necessary.
|
||||||
|
//!
|
||||||
|
//! Constructor functions for all (non-deprecated) HTML tags can be found in the
|
||||||
|
//! [`html`] module, SVG tags in [`svg`] and MathML tags in [`mathml`]. These
|
||||||
|
//! three modules are designed to be wildcard-included for more concise code,
|
||||||
|
//! either on a per-function or per-file basis.
|
||||||
|
//!
|
||||||
|
//! Element construction uses the [`ElementComponent`] trait, which represents
|
||||||
|
//! not only element contents but also attributes. Tuples, arrays, [`Vec`],
|
||||||
|
//! [`Option`], and [`Result`] can be used to combine components. The order of
|
||||||
|
//! content components is preserved. To set attributes, include [`Attr`] values
|
||||||
|
//! as components.
|
||||||
|
//!
|
||||||
|
//! If you want to render an entire web page, wrap an [`html::html`] element in
|
||||||
|
//! a [`Document`]. When rendered, documents include the `<!DOCTYPE html>`
|
||||||
|
//! annotation required by the standard.
|
||||||
|
//!
|
||||||
|
//! ## Usage example
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use el::{Attr, Render, html::*};
|
||||||
|
//!
|
||||||
|
//! let page: String = html((
|
||||||
|
//! head((
|
||||||
|
//! meta(Attr::new("charset", "utf-8")),
|
||||||
|
//! meta((
|
||||||
|
//! Attr::new("name", "viewport"),
|
||||||
|
//! Attr::new("content", "width=device-width, initial-scale=1"),
|
||||||
|
//! )),
|
||||||
|
//! title("Example page"),
|
||||||
|
//! )),
|
||||||
|
//! body((
|
||||||
|
//! h1((Attr::id("heading"), "Example page")),
|
||||||
|
//! p(("This is an example for a ", em("simple"), " web page.")),
|
||||||
|
//! )),
|
||||||
|
//! ))
|
||||||
|
//! .render_to_string()
|
||||||
|
//! .unwrap();
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Axum support
|
||||||
|
//!
|
||||||
|
//! The [axum] crate is supported via the optional `axum` feature flag. When it
|
||||||
|
//! is enabled, [`Document`] implements axum's `IntoResponse` trait and can be
|
||||||
|
//! returned directly from handlers. In order to prevent accidentally returning
|
||||||
|
//! incomplete HTML documents, [`Element`] does not implement `IntoResponse`.
|
||||||
|
//!
|
||||||
|
//! ```toml
|
||||||
|
//! [dependencies]
|
||||||
|
//! el = { version = "...", features = ["axum"] }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! [axum]: https://crates.io/crates/axum
|
||||||
|
|
||||||
#[cfg(feature = "axum")]
|
#[cfg(feature = "axum")]
|
||||||
mod axum;
|
mod axum;
|
||||||
|
|
|
||||||
|
|
@ -6,33 +6,42 @@ use crate::{
|
||||||
Document,
|
Document,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The cause of an [`Error`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ErrorCause {
|
pub enum ErrorCause {
|
||||||
|
/// An error occurred while formatting a value.
|
||||||
Format(fmt::Error),
|
Format(fmt::Error),
|
||||||
InvalidTagName(String),
|
/// A name is not a valid tag name.
|
||||||
InvalidAttrName(String),
|
InvalidTagName { name: String },
|
||||||
|
/// A name is not a valid attribute name.
|
||||||
|
InvalidAttrName { name: String },
|
||||||
|
/// A child is in a place where it is not allowed (e.g. it is the child of a
|
||||||
|
/// [`ElementKind::Void`] element).
|
||||||
InvalidChild,
|
InvalidChild,
|
||||||
InvalidRawText(String),
|
/// Text inside a [`ElementKind::RawText`] element contains forbidden
|
||||||
|
/// structures.
|
||||||
|
InvalidRawText { text: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An error that can occur during element rendering.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
reverse_path: Vec<String>,
|
reverse_path: Vec<(usize, Option<String>)>,
|
||||||
cause: ErrorCause,
|
cause: ErrorCause,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
pub fn new(cause: ErrorCause) -> Self {
|
pub(crate) fn new(cause: ErrorCause) -> Self {
|
||||||
Self {
|
Self {
|
||||||
reverse_path: vec![],
|
reverse_path: vec![],
|
||||||
cause,
|
cause,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn at(mut self, index: usize, child: &Content) -> Self {
|
pub(crate) fn at(mut self, index: usize, child: &Content) -> Self {
|
||||||
self.reverse_path.push(match child {
|
self.reverse_path.push(match child {
|
||||||
Content::Element(el) => format!("{index}[{}]", el.name),
|
Content::Element(el) => (index, Some(el.name.clone())),
|
||||||
_ => index.to_string(),
|
_ => (index, None),
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +75,11 @@ impl Error {
|
||||||
})
|
})
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The cause of the error.
|
||||||
|
pub fn cause(&self) -> &ErrorCause {
|
||||||
|
&self.cause
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
|
|
@ -74,10 +88,10 @@ impl fmt::Display for Error {
|
||||||
|
|
||||||
match &self.cause {
|
match &self.cause {
|
||||||
ErrorCause::Format(error) => write!(f, "{error}")?,
|
ErrorCause::Format(error) => write!(f, "{error}")?,
|
||||||
ErrorCause::InvalidTagName(name) => write!(f, "Invalid tag name {name:?}")?,
|
ErrorCause::InvalidTagName { name } => write!(f, "Invalid tag name {name:?}")?,
|
||||||
ErrorCause::InvalidAttrName(name) => write!(f, "Invalid attribute name {name:?}")?,
|
ErrorCause::InvalidAttrName { name } => write!(f, "Invalid attribute name {name:?}")?,
|
||||||
ErrorCause::InvalidChild => write!(f, "Invalid child")?,
|
ErrorCause::InvalidChild => write!(f, "Invalid child")?,
|
||||||
ErrorCause::InvalidRawText(text) => write!(f, "Invalid raw text {text:?}")?,
|
ErrorCause::InvalidRawText { text } => write!(f, "Invalid raw text {text:?}")?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -92,11 +106,20 @@ impl From<fmt::Error> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A wrapper around [`std::result::Result`] with the error [`Error`].
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
/// Render an [`Element`] or a [`Document`] to a [`fmt::Write`]; usually a
|
||||||
|
/// [`String`].
|
||||||
|
///
|
||||||
|
/// To implement this trait, only [`Self::render`] needs to be implemented.
|
||||||
pub trait Render {
|
pub trait Render {
|
||||||
|
/// Render to a writer.
|
||||||
fn render<W: fmt::Write>(&self, w: &mut W) -> Result<()>;
|
fn render<W: fmt::Write>(&self, w: &mut W) -> Result<()>;
|
||||||
|
|
||||||
|
/// Render directly to a [`String`].
|
||||||
|
///
|
||||||
|
/// This method is implemented by default and uses [`Self::render`].
|
||||||
fn render_to_string(&self) -> Result<String> {
|
fn render_to_string(&self) -> Result<String> {
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
self.render(&mut result)?;
|
self.render(&mut result)?;
|
||||||
|
|
@ -137,11 +160,15 @@ impl Render for Element {
|
||||||
fn render<W: fmt::Write>(&self, w: &mut W) -> Result<()> {
|
fn render<W: fmt::Write>(&self, w: &mut W) -> Result<()> {
|
||||||
// Checks
|
// Checks
|
||||||
if !check::is_valid_tag_name(&self.name) {
|
if !check::is_valid_tag_name(&self.name) {
|
||||||
return Err(Error::new(ErrorCause::InvalidTagName(self.name.clone())));
|
return Err(Error::new(ErrorCause::InvalidTagName {
|
||||||
|
name: self.name.clone(),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
for name in self.attributes.keys() {
|
for name in self.attributes.keys() {
|
||||||
if !check::is_valid_attribute_name(name) {
|
if !check::is_valid_attribute_name(name) {
|
||||||
return Err(Error::new(ErrorCause::InvalidAttrName(name.clone())));
|
return Err(Error::new(ErrorCause::InvalidAttrName {
|
||||||
|
name: name.clone(),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,9 +201,9 @@ impl Render for Element {
|
||||||
Content::Text(text) if check::is_valid_raw_text(&self.name, text) => {
|
Content::Text(text) if check::is_valid_raw_text(&self.name, text) => {
|
||||||
write!(w, "{text}").map_err(|e| e.into())
|
write!(w, "{text}").map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
Content::Text(text) => {
|
Content::Text(text) => Err(Error::new(ErrorCause::InvalidRawText {
|
||||||
Err(Error::new(ErrorCause::InvalidRawText(text.clone())))
|
text: text.clone(),
|
||||||
}
|
})),
|
||||||
_ => Err(Error::new(ErrorCause::InvalidChild)),
|
_ => Err(Error::new(ErrorCause::InvalidChild)),
|
||||||
},
|
},
|
||||||
ElementKind::EscapableRawText => match child {
|
ElementKind::EscapableRawText => match child {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue