diff --git a/htmx-macros/src/htmx/html.rs b/htmx-macros/src/htmx/html.rs
index 1fb35ee..29f6b40 100644
--- a/htmx-macros/src/htmx/html.rs
+++ b/htmx-macros/src/htmx/html.rs
@@ -87,21 +87,21 @@ pub fn expand_node(node: Node) -> Result {
};
let script = script.into_token_stream();
if let Ok(script) = parse2::(script.clone()) {
- quote!(__html.child_expr(#script);)
+ quote!(::htmx::ToScript::to_script(script, &mut __html);)
} else if let Ok(block) =
parse2::>(script.clone()).map(Recoverable::inner)
{
- quote!(__html.child_expr({#[allow(unused_braces)] #block});)
+ quote!(::htmx::ToScript::to_script(&{#[allow(unused_braces)] #block}, &mut __html);)
} else {
let script: Script = parse2(script)?;
let script = script.to_java_script();
- quote!(__html.child(#script);)
+ quote!(::htmx::ToScript::to_script(#script, &mut __html);)
}
} else {
expand_nodes(children)?
};
- let body = (!children.is_empty()).then(|| quote!(let mut __html = __html.body();));
- let main = quote!({let mut __html = #name #(__html.#attributes;)* #body; #children});
+ let body = (!children.is_empty()).then(|| quote!(let mut __html = __html.body(|mut __html| {#children});));
+ let main = quote!({let mut __html = #name #(.#attributes)*; #body;});
match close_tag {
Some(CloseTag {
@@ -139,11 +139,11 @@ pub fn ensure_tag_name(name: String, span: impl ToTokens) -> Result Result<(TokenStream, bool)> {
match name {
- NodeName::Path(path) => Ok((quote!(#path::new(&mut __html);), false)),
+ NodeName::Path(path) => Ok((quote!(#path::new(&mut __html)), false)),
name @ NodeName::Punctuated(_) => {
let name = ensure_tag_name(name.to_string(), name)?;
Ok((
- quote!(::htmx::CustomElement::new_unchecked(&mut __html, #name);),
+ quote!(::htmx::CustomElement::new_unchecked(&mut __html, #name)),
true,
))
}
@@ -161,12 +161,12 @@ fn name_to_struct(name: NodeName) -> Result<(TokenStream, bool)> {
{
let name = ensure_tag_name(name.value(), name)?;
Ok((
- quote!(::htmx::CustomElement::new_unchecked(&mut __html, #name);),
+ quote!(::htmx::CustomElement::new_unchecked(&mut __html, #name)),
true,
))
} else {
Ok((
- quote!(::htmx::CustomElement::new(&mut __html, #name);),
+ quote!(::htmx::CustomElement::new(&mut __html, #name)),
true,
))
}
diff --git a/htmx-macros/src/htmx/rusty.rs b/htmx-macros/src/htmx/rusty.rs
index e173b67..7af5d38 100644
--- a/htmx-macros/src/htmx/rusty.rs
+++ b/htmx-macros/src/htmx/rusty.rs
@@ -410,13 +410,12 @@ impl Element {
let attrs = attrs.attrs.into_iter().map(Attr::expand);
- let body = children.peek().is_some().then(|| quote!(let mut __html = __html.body()));
+ let body = children.peek().is_some().then(|| quote!(__html.body(|mut __html| {#(#children)*})));
quote!({
let mut __html = #name
#(__html #attrs;)*
#body;
- #(#children)*
})
}
}
diff --git a/src/lib.rs b/src/lib.rs
index d4b37de..3e68975 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -389,7 +389,6 @@ impl WriteHtml for ManuallyDrop {
///
/// The [`html!`] macro uses them for all tags that contain `-` making it
/// possible to use web-components.
-#[must_use]
pub struct CustomElement {
html: ManuallyDrop,
name: ManuallyDrop>,
@@ -444,11 +443,7 @@ impl CustomElement {
/// [`AnyAttributeValue`], without checking for invalid characters.
///
/// Note: This function does contain the check for [invalid attribute names](https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0) only in debug builds, failing to ensure valid keys can lead to broken HTML output.
- pub fn custom_attr_unchecked(
- &mut self,
- key: impl Display,
- value: impl ToAttribute,
- ) {
+ pub fn custom_attr_unchecked(&mut self, key: impl Display, value: impl ToAttribute) {
debug_assert!(!key.to_string().chars().any(|c| c.is_whitespace()
|| c.is_control()
|| matches!(c, '\0' | '"' | '\'' | '>' | '/' | '=')), "invalid key `{key}`, https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0");
@@ -456,52 +451,29 @@ impl CustomElement {
value.write(&mut self.html);
}
- pub fn custom_attr_composed(self, key: impl Display) -> CustomElement {
- assert!(!key.to_string().chars().any(|c| c.is_whitespace()
- || c.is_control()
- || matches!(c, '\0' | '"' | '\'' | '>' | '/' | '=')), "invalid key `{key}`, https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0");
- self.custom_attr_composed_unchecked(key)
- }
-
- pub fn custom_attr_composed_unchecked(
- mut self,
- key: impl Display,
- ) -> CustomElement {
- debug_assert!(!key.to_string().chars().any(|c| c.is_whitespace()
- || c.is_control()
- || matches!(c, '\0' | '"' | '\'' | '>' | '/' | '=')), "invalid key `{key}`, https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0");
- write!(self.html, " {key}=\"");
- self.change_state()
- }
-
- pub fn body(mut self) -> CustomElement {
+ // TODO, use closure like body
+ // pub fn custom_attr_composed(self, key: impl Display) -> CustomElement { assert!(!key.to_string().chars().any(|c|
+ // c.is_whitespace() || c.is_control()
+ // || matches!(c, '\0' | '"' | '\'' | '>' | '/' | '=')), "invalid key `{key}`, https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0");
+ // self.custom_attr_composed_unchecked(key)
+ // }
+
+ // pub fn custom_attr_composed_unchecked(
+ // mut self,
+ // key: impl Display,
+ // ) -> CustomElement {
+ // debug_assert!(!key.to_string().chars().any(|c| c.is_whitespace()
+ // || c.is_control()
+ // || matches!(c, '\0' | '"' | '\'' | '>' | '/' | '=')), "invalid key `{key}`, https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0");
+ // write!(self.html, " {key}=\"");
+ // self.change_state()
+ // }
+
+ pub fn body(mut self, body: impl FnOnce(&mut Html)) {
self.html.write_gt();
- self.change_state()
- }
-}
-
-impl WriteHtml for CustomElement {
- fn write_str(&mut self, s: &str) {
- self.html.write_str(s);
- }
-
- fn write_char(&mut self, c: char) {
- self.html.write_char(c);
- }
-
- fn write_fmt(&mut self, a: std::fmt::Arguments) {
- self.html.write_fmt(a);
- }
-}
-
-impl CustomElement {
- pub fn child_expr(mut self, child: impl ToHtml) -> Self {
- child.to_html(&mut self);
- self
- }
-
- pub fn child(self, child: impl FnOnce(Self) -> C) -> C {
- child(self)
+ body(&mut self.html);
+ self.change_state::();
}
}
@@ -516,14 +488,6 @@ impl CustomElement {
state: PhantomData,
}
}
-
- pub fn close(mut self) -> Html {
- S::close_tag(&mut self.html);
- self.html.write_close_tag_unchecked(self.name.as_ref());
- let html = unsafe { ManuallyDrop::take(&mut self.html) };
- std::mem::forget(self);
- html
- }
}
impl Drop for CustomElement {
@@ -533,20 +497,6 @@ impl Drop for CustomElement {
}
}
-impl CustomElement {
- pub fn attr_value(mut self, value: impl ToAttribute) -> Self {
- if !value.is_unset() {
- value.write_inner(&mut self.html);
- }
- self
- }
-
- pub fn close_attr(mut self) -> CustomElement {
- self.html.write_quote();
- self.change_state()
- }
-}
-
/// Puts content directly into HTML bypassing HTML-escaping.
///
/// ```
@@ -567,6 +517,30 @@ impl<'a> RawHtml<'a> {
}
}
+pub struct Fragment(pub F);
+
+// TODO reconsider elements implementing WriteHtml, maybe it would be better for them to implement a way to access the underlying `Html`
+impl Fragment {
+ #[allow(non_snake_case)]
+ pub fn EMPTY(self, html: impl WriteHtml) {}
+}
+
+impl IntoHtml for Fragment {
+ fn into_html(self, html: Html) {
+ self.0(html);
+ }
+}
+
+pub trait IntoHtml {
+ fn into_html(self, html: Html);
+}
+
+impl IntoHtml for T {
+ fn into_html(self, html: Html) {
+ self.to_html(html);
+ }
+}
+
pub trait ToHtml {
fn to_html(&self, html: impl WriteHtml);
}
@@ -629,3 +603,49 @@ impl ElementState for Body {
pub trait ElementState {
fn close_tag(html: impl WriteHtml);
}
+
+forr! {$type:ty in [&str, String, Cow<'_, str>]$*
+ impl ToHtml for $type {
+ fn to_html(&self, mut out: impl WriteHtml) {
+ write!(out, "{}", html_escape::encode_text(&self));
+ }
+ }
+
+ impl ToScript for $type {
+ fn to_script(&self, mut out: impl WriteHtml) {
+ write!(out, "{}", html_escape::encode_script(&self));
+ }
+ }
+
+ impl ToStyle for $type {
+ fn to_style(&self, mut out: impl WriteHtml) {
+ write!(out, "{}", html_escape::encode_style(&self));
+ }
+ }
+}
+
+impl ToHtml for char {
+ fn to_html(&self, mut out: impl WriteHtml) {
+ write!(out, "{}", html_escape::encode_text(&self.to_string()));
+ }
+}
+
+pub trait ToScript {
+ fn to_script(&self, out: impl WriteHtml);
+}
+
+impl ToScript for &T {
+ fn to_script(&self, out: impl WriteHtml) {
+ T::to_script(self, out);
+ }
+}
+
+pub trait ToStyle {
+ fn to_style(&self, out: impl WriteHtml);
+}
+
+impl ToStyle for &T {
+ fn to_style(&self, out: impl WriteHtml) {
+ T::to_style(self, out);
+ }
+}
diff --git a/src/native.rs b/src/native.rs
index 9e3b83c..03576fa 100644
--- a/src/native.rs
+++ b/src/native.rs
@@ -1,7 +1,6 @@
//! Native HTML elements
#![allow(non_camel_case_types, clippy::return_self_not_must_use)]
-use std::borrow::Cow;
use std::fmt::Display;
use std::marker::PhantomData;
use std::mem::ManuallyDrop;
@@ -9,53 +8,10 @@ use std::mem::ManuallyDrop;
use forr::{forr, iff};
use crate::attributes::{Any, DateTime, FlagOrValue, Number, TimeDateTime, ToAttribute};
-use crate::{Body, ClassesAttr, CustomAttr, ElementState, StyleAttr, Tag, ToHtml, WriteHtml};
-
-forr! {$type:ty in [&str, String, Cow<'_, str>]$*
- impl ToHtml for $type {
- fn to_html(&self, mut out: impl WriteHtml) {
- write!(out, "{}", html_escape::encode_text(&self));
- }
- }
-
- impl ToScript for $type {
- fn to_script(&self, mut out: impl WriteHtml) {
- write!(out, "{}", html_escape::encode_script(&self));
- }
- }
-
- impl ToStyle for $type {
- fn to_style(&self, mut out: impl WriteHtml) {
- write!(out, "{}", html_escape::encode_style(&self));
- }
- }
-}
-
-impl ToHtml for char {
- fn to_html(&self, mut out: impl WriteHtml) {
- write!(out, "{}", html_escape::encode_text(&self.to_string()));
- }
-}
-
-pub trait ToScript {
- fn to_script(&self, out: impl WriteHtml);
-}
-
-impl ToScript for &T {
- fn to_script(&self, out: impl WriteHtml) {
- T::to_script(self, out);
- }
-}
-
-pub trait ToStyle {
- fn to_style(&self, out: impl WriteHtml);
-}
-
-impl ToStyle for &T {
- fn to_style(&self, out: impl WriteHtml) {
- T::to_style(self, out);
- }
-}
+use crate::{
+ Body, ClassesAttr, CustomAttr, ElementState, StyleAttr, Tag, ToHtml, ToScript, ToStyle,
+ WriteHtml,
+};
macro_rules! attribute {
($elem:ident|$name:ident) => {
@@ -180,8 +136,9 @@ forr! { $type:ty in [a, abbr, address, area, article, aside, audio, b, base, bdi
}
iff! {!equals_any($type)[(area), (base), (br), (col), (embeded), (hr), (input), (link), (meta), (source), (track), (wbr)] $:
- pub fn body(mut self) -> $type {
+ pub fn body(mut self, body: impl FnOnce(&mut T)) -> $type {
self.html.write_gt();
+ body(&mut self.html);
self.change_state()
}
}
diff --git a/src/utils.rs b/src/utils.rs
index 5fddaec..6e40fae 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,5 +1,4 @@
-use crate::native::ToScript;
-use crate::{ElementState, ToHtml, WriteHtml};
+use crate::{IntoHtml, ToHtml, ToScript, WriteHtml};
/// Embed [HTMX script](https://htmx.org/).
///
@@ -102,25 +101,113 @@ impl, I: IntoIterator- > From for AttrVec {
// )
// }
-pub struct HtmlPage {
- html: ::std::mem::ManuallyDrop,
- state: ::std::marker::PhantomData
,
- // state
+// Components need to follow the following contract for :
+// - new(html: Html) -> Self;
+// - close(self); Closes element without body
+// - body(self, impl FnOnce(Html)); Closes element with body, only required when
+// accepting children
+
+pub struct HtmlPage {
+ html: Html,
+ mobile: Mobile,
+ title: Title,
+ style_sheets: StyleSheets,
+ scripts: Scripts,
+ lang: Lang,
}
const _: () = {
- use ::htmx::WriteHtml;
- impl HtmlPage {
+ use ::htmx::WriteHtml as _;
+
+ struct Unset;
+ struct Set(T);
+
+ impl HtmlPage {
pub fn new(html: Html) -> Self {
Self {
- html: ::std::mem::ManuallyDrop::new(html),
- state: ::std::marker::PhantomData,
+ html,
+ mobile: Unset,
+ title: Unset,
+ style_sheets: Unset,
+ scripts: Unset,
+ lang: Unset,
+ }
+ }
+ }
+
+ impl
+ HtmlPage
+ {
+ pub fn mobile(
+ self,
+ mobile: bool,
+ ) -> HtmlPage, Title, StyleSheets, Scripts, Lang> {
+ let Self {
+ html,
+ mobile: _,
+ title,
+ style_sheets,
+ scripts,
+ lang,
+ } = self;
+ HtmlPage {
+ html,
+ mobile: Set(mobile),
+ title,
+ style_sheets,
+ scripts,
+ lang,
}
}
+ }
+
+ #[allow(non_camel_case_types)]
+ struct mobile_was_already_set;
+
+ impl
+ HtmlPage, Title, StyleSheets, Scripts, Lang>
+ {
+ #[deprecated = "mobile was already set"]
+ pub fn mobile(
+ self,
+ mobile: bool,
+ _: mobile_was_already_set,
+ ) -> HtmlPage, Title, StyleSheets, Scripts, Lang> {
+ let Self {
+ html,
+ mobile: _,
+ title,
+ style_sheets,
+ scripts,
+ lang,
+ } = self;
+ HtmlPage {
+ html,
+ mobile: Set(mobile),
+ title,
+ style_sheets,
+ scripts,
+ lang,
+ }
+ }
+ }
+
+ impl
+ HtmlPage, Title, StyleSheets, Scripts, Lang>
+ {
+ pub fn body(self, body: impl ::htmx::IntoHtml) {
+ let Self {
+ html,
+ mobile: Set(mobile),
+ title,
+ style_sheets,
+ scripts,
+ lang,
+ } = self;
+ }
- pub fn lang(&mut self, value: impl ::htmx::attributes::ToAttribute) {
- self.html.write_str(" lang");
- value.write(&mut self.html);
+ pub fn close(self) {
+ self.body(::htmx::Fragment::EMPTY)
}
}
};
diff --git a/tests/utils.rs b/tests/utils.rs
index bf2fb20..6587ed0 100644
--- a/tests/utils.rs
+++ b/tests/utils.rs
@@ -5,8 +5,9 @@ use insta::assert_snapshot;
fn html_page() {
assert_snapshot!(
html! {
-
+
+
}
.to_string()