diff --git a/CHANGELOG.md b/CHANGELOG.md index e7799ce..5f9dfd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ A dependency update to an incompatible version is considered a breaking change. ## Releasing a new version -0. Ensure tests don't fail *ahem* +0. Ensure tests don't fail 1. Update dependencies in a separate commit, if necessary 2. Set new version number in [`Cargo.toml`](Cargo.toml) 3. Add new section in this changelog @@ -17,6 +17,46 @@ A dependency update to an incompatible version is considered a breaking change. ## Unreleased +## v0.2.0 - 2025-01-01 + +### Changed + +- **(breaking)** Updated `axum-core` dependency to `0.5.0` +- Relaxed lower bound on `http` dependency to `1.0.0` + +## v0.1.3 - 2024-12-21 + +### Added + +- `html::attr::Rel` + +### Fixed + +- Rendering of HTML comments + +## v0.1.2 - 2024-12-14 + +### Added + +- `Attr::set` +- `html::attr` + +### Deprecated + +- `Attr::new` in favor of `Attr::set` +- `Attr::id` in favor of `html::attr::id` +- `Attr::class` in favor of `html::attr::class` +- `Attr::style` in favor of `html::attr::style` +- `Attr::data` in favor of `html::attr::data_x` + +## v0.1.1 - 2024-12-08 + +### Added + +- `Element::into_document` +- `impl From<&String> for Content` +- Eponymous JS helper function in readme + ## v0.1.0 - 2024-12-02 Initial release diff --git a/Cargo.toml b/Cargo.toml index 20bd437..b976823 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "el" -version = "0.1.0" -edition = "2021" +version = "0.2.0" +edition = "2024" authors = ["Garmelon "] description = "Write and manipulate HTML elements as data" -repository = "https://github.com/Garmelon/el" +repository = "https://git.plugh.de/Garmelon/el" license = "MIT OR Apache-2.0" keywords = ["html", "svg", "mathml", "hiccup"] categories = ["web-programming", "template-engine"] @@ -13,16 +13,18 @@ categories = ["web-programming", "template-engine"] axum = ["dep:axum-core", "dep:http"] [dependencies] -axum-core = { version = "0.4.5", optional = true } -http = { version = "1.1.0", optional = true } +axum-core = { version = "0.5.0", optional = true } +http = { version = "1.0.0", optional = true } [lints] rust.unsafe_code = { level = "forbid", priority = 1 } # Lint groups -rust.deprecated_safe = "warn" -rust.future_incompatible = "warn" -rust.keyword_idents = "warn" -rust.rust_2018_idioms = "warn" +rust.deprecated-safe = "warn" +rust.future-incompatible = "warn" +rust.keyword-idents = "warn" +rust.nonstandard-style = "warn" +rust.refining-impl-trait = "warn" +rust.rust-2018-idioms = "warn" rust.unused = "warn" # Individual lints rust.let_underscore_drop = "warn" diff --git a/README.md b/README.md index 9e4e685..842db85 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,70 @@ # 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. +`el` is a no-dependencies 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 +## Show me a simple example ```rs -use el::{Attr, Render, html::*}; +use el::{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"), + attr::name("viewport"), + attr::content("width=device-width, initial-scale=1"), )), title("Example page"), )), body(( - h1((Attr::id("heading"), "Example page")), + h1((attr::id("heading"), "Example page")), p(("This is an example for a ", em("simple"), " web page.")), )), )) +.into_document() .render_to_string() .unwrap(); ``` +## What now? + +See the top-level crate documentation for more info. + +## But what about that small helper function? + +Here it is in full, for posterity: + +```js +function el(name, attributes, ...children) { + const element = document.createElement(name); + for (const [name, value] of Object.entries(attributes)) + element.setAttribute(name, value); + element.append(...children); + return element; +} +``` + +Use it like so: + +```js +const page = el("html", {}, + el("head", {}, + el("meta", { + name: "viewport", + content: "width=device-width, initial-scale=1", + }), + el("title", {}, "Example page"), + ), + el("body", {}, + el("h1", { id: "heading" }, "Example page"), + el("p", {}, "This is an example for a ", el("em", {}, "simple"), " web page."), + ), +); +``` + ## License This entire project is dual-licensed under the [Apache 2.0] and [MIT] licenses. diff --git a/src/axum.rs b/src/axum.rs index 44059bb..d0b7c64 100644 --- a/src/axum.rs +++ b/src/axum.rs @@ -1,5 +1,5 @@ use axum_core::response::IntoResponse; -use http::{header, HeaderValue, StatusCode}; +use http::{HeaderValue, StatusCode, header}; use crate::{Document, Render}; diff --git a/src/check.rs b/src/check.rs index c4f3387..01f3a59 100644 --- a/src/check.rs +++ b/src/check.rs @@ -58,10 +58,9 @@ pub fn is_valid_raw_text(tag_name: &str, text: &str) -> bool { // "[...] followed by characters that case-insensitively match the tag // name of the element [...]" // - // Note: Since we know that tag names are ascii-only, we can convert - // both to lowercase for a case-insensitive comparison without weird - // unicode shenanigans. - if potential_tag_name.to_ascii_lowercase() != tag_name.to_ascii_lowercase() { + // Note: Since we know that tag names are ascii-only, we can use an + // ASCII-based case insensitive comparison without unicode shenanigans. + if !potential_tag_name.eq_ignore_ascii_case(tag_name) { continue; } diff --git a/src/element.rs b/src/element.rs index 6e92ea9..b2b12e0 100644 --- a/src/element.rs +++ b/src/element.rs @@ -1,4 +1,4 @@ -use std::collections::{btree_map::Entry, BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, btree_map::Entry}; /// The kind of an element. /// @@ -87,6 +87,12 @@ impl Content { impl From for Content { fn from(value: String) -> Self { + Self::Text(value) + } +} + +impl From<&String> for Content { + fn from(value: &String) -> Self { Self::text(value) } } @@ -239,6 +245,14 @@ impl Element { self.add(c); self } + + /// Convert this element into a [`Document`]. + /// + /// This function is equivalent to calling `self.into()` but may be more + /// convenient in some cases. + pub fn into_document(self) -> Document { + self.into() + } } /// A component can add itself to an [`Element`] by modifying it. @@ -287,7 +301,7 @@ impl Attr { /// When this attribute is added to an [`Element`] through /// [`ElementComponent::add_to_element`] and an attribute of the same name /// already exists, it replaces that attribute's value. - pub fn new(name: impl ToString, value: impl ToString) -> Self { + pub fn set(name: impl ToString, value: impl ToString) -> Self { Self { name: name.to_string(), value: value.to_string(), @@ -295,6 +309,16 @@ impl Attr { } } + /// Create or replace an attribute. + /// + /// When this attribute is added to an [`Element`] through + /// [`ElementComponent::add_to_element`] and an attribute of the same name + /// already exists, it replaces that attribute's value. + #[deprecated = "use `Attr::set` instead"] + pub fn new(name: impl ToString, value: impl ToString) -> Self { + Self::set(name, value) + } + /// Create or append to an attribute. /// /// When this attribute is added to an [`Element`] through @@ -316,28 +340,31 @@ impl Attr { /// When rendering an empty attribute as HTML, the value can be omitted: /// `name=""` is equivalent to just `name`. pub fn yes(name: impl ToString) -> Self { - Self::new(name, "") + Self::set(name, "") } /// Create (or replace) an `id` attribute. /// /// `Attr::id(id)` is equivalent to `Attr::new("id", id)`. + #[deprecated = "use `html::attr::id` instead"] pub fn id(id: impl ToString) -> Self { - Self::new("id", id) + Self::set("id", id) } - /// Create (or append) to a `class` attribute. + /// Create (or append to) a `class` attribute. /// /// `Attr::class(class)` is equivalent to /// `Attr::append("class", class, " ")`. + #[deprecated = "use `html::attr::class` instead"] pub fn class(class: impl ToString) -> Self { Self::append("class", class, " ") } - /// Create (or append) to a `style` attribute. + /// Create (or append to) a `style` attribute. /// /// `Attr::style(style)` is equivalent to /// `Attr::append("style", style, ";")`. + #[deprecated = "use `html::attr::style` instead"] pub fn style(style: impl ToString) -> Self { Self::append("style", style, ";") } @@ -348,8 +375,9 @@ impl Attr { /// `Attr::new(format!("data-{name}"), value)`. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-* + #[deprecated = "use `html::attr::data_x` instead"] pub fn data(name: impl ToString, value: impl ToString) -> Self { - Self::new(format!("data-{}", name.to_string()), value) + Self::set(format!("data-{}", name.to_string()), value) } } @@ -379,7 +407,7 @@ impl ElementComponent for Attr { impl ElementComponent for HashMap { fn add_to_element(self, element: &mut Element) { for (name, value) in self { - Attr::new(name, value).add_to_element(element); + Attr::set(name, value).add_to_element(element); } } } @@ -387,7 +415,7 @@ impl ElementComponent for HashMap { impl ElementComponent for BTreeMap { fn add_to_element(self, element: &mut Element) { for (name, value) in self { - Attr::new(name, value).add_to_element(element); + Attr::set(name, value).add_to_element(element); } } } @@ -473,8 +501,12 @@ element_component_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11); element_component_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12); element_component_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13); element_component_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14); -element_component_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14, C15); -element_component_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14, C15, C16); +element_component_tuple!( + C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14, C15 +); +element_component_tuple!( + C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14, C15, C16 +); /// A full HTML document including doctype. /// diff --git a/src/html.rs b/src/html.rs index e5b8221..927eb14 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,5 +1,9 @@ -//! Definitions for all non-deprecated HTML elements +//! Definitions for HTML elements and attributes //! ([MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)). +//! +//! Deprecated HTML elements are not included. + +pub mod attr; use crate::{Element, ElementComponent, ElementKind}; diff --git a/src/html/attr.rs b/src/html/attr.rs new file mode 100644 index 0000000..7a6b71a --- /dev/null +++ b/src/html/attr.rs @@ -0,0 +1,1107 @@ +//! Definitions for common element attributes +//! (see [Attributes][0] and [Global attributes][1] on MDN). +//! +//! Deprecated or redundant attributes are not included. +//! +//! [0]: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes +//! [1]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes + +use std::fmt; + +use crate::{Attr, Element, ElementComponent}; + +macro_rules! url { + ( global, $name:expr ) => { + concat!( + "[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/", + $name, + ")" + ) + }; + ( normal, $name:expr ) => { + concat!( + "[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/", + $name, + ")" + ) + }; + ( element $element:expr, $name:expr ) => { + concat!( + "[`<", + $element, + ">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/", + $element, + "#", + $name, + ")" + ) + }; +} + +macro_rules! attr_yes { + ( + $name:ident as $article:ident $actual:expr; + at $url:expr; + ) => { + #[doc = concat!("Create (or replace) ", stringify!($article), " `", $actual, "` attribute")] + #[doc = concat!("(", $url, ").")] + pub fn $name() -> Attr { + Attr::yes($actual) + } + }; +} + +macro_rules! attr_set { + ( + $name:ident as $article:ident $actual:expr; + at $url:expr; + ) => { + #[doc = concat!("Create (or replace) ", stringify!($article), " `", $actual, "` attribute")] + #[doc = concat!("(", $url, ").")] + pub fn $name(value: impl ToString) -> Attr { + Attr::set($actual, value) + } + }; +} + +macro_rules! attr_append { + ( + $name:ident as $article:ident $actual:expr, separated by $separator:expr; + at $url:expr; + ) => { + #[doc = concat!("Create (or append to) ", stringify!($article), " `", $actual, "` attribute")] + #[doc = concat!("(", $url, ").")] + pub fn $name(value: impl ToString) -> Attr { + Attr::append($actual, value, $separator) + } + }; +} + +macro_rules! attr_enum { + ( + $name:ident as $article:ident $actual:expr; + at $url:expr; + $( $valname:ident => $valstr:expr, )* + ) => { + #[doc = concat!("Create (or replace) ", stringify!($article), " `", $actual, "` attribute")] + #[doc = concat!("(", $url, ").")] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum $name { + $( + #[doc = concat!("The value `", stringify!($valstr), "`.")] + $valname, + )* + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $( Self::$valname => $valstr.fmt(f), )* + } + } + } + + impl ElementComponent for $name { + fn add_to_element(self, element: &mut Element) { + Attr::set($actual, self).add_to_element(element); + } + } + }; + ( + $name:ident as $article:ident $actual:expr, separated by $separator:expr; + at $url:expr; + $( $valname:ident => $valstr:expr, )* + ) => { + #[doc = concat!("Create (or append to) ", stringify!($article), " `", $actual, "` attribute")] + #[doc = concat!("(", $url, ").")] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum $name { + $( + #[doc = concat!("The value `", stringify!($valstr), "`.")] + $valname, + )* + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $( Self::$valname => $valstr.fmt(f), )* + } + } + } + + impl ElementComponent for $name { + fn add_to_element(self, element: &mut Element) { + Attr::append($actual, self, $separator).add_to_element(element); + } + } + }; +} + +//////////////// +// Attributes // +//////////////// + +attr_append! { + accept as an "accept", separated by ", "; + at url!(normal, "accept"); +} + +attr_append! { + accesskey as an "accesskey", separated by " "; + at url!(global, "accesskey"); +} + +attr_set! { + action as an "action"; + at url!(element "form", "action"); +} + +attr_append! { + allow as an "allow", separated by "; "; + at url!(element "iframe", "allow"); +} + +attr_set! { + alt as an "alt"; + at concat!( + url!(element "area", "alt"), ", ", + url!(element "img", "alt"), ", ", + url!(element "input", "alt") + ); +} + +attr_enum! { + As as an "as"; + at url!(element "link", "as"); + Audio => "audio", + Document => "document", + Embed => "embed", + Fetch => "fetch", + Font => "font", + Image => "image", + Object => "object", + Script => "script", + Style => "style", + Track => "track", + Video => "video", + Worker => "worker", +} + +attr_yes! { + r#async as an "async"; + at url!(element "script", "async"); +} + +attr_enum! { + Autocapitalize as an "autocapitalize"; + at url!(global, "autocapitalize"); + None => "none", + Sentences => "sentences", + Words => "words", + Characters => "characters", +} + +attr_append! { + autocomplete as an "autocomplete", separated by " "; + at url!(normal, "autocomplete"); +} + +attr_yes! { + autofocus as an "autofocus"; + at url!(global, "autofocus"); +} + +attr_yes! { + autoplay as an "autoplay"; + at concat!( + url!(element "audio", "autoplay"), ", ", + url!(element "video", "autoplay") + ); +} + +attr_enum! { + Capture as a "capture"; + at url!(normal, "capture"); + User => "user", + Environment => "environment", +} + +attr_yes! { + checked as a "checked"; + at url!(element "input", "checked"); +} + +attr_set! { + cite as a "cite"; + at concat!( + url!(element "blockquote", "cite"), ", ", + url!(element "del", "cite"), ", ", + url!(element "ins", "cite"), ", ", + url!(element "q", "cite") + ); +} + +attr_append! { + class as a "class", separated by " "; + at url!(global, "class"); +} + +attr_set! { + cols as a "cols"; + at url!(element "textarea", "cols"); +} + +attr_set! { + colspan as a "colspan"; + at concat!( + url!(element "td", "colspan"), ", ", + url!(element "th", "colspan") + ); +} + +attr_set! { + content as a "content"; + at url!(element "meta", "content"); +} + +attr_enum! { + Contenteditable as a "contenteditable"; + at url!(global, "contenteditable"); + True => "", + False => "false", + PlaintextOnly => "plaintext-only", +} + +attr_yes! { + controls as a "controls"; + at concat!( + url!(element "audio", "controls"), ", ", + url!(element "video", "controls") + ); +} + +attr_set! { + coords as a "coords"; + at url!(element "area", "coords"); +} + +attr_enum! { + Crossorigin as a "crossorigin"; + at url!(normal, "crossorigin"); + Anonymous => "anonymous", + UseCredentials => "use-credentials", +} + +attr_set! { + data as a "data"; + at url!(element "object", "data"); +} + +/// Create (or replace) a `data-*` attribute +/// ([MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*)). +pub fn data_x(name: impl ToString, value: impl ToString) -> Attr { + Attr::set(format!("data-{}", name.to_string()), value) +} + +attr_set! { + datetime as a "datetime"; + at concat!( + url!(element "del", "datetime"), ", ", + url!(element "ins", "datetime"), ", ", + url!(element "time", "datetime") + ); +} + +attr_enum! { + Decoding as a "decoding"; + at url!(element "img", "decoding"); + Sync => "sync", + Async => "async", + Auto => "auto", +} + +attr_yes! { + default as a "default"; + at url!(element "track", "default"); +} + +attr_yes! { + defer as a "defer"; + at url!(element "script", "defer"); +} + +attr_enum! { + Dir as a "dir"; + at url!(global, "dir"); + Ltr => "ltr", + Rtl => "rtl", + Auto => "auto", +} + +attr_set! { + dirname as a "dirname"; + at url!(normal, "dirname"); +} + +attr_yes! { + disabled as a "disabled"; + at url!(normal, "disabled"); +} + +attr_set! { + download as a "download"; + at concat!( + url!(element "a", "download"), ", ", + url!(element "area", "download") + ); +} + +attr_enum! { + Draggable as a "draggable"; + at url!(global, "draggable"); + True => "true", + False => "false", +} + +attr_enum! { + Enctype as an "enctype"; + at url!(element "form", "enctype"); + Form => "application/x-www-form-urlencoded", + Multipart => "multipart/form-data", + Plain => "text/plain", +} + +attr_enum! { + Enterkeyhint as an "enterkeyhint"; + at url!(global, "enterkeyhint"); + Enter => "enter", + Done => "done", + Go => "go", + Next => "next", + Previous => "previous", + Search => "search", + Send => "send", +} + +attr_append! { + exportparts as an "exportparts", separated by ", "; + at url!(global, "exportparts"); +} + +attr_set! { + r#for as a "for"; + at url!(normal, "for"); +} + +attr_set! { + form as a "form"; + at concat!( + url!(element "button", "form"), ", ", + url!(element "fieldset", "form"), ", ", + url!(element "input", "form"), ", ", + url!(element "label", "form"), ", ", + url!(element "meter", "form"), ", ", + url!(element "object", "form"), ", ", + url!(element "output", "form"), ", ", + url!(element "progress", "form"), ", ", + url!(element "select", "form"), ", ", + url!(element "textarea", "form") + ); +} + +attr_set! { + formaction as a "formaction"; + at concat!( + url!(element "button", "formaction"), ", ", + url!(element "input", "formaction") + ); +} + +attr_enum! { + Formenctype as a "formenctype"; + at concat!( + url!(element "button", "formenctype"), ", ", + url!(element "input", "formenctype") + ); + Form => "application/x-www-form-urlencoded", + Multipart => "multipart/form-data", + Plain => "text/plain", +} + +attr_enum! { + Formmethod as a "formmethod"; + at concat!( + url!(element "button", "formmethod"), ", ", + url!(element "input", "formmethod") + ); + Post => "post", + Get => "get", + Dialog => "dialog", +} + +attr_yes! { + formnovalidate as a "formnovalidate"; + at concat!( + url!(element "button", "formnovalidate"), ", ", + url!(element "input", "formnovalidate") + ); +} + +attr_enum! { + Formtarget as a "formtarget"; + at concat!( + url!(element "button", "formtarget"), ", ", + url!(element "input", "formtarget") + ); + Self_ => "_self", + Blank => "_blank", + Parent => "_parent", + Top => "_top", +} + +attr_append! { + headers as a "headers", separated by " "; + at concat!( + url!(element "td", "headers"), ", ", + url!(element "th", "headers") + ); +} + +attr_set! { + height as a "height"; + at concat!( + url!(element "canvas", "height"), ", ", + url!(element "embed", "height"), ", ", + url!(element "iframe", "height"), ", ", + url!(element "img", "height"), ", ", + url!(element "input", "height"), ", ", + url!(element "object", "height"), ", ", + url!(element "video", "height") + ); +} + +attr_enum! { + Hidden as a "hidden"; + at url!(global, "hidden"); + Yes => "", + UntilFound => "until-found", +} + +attr_set! { + high as a "high"; + at url!(element "meter", "high"); +} + +attr_set! { + href as an "href"; + at concat!( + url!(element "a", "href"), ", ", + url!(element "area", "href"), ", ", + url!(element "base", "href"), ", ", + url!(element "link", "href") + ); +} + +attr_set! { + hreflang as an "hreflang"; + at concat!( + url!(element "a", "hreflang"), ", ", + url!(element "link", "hreflang") + ); +} + +attr_enum! { + HttpEquiv as an "http-equiv"; + at url!(element "meta", "http-equiv"); + ContentSecurityPolicy => "content-security-policy", + ContentType => "content-type", + DefaultStyle => "default-style", + XUaCompatible => "x-ua-compatible", + Refresh => "refresh", +} + +attr_set! { + id as an "id"; + at url!(global, "id"); +} + +attr_yes! { + inert as an "inert"; + at url!(global, "inert"); +} + +attr_set! { + integrity as an "integrity"; + at concat!( + url!(element "link", "integrity"), ", ", + url!(element "script", "integrity") + ); +} + +attr_enum! { + Inputmode as an "inputmode"; + at url!(global, "inputmode"); + None => "none", + Text => "text", + Decimal => "decimal", + Numeric => "numeric", + Tel => "tel", + Search => "search", + Email => "email", + Url => "url", +} + +attr_set! { + is as an "is"; + at url!(global, "is"); +} + +attr_yes! { + ismap as an "ismap"; + at url!(element "img", "ismap"); +} + +attr_set! { + itemid as an "itemid"; + at url!(global, "itemid"); +} + +attr_set! { + itemprop as an "itemprop"; + at url!(global, "itemprop"); +} + +attr_set! { + itemref as an "itemref"; + at url!(global, "itemref"); +} + +attr_yes! { + itemscope as an "itemscope"; + at url!(global, "itemscope"); +} + +attr_set! { + itemtype as an "itemtype"; + at url!(global, "itemtype"); +} + +attr_enum! { + Kind as a "kind"; + at url!(element "track", "kind"); + Subtitles => "subtitles", + Captions => "captions", + Chapters => "chapters", + Metadata => "metadata", +} + +attr_set! { + lang as a "lang"; + at url!(global, "lang"); +} + +attr_enum! { + Loading as a "loading"; + at concat!( + url!(element "img", "loading"), ", ", + url!(element "iframe", "loading") + ); + Eager => "eager", + Lazy => "lazy", +} + +attr_set! { + list as a "list"; + at url!(element "input", "list"); +} + +attr_yes! { + r#loop as a "loop"; + at concat!( + url!(element "audio", "loop"), ", ", + url!(element "video", "loop") + ); +} + +attr_set! { + low as a "low"; + at url!(element "meter", "low"); +} + +attr_set! { + max as a "max"; + at url!(normal, "max"); +} + +attr_set! { + maxlength as a "maxlength"; + at url!(normal, "maxlength"); +} + +attr_set! { + minlength as a "minlength"; + at url!(normal, "minlength"); +} + +attr_enum! { + Method as a "method"; + at url!(element "form", "method"); + Post => "post", + Get => "get", + Dialog => "dialog", +} + +attr_set! { + min as a "min"; + at url!(normal, "min"); +} + +attr_yes! { + multiple as a "multiple"; + at url!(normal, "multiple"); +} + +attr_yes! { + muted as a "muted"; + at concat!( + url!(element "audio", "muted"), ", ", + url!(element "video", "muted") + ); +} + +attr_set! { + name as a "name"; + at concat!( + url!(element "button", "name"), ", ", + url!(element "form", "name"), ", ", + url!(element "fieldset", "name"), ", ", + url!(element "iframe", "name"), ", ", + url!(element "input", "name"), ", ", + url!(element "object", "name"), ", ", + url!(element "output", "name"), ", ", + url!(element "select", "name"), ", ", + url!(element "textarea", "name"), ", ", + url!(element "map", "name"), ", ", + url!(element "meta", "name") + ); +} + +attr_set! { + nonce as a "nonce"; + at url!(global, "nonce"); +} + +attr_yes! { + novalidate as a "novalidate"; + at url!(element "form", "novalidate"); +} + +attr_yes! { + open as an "open"; + at concat!( + url!(element "details", "open"), ", ", + url!(element "dialog", "open") + ); +} + +attr_set! { + optimum as an "optimum"; + at url!(element "meter", "optimum"); +} + +attr_append! { + part as a "part", separated by " "; + at url!(global, "part"); +} + +attr_set! { + pattern as a "pattern"; + at url!(normal, "pattern"); +} + +attr_append! { + ping as a "ping", separated by " "; + at concat!( + url!(element "a", "ping"), ", ", + url!(element "area", "ping") + ); +} + +attr_set! { + placeholder as a "placeholder"; + at url!(normal, "placeholder"); +} + +attr_yes! { + playsinline as a "playsinline"; + at url!(element "video", "playsinline"); +} + +attr_enum! { + Popover as a "popover"; + at url!(global, "popover"); + Auto => "", + Manual => "manual", +} + +attr_set! { + poster as a "poster"; + at url!(element "video", "poster"); +} + +attr_enum! { + Preload as a "preload"; + at concat!( + url!(element "audio", "preload"), ", ", + url!(element "video", "preload") + ); + None => "none", + Metadata => "metadata", + Auto => "auto", +} + +attr_yes! { + readonly as a "readonly"; + at url!(normal, "readonly"); +} + +attr_enum! { + Referrerpolicy as a "referrerpolicy"; + at concat!( + url!(element "a", "referrerpolicy"), ", ", + url!(element "area", "referrerpolicy"), ", ", + url!(element "iframe", "referrerpolicy"), ", ", + url!(element "img", "referrerpolicy"), ", ", + url!(element "link", "referrerpolicy"), ", ", + url!(element "script", "referrerpolicy"), ", ", + ); + NoReferrer => "no-referrer", + NoReferrerWhenDowngrade => "no-referrer-when-downgrade", + Origin => "origin", + OriginWhenCrossOrigin => "origin-when-cross-origin", + SameOrigin => "same-origin", + StrictOrigin => "strict-origin", + StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin", + UnsafeUrl => "unsafe-url", +} + +attr_enum! { + Rel as a "rel", separated by " "; + at url!(normal, "rel"); + Alternate => "alternate", + Author => "author", + Bookmark => "bookmark", + Canonical => "canonical", + DnsPrefetch => "dns-prefetch", + External => "external", + Expect => "expect", + Help => "help", + Icon => "icon", + License => "license", + Manifest => "manifest", + Me => "me", + Modulepreload => "modulepreload", + Next => "next", + Nofollow => "nofollow", + Noopener => "noopener", + Noreferrer => "noreferrer", + Opener => "opener", + Pingback => "pingback", + Preconnect => "preconnect", + Prefetch => "prefetch", + Preload => "preload", + Prerender => "prerender", + Prev => "prev", + PrivacyPolicy => "privacy-policy", + Search => "search", + Stylesheet => "stylesheet", + Tag => "tag", + TermsOfService => "terms-of-service", +} + +attr_append! { + rel as a "rel", separated by " "; + at url!(normal, "rel"); +} + +attr_yes! { + required as a "required"; + at url!(normal, "required"); +} + +attr_yes! { + reversed as a "reversed"; + at url!(element "ol", "reversed"); +} + +attr_set! { + rows as a "rows"; + at url!(element "textarea", "rows"); +} + +attr_set! { + rowspan as a "rowspan"; + at concat!( + url!(element "td", "rowspan"), ", ", + url!(element "th", "rowspan") + ); +} + +attr_append! { + sandbox as a "sandbox", separated by " "; + at url!(element "iframe", "sandbox"); +} + +attr_enum! { + Scope as a "scope"; + at url!(element "th", "scope"); + Row => "row", + Col => "col", + Rowgroup => "rowgroup", + Colgroup => "colgroup", +} + +attr_yes! { + selected as a "selected"; + at url!(element "option", "selected"); +} + +attr_enum! { + Shape as a "shape"; + at url!(element "area", "shape"); + Rect => "rect", + Circle => "circle", + Poly => "poly", + Default => "default", +} + +attr_set! { + size as a "size"; + at url!(normal, "size"); +} + +// The "sizes" attribute for is whitespace-separated while the "sizes" +// attribute for and is comma-separated. The naming here assumes +// that you usually want to set "sizes" on an and not a . + +attr_append! { + sizes as a "sizes", separated by ", "; + at concat!( + url!(element "img", "sizes"), ", ", + url!(element "source", "sizes") + ); +} + +attr_append! { + sizes_link as a "sizes", separated by " "; + at url!(element "link", "sizes"); +} + +attr_set! { + slot as a "slot"; + at url!(global, "slot"); +} + +attr_set! { + span as a "span"; + at concat!( + url!(element "col", "span"), ", ", + url!(element "colgroup", "span") + ); +} + +attr_enum! { + Spellcheck as a "spellcheck"; + at url!(global, "spellcheck"); + True => "", + False => "false", +} + +attr_set! { + src as a "src"; + at concat!( + url!(element "audio", "src"), ", ", + url!(element "embed", "src"), ", ", + url!(element "iframe", "src"), ", ", + url!(element "img", "src"), ", ", + url!(element "input", "src"), ", ", + url!(element "script", "src"), ", ", + url!(element "source", "src"), ", ", + url!(element "track", "src"), ", ", + url!(element "video", "src") + ); +} + +attr_set! { + srcdoc as a "srcdoc"; + at url!(element "iframe", "srcdoc"); +} + +attr_set! { + srclang as a "srclang"; + at url!(element "track", "srclang"); +} + +attr_append! { + srcset as a "srcset", separated by ", "; + at concat!( + url!(element "img", "srcset"), ", ", + url!(element "source", "srcset") + ); +} + +attr_set! { + start as a "start"; + at url!(element "ol", "start"); +} + +attr_set! { + step as a "step"; + at url!(normal, "step"); +} + +attr_append! { + style as a "style", separated by "; "; + at url!(global, "style"); +} + +attr_set! { + tabindex as a "tabindex"; + at url!(global, "tabindex"); +} + +attr_enum! { + Target as a "target"; + at concat!( + url!(element "a", "target"), ", ", + url!(element "area", "target"), ", ", + url!(element "base", "target"), ", ", + url!(element "form", "target") + ); + Self_ => "_self", + Blank => "_blank", + Parent => "_parent", + Top => "_top", + UnfencedTop => "_unfencedTop", +} + +attr_set! { + title as a "title"; + at url!(global, "title"); +} + +attr_enum! { + Translate as a "translate"; + at url!(global, "translate"); + Yes => "", + No => "no", +} + +attr_set! { + r#type as a "type"; + at concat!( + url!(element "embed", "type"), ", ", + url!(element "object", "type"), ", ", + url!(element "source", "type"), ", ", + url!(element "link", "type") + ); +} + +attr_enum! { + TypeButton as a "type"; + at url!(element "button", "type"); + Submit => "submit", + Reset => "reset", + Button => "button", +} + +attr_enum! { + TypeInput as a "type"; + at url!(element "input", "type"); + Button => "button", + Checkbox => "checkbox", + Color => "color", + Date => "date", + DatetimeLocal => "datetime-local", + Email => "email", + File => "file", + Hidden => "hidden", + Image => "image", + Month => "month", + Number => "number", + Password => "password", + Radio => "radio", + Range => "range", + Reset => "reset", + Search => "search", + Submit => "submit", + Tel => "tel", + Text => "text", + Time => "time", + Url => "url", + Week => "week", +} + +attr_enum! { + TypeOl as a "type"; + at url!(element "ol", "type"); + LowercaseAlphabetic => "a", + UppercaseAlphabetic => "A", + LowercaseRoman => "i", + UppercaseRoman => "I", + Numbers => "1", +} + +attr_enum! { + TypeScript as a "type"; + at url!(element "script", "type"); + Classic => "", + Importmap => "importmap", + Module => "module", +} + +attr_set! { + usemap as a "usemap"; + at url!(element "img", "usemap"); +} + +attr_set! { + value as a "value"; + at concat!( + url!(element "button", "value"), ", ", + url!(element "data", "value"), ", ", + url!(element "input", "value"), ", ", + url!(element "li", "value"), ", ", + url!(element "meter", "value"), ", ", + url!(element "option", "value"), ", ", + url!(element "progress", "value") + ); +} + +attr_set! { + width as a "width"; + at concat!( + url!(element "canvas", "width"), ", ", + url!(element "embed", "width"), ", ", + url!(element "iframe", "width"), ", ", + url!(element "img", "width"), ", ", + url!(element "input", "width"), ", ", + url!(element "object", "width"), ", ", + url!(element "video", "width") + ); +} + +attr_enum! { + Wrap as a "wrap"; + at url!(element "textarea", "wrap"); + Hard => "hard", + Soft => "soft", +} + +attr_enum! { + WritingSuggestions as a "writingsuggestions"; + at url!(global, "writingsuggestions"); + True => "", + False => "false", +} diff --git a/src/lib.rs b/src/lib.rs index d3d1826..bf907f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,22 +33,22 @@ //! ## Usage example //! //! ``` -//! use el::{Attr, Render, html::*}; +//! use el::{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"), +//! attr::name("viewport"), +//! attr::content("width=device-width, initial-scale=1"), //! )), //! title("Example page"), //! )), //! body(( -//! h1((Attr::id("heading"), "Example page")), +//! h1((attr::id("heading"), "Example page")), //! p(("This is an example for a ", em("simple"), " web page.")), //! )), //! )) +//! .into_document() //! .render_to_string() //! .unwrap(); //! ``` @@ -66,6 +66,10 @@ //! ``` //! //! [axum]: https://crates.io/crates/axum +//! +//! ## But what about that small helper function? +//! +//! See the readme for more details. #[cfg(feature = "axum")] mod axum; @@ -80,21 +84,20 @@ pub use self::{element::*, render::*}; #[cfg(test)] mod tests { - use crate::{html::*, Attr, Content, Element, Render}; + use crate::{Attr, Content, Element, Render, html::*}; #[test] fn simple_website() { - let els = [ - Content::doctype(), - html(( - head(title("Hello")), - body((h1("Hello"), p(("Hello ", em("world"), "!")))), - )) - .into(), - ]; + let page = html(( + head(title("Hello")), + body((h1("Hello"), p(("Hello ", em("world"), "!")))), + )) + .into_document() + .render_to_string() + .unwrap(); assert_eq!( - els.render_to_string().unwrap(), + page, concat!( "", "Hello", @@ -127,9 +130,11 @@ mod tests { assert!(script("hello world").render_to_string().is_err()); - assert!(script("hello "#, ); + + assert_eq!( + p(( + attr::id("foo"), + attr::id("bar"), + attr::class("foo"), + attr::class("bar"), + )) + .render_to_string() + .unwrap(), + r#"

"#, + ) } #[test] fn always_lowercase() { assert_eq!( Element::normal("HTML") - .with(Attr::new("LANG", "EN")) + .with(Attr::set("LANG", "EN")) .render_to_string() .unwrap(), r#""#, ); } + + #[test] + fn comments() { + assert_eq!( + html((" ", Content::comment("abc"))) + .render_to_string() + .unwrap(), + r#"<!--abc--> "#, + ); + + assert_eq!( + html(Content::comment("Hello !")) + .render_to_string() + .unwrap(), + r#""#, + ); + + assert_eq!( + html(Content::comment("->"#, + ); + } } diff --git a/src/render.rs b/src/render.rs index 52770b5..1109626 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,9 +1,8 @@ use std::{error, fmt}; use crate::{ - check, + Document, check, element::{Content, Element, ElementKind}, - Document, }; /// The cause of an [`Error`]. @@ -246,6 +245,8 @@ fn render_text(w: &mut W, text: &str) -> Result<()> { } fn render_comment(w: &mut W, text: &str) -> Result<()> { + write!(w, "")?; Ok(()) }