Compare commits

..

No commits in common. "master" and "v0.1.1" have entirely different histories.

10 changed files with 53 additions and 1250 deletions

View file

@ -17,38 +17,6 @@ A dependency update to an incompatible version is considered a breaking change.
## Unreleased ## 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 ## v0.1.1 - 2024-12-08
### Added ### Added

View file

@ -1,10 +1,10 @@
[package] [package]
name = "el" name = "el"
version = "0.2.0" version = "0.1.1"
edition = "2024" edition = "2021"
authors = ["Garmelon <garmelon@plugh.de>"] authors = ["Garmelon <garmelon@plugh.de>"]
description = "Write and manipulate HTML elements as data" description = "Write and manipulate HTML elements as data"
repository = "https://git.plugh.de/Garmelon/el" repository = "https://github.com/Garmelon/el"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["html", "svg", "mathml", "hiccup"] keywords = ["html", "svg", "mathml", "hiccup"]
categories = ["web-programming", "template-engine"] categories = ["web-programming", "template-engine"]
@ -13,18 +13,16 @@ categories = ["web-programming", "template-engine"]
axum = ["dep:axum-core", "dep:http"] axum = ["dep:axum-core", "dep:http"]
[dependencies] [dependencies]
axum-core = { version = "0.5.0", optional = true } axum-core = { version = "0.4.5", optional = true }
http = { version = "1.0.0", optional = true } http = { version = "1.1.0", optional = true }
[lints] [lints]
rust.unsafe_code = { level = "forbid", priority = 1 } rust.unsafe_code = { level = "forbid", priority = 1 }
# Lint groups # Lint groups
rust.deprecated-safe = "warn" rust.deprecated_safe = "warn"
rust.future-incompatible = "warn" rust.future_incompatible = "warn"
rust.keyword-idents = "warn" rust.keyword_idents = "warn"
rust.nonstandard-style = "warn" rust.rust_2018_idioms = "warn"
rust.refining-impl-trait = "warn"
rust.rust-2018-idioms = "warn"
rust.unused = "warn" rust.unused = "warn"
# Individual lints # Individual lints
rust.let_underscore_drop = "warn" rust.let_underscore_drop = "warn"

View file

