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()