@ -9,18 +9,19 @@ and named after a small helper function I once wrote in JS.
## Show me a simple example ## Show me a simple example
```rs ```rs
use el::{Render, html::*}; use el::{Attr, Render, html::*};
let page: String = html(( let page: String = html((
head(( head((
meta(Attr::new("charset", "utf-8")),
meta(( meta((
attr::name("viewport"), Attr::new("name", "viewport"),
attr::content("width=device-width, initial-scale=1"), Attr::new("content", "width=device-width, initial-scale=1"),
)), )),
title("Example page"), title("Example page"),
)), )),
body(( body((
h1((attr::id("heading"), "Example page")), h1((Attr::id("heading"), "Example page")),
p(("This is an example for a ", em("simple"), " web page.")), p(("This is an example for a ", em("simple"), " web page.")),
)), )),
)) ))
@ -35,7 +36,7 @@ See the top-level crate documentation for more info.
## But what about that small helper function? ## But what about that small helper function?
Here it is in full, for posterity: Here it is in full, for posteriority:
```js ```js
function el(name, attributes, ...children) { function el(name, attributes, ...children) {
@ -52,11 +53,12 @@ Use it like so:
```js ```js
const page = el("html", {}, const page = el("html", {},
el("head", {}, el("head", {},
el("meta", { charset: "utf-8" }),
el("meta", { el("meta", {
name: "viewport", name: "viewport",
content: "width=device-width, initial-scale=1", content: "width=device-width, initial-scale=1",
}), }),
el("title", {}, "Example page"), el("title", {}, "Example page")
), ),
el("body", {}, el("body", {},
el("h1", { id: "heading" }, "Example page"), el("h1", { id: "heading" }, "Example page"),

View file

@ -1,5 +1,5 @@
use axum_core::response::IntoResponse; use axum_core::response::IntoResponse;
use http::{HeaderValue, StatusCode, header}; use http::{header, HeaderValue, StatusCode};
use crate::{Document, Render}; use crate::{Document, Render};

View file

@ -58,9 +58,10 @@ pub fn is_valid_raw_text(tag_name: &str, text: &str) -> bool {
// "[...] followed by characters that case-insensitively match the tag // "[...] followed by characters that case-insensitively match the tag
// name of the element [...]" // name of the element [...]"
// //
// Note: Since we know that tag names are ascii-only, we can use an // Note: Since we know that tag names are ascii-only, we can convert
// ASCII-based case insensitive comparison without unicode shenanigans. // both to lowercase for a case-insensitive comparison without weird
if !potential_tag_name.eq_ignore_ascii_case(tag_name) { // unicode shenanigans.
if potential_tag_name.to_ascii_lowercase() != tag_name.to_ascii_lowercase() {
continue; continue;
} }

View file

@ -1,4 +1,4 @@
use std::collections::{BTreeMap, HashMap, btree_map::Entry}; use std::collections::{btree_map::Entry, BTreeMap, HashMap};
/// The kind of an element. /// The kind of an element.
/// ///
@ -301,7 +301,7 @@ impl Attr {
/// When this attribute is added to an [`Element`] through /// When this attribute is added to an [`Element`] through
/// [`ElementComponent::add_to_element`] and an attribute of the same name /// [`ElementComponent::add_to_element`] and an attribute of the same name
/// already exists, it replaces that attribute's value. /// already exists, it replaces that attribute's value.
pub fn set(name: impl ToString, value: impl ToString) -> Self { pub fn new(name: impl ToString, value: impl ToString) -> Self {
Self { Self {
name: name.to_string(), name: name.to_string(),
value: value.to_string(), value: value.to_string(),
@ -309,16 +309,6 @@ 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. /// Create or append to an attribute.
/// ///
/// When this attribute is added to an [`Element`] through /// When this attribute is added to an [`Element`] through
@ -340,31 +330,28 @@ impl Attr {
/// When rendering an empty attribute as HTML, the value can be omitted: /// When rendering an empty attribute as HTML, the value can be omitted:
/// `name=""` is equivalent to just `name`. /// `name=""` is equivalent to just `name`.
pub fn yes(name: impl ToString) -> Self { pub fn yes(name: impl ToString) -> Self {
Self::set(name, "") Self::new(name, "")
} }
/// Create (or replace) an `id` attribute. /// Create (or replace) an `id` attribute.
/// ///
/// `Attr::id(id)` is equivalent to `Attr::new("id", id)`. /// `Attr::id(id)` is equivalent to `Attr::new("id", id)`.
#[deprecated = "use `html::attr::id` instead"]
pub fn id(id: impl ToString) -> Self { pub fn id(id: impl ToString) -> Self {
Self::set("id", id) Self::new("id", id)
} }
/// Create (or append to) a `class` attribute. /// Create (or append) to a `class` attribute.
/// ///
/// `Attr::class(class)` is equivalent to /// `Attr::class(class)` is equivalent to
/// `Attr::append("class", class, " ")`. /// `Attr::append("class", class, " ")`.
#[deprecated = "use `html::attr::class` instead"]
pub fn class(class: impl ToString) -> Self { pub fn class(class: impl ToString) -> Self {
Self::append("class", class, " ") 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::style(style)` is equivalent to
/// `Attr::append("style", style, ";")`. /// `Attr::append("style", style, ";")`.
#[deprecated = "use `html::attr::style` instead"]
pub fn style(style: impl ToString) -> Self { pub fn style(style: impl ToString) -> Self {
Self::append("style", style, ";") Self::append("style", style, ";")
} }
@ -375,9 +362,8 @@ impl Attr {
/// `Attr::new(format!("data-{name}"), value)`. /// `Attr::new(format!("data-{name}"), value)`.
/// ///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-* /// [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 { pub fn data(name: impl ToString, value: impl ToString) -> Self {
Self::set(format!("data-{}", name.to_string()), value) Self::new(format!("data-{}", name.to_string()), value)
} }
} }
@ -407,7 +393,7 @@ impl ElementComponent for Attr {
impl ElementComponent for HashMap<String, String> { impl ElementComponent for HashMap<String, String> {
fn add_to_element(self, element: &mut Element) { fn add_to_element(self, element: &mut Element) {
for (name, value) in self { for (name, value) in self {
Attr::set(name, value).add_to_element(element); Attr::new(name, value).add_to_element(element);
} }
} }
} }
@ -415,7 +401,7 @@ impl ElementComponent for HashMap<String, String> {
impl ElementComponent for BTreeMap<String, String> { impl ElementComponent for BTreeMap<String, String> {
fn add_to_element(self, element: &mut Element) { fn add_to_element(self, element: &mut Element) {
for (name, value) in self { for (name, value) in self {
Attr::set(name, value).add_to_element(element); Attr::new(name, value).add_to_element(element);
} }
} }
} }
@ -501,12 +487,8 @@ 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);
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);
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);
element_component_tuple!( element_component_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14, C15);
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, C16
);
/// A full HTML document including doctype. /// A full HTML document including doctype.
/// ///

View file

@ -1,9 +1,5 @@
//! Definitions for HTML elements and attributes //! Definitions for all non-deprecated HTML elements
//! ([MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)). //! ([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}; use crate::{Element, ElementComponent, ElementKind};

File diff suppressed because it is too large Load diff

View file

@ -33,18 +33,19 @@
//! ## Usage example //! ## Usage example
//! //!
//! ``` //! ```
//! use el::{Render, html::*}; //! use el::{Attr, Render, html::*};
//! //!
//! let page: String = html(( //! let page: String = html((
//! head(( //! head((
//! meta(Attr::new("charset", "utf-8")),
//! meta(( //! meta((
//! attr::name("viewport"), //! Attr::new("name", "viewport"),
//! attr::content("width=device-width, initial-scale=1"), //! Attr::new("content", "width=device-width, initial-scale=1"),
//! )), //! )),
//! title("Example page"), //! title("Example page"),
//! )), //! )),
//! body(( //! body((
//! h1((attr::id("heading"), "Example page")), //! h1((Attr::id("heading"), "Example page")),
//! p(("This is an example for a ", em("simple"), " web page.")), //! p(("This is an example for a ", em("simple"), " web page.")),
//! )), //! )),
//! )) //! ))
@ -84,7 +85,7 @@ pub use self::{element::*, render::*};
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{Attr, Content, Element, Render, html::*}; use crate::{html::*, Attr, Element, Render};
#[test] #[test]
fn simple_website() { fn simple_website() {
@ -130,11 +131,9 @@ mod tests {
assert!(script("hello </script> world").render_to_string().is_err()); assert!(script("hello </script> world").render_to_string().is_err());
assert!( assert!(script("hello </ScRiPt ... world")
script("hello </ScRiPt ... world")
.render_to_string() .render_to_string()
.is_err() .is_err());
);
} }
#[test] #[test]
@ -151,10 +150,10 @@ mod tests {
fn attributes() { fn attributes() {
assert_eq!( assert_eq!(
input(( input((
Attr::set("name", "tentacles"), Attr::new("name", "tentacles"),
attr::TypeInput::Number, Attr::new("type", "number"),
attr::min(10), Attr::new("min", 10),
Attr::append("max", 100, "FOOBAA"), Attr::new("max", 100),
)) ))
.render_to_string() .render_to_string()
.unwrap(), .unwrap(),
@ -162,55 +161,21 @@ mod tests {
); );
assert_eq!( assert_eq!(
input((Attr::set("name", "horns"), Attr::yes("checked"))) input((Attr::new("name", "horns"), Attr::yes("checked")))
.render_to_string() .render_to_string()
.unwrap(), .unwrap(),
r#"<input checked name="horns">"#, r#"<input checked name="horns">"#,
); );
assert_eq!(
p((
attr::id("foo"),
attr::id("bar"),
attr::class("foo"),
attr::class("bar"),
))
.render_to_string()
.unwrap(),
r#"<p class="foo bar" id="bar"></p>"#,
)
} }
#[test] #[test]
fn always_lowercase() { fn always_lowercase() {
assert_eq!( assert_eq!(
Element::normal("HTML") Element::normal("HTML")
.with(Attr::set("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>"#,
); );
} }
#[test]
fn comments() {
assert_eq!(
html(("<!--abc--> ", Content::comment("abc")))
.render_to_string()
.unwrap(),
r#"<html>&lt;!--abc--&gt; <!--abc--></html>"#,
);
assert_eq!(
html(Content::comment("Hello <!-- world -->!"))
.render_to_string()
.unwrap(),
r#"<html><!--Hello <!== world ==>!--></html>"#,
);
assert_eq!(
html(Content::comment("-><!-")).render_to_string().unwrap(),
r#"<html><!-- -><!- --></html>"#,
);
}
} }

View file

@ -1,8 +1,9 @@
use std::{error, fmt}; use std::{error, fmt};
use crate::{ use crate::{
Document, check, check,
element::{Content, Element, ElementKind}, element::{Content, Element, ElementKind},
Document,
}; };
/// The cause of an [`Error`]. /// The cause of an [`Error`].
@ -245,8 +246,6 @@ fn render_text<W: fmt::Write>(w: &mut W, text: &str) -> Result<()> {
} }
fn render_comment<W: fmt::Write>(w: &mut W, text: &str) -> Result<()> { fn render_comment<W: fmt::Write>(w: &mut W, text: &str) -> Result<()> {
write!(w, "<!--")?;
// A comment... // A comment...
// - must not start with the string ">" // - must not start with the string ">"
// - must not start with the string "->" // - must not start with the string "->"
@ -270,7 +269,6 @@ fn render_comment<W: fmt::Write>(w: &mut W, text: &str) -> Result<()> {
write!(w, " ")?; write!(w, " ")?;
} }
write!(w, "-->")?;
Ok(()) Ok(())
} }