♻️ (pagetop): Props::unpack() exige &mut Context

This commit is contained in:
Manuel Cillero 2026-09-08 07:28:50 +02:00
parent ee1b48ff3f
commit 8577ca8a59
14 changed files with 280 additions and 190 deletions

View file

@ -25,13 +25,13 @@
//! ```rust,no_run //! ```rust,no_run
//! # use pagetop::prelude::*; //! # use pagetop::prelude::*;
//! # use pagetop_htmx::prelude::*; //! # use pagetop_htmx::prelude::*;
//! # let cx = Context::default(); //! # let mut cx = Context::default();
//! let props = Props::new(hx::GET, "/api/items") //! let props = Props::new(hx::GET, "/api/items")
//! .with_prop(PropsOp::set(hx::TARGET, "#list")) //! .with_prop(PropsOp::set(hx::TARGET, "#list"))
//! .with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML)); //! .with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML));
//! //!
//! let markup = html! { //! let markup = html! {
//! button (props.unpack(&cx)) { "Load" } //! button (props.unpack(&mut cx)) { "Load" }
//! }; //! };
//! ``` //! ```
//! //!

View file

@ -88,7 +88,7 @@ impl Crumb {
} }
// Renderiza con enlace si tiene ruta, o texto plano en otro caso. Sólo lo usa `Breadcrumb`. // Renderiza con enlace si tiene ruta, o texto plano en otro caso. Sólo lo usa `Breadcrumb`.
pub(super) fn render_crumb(&self, cx: &Context) -> Markup { pub(super) fn render_crumb(&self, cx: &mut Context) -> Markup {
let label = self.label().using(cx); let label = self.label().using(cx);
match self.route() { match self.route() {
Some(route) => html! { Some(route) => html! {

View file

@ -85,7 +85,7 @@ impl Column {
// Traduce la etiqueta y, si la columna es ordenable, la envuelve en su enlace con `aria-sort` // Traduce la etiqueta y, si la columna es ordenable, la envuelve en su enlace con `aria-sort`
// y las clases `table-sort*` ya resueltas por `table::SortLink`. Sólo lo usa `Table` al // y las clases `table-sort*` ya resueltas por `table::SortLink`. Sólo lo usa `Table` al
// renderizar. // renderizar.
pub(super) fn render_header(&self, cx: &Context) -> Markup { pub(super) fn render_header(&self, cx: &mut Context) -> Markup {
let label = self.label().using(cx); let label = self.label().using(cx);
let Some(sort) = self.sort() else { let Some(sort) = self.sort() else {

View file

@ -71,7 +71,7 @@ impl PageTopSvg {
/// [`Lc::none()`]: crate::locale::Lc::none /// [`Lc::none()`]: crate::locale::Lc::none
/// [`Image`]: crate::base::component::Image /// [`Image`]: crate::base::component::Image
/// [`image::Source::Logo`]: crate::base::component::image::Source::Logo /// [`image::Source::Logo`]: crate::base::component::image::Source::Logo
pub fn markup_with(&self, cx: &Context, props: &Props, label: Lc) -> Markup { pub fn markup_with(&self, cx: &mut Context, props: &Props, label: Lc) -> Markup {
let label = label.lookup(cx); let label = label.lookup(cx);
html! { html! {
svg svg

View file

@ -19,18 +19,18 @@ use std::panic::Location;
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// En los ejemplos se omite la construcción explícita de `Context` (`let cx = Context::default();`) /// Se omite la construcción explícita de `Context` (`let mut cx = Context::default();`) para no
/// para no distraer del resto del ejemplo. /// distraer del resto del ejemplo.
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// # let cx = Context::default(); /// # let mut cx = Context::default();
/// let props = Props::new("hx-get", "/api/items") /// let props = Props::new("hx-get", "/api/items")
/// .with_prop(PropsOp::set("hx-target", "#lista")) /// .with_prop(PropsOp::set("hx-target", "#lista"))
/// .with_prop(PropsOp::set("hx-swap", "outerHTML")); /// .with_prop(PropsOp::set("hx-swap", "outerHTML"));
/// ///
/// let markup = html! { /// let markup = html! {
/// button (props.unpack(&cx)) { "Cargar" } /// button (props.unpack(&mut cx)) { "Cargar" }
/// }; /// };
/// ///
/// assert_eq!( /// assert_eq!(
@ -47,9 +47,9 @@ use std::panic::Location;
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// # let cx = Context::default(); /// # let mut cx = Context::default();
/// let props = Props::default().with_id("My Button"); /// let props = Props::default().with_id("My Button");
/// let markup = html! { button (props.unpack(&cx)) { "OK" } }; /// let markup = html! { button (props.unpack(&mut cx)) { "OK" } };
/// assert_eq!(markup.into_string(), r#"<button id="my_button">OK</button>"#); /// assert_eq!(markup.into_string(), r#"<button id="my_button">OK</button>"#);
/// ``` /// ```
/// ///
@ -73,13 +73,13 @@ use std::panic::Location;
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// # let cx = Context::default(); /// # let mut cx = Context::default();
/// let props = Props::default() /// let props = Props::default()
/// .with_prop(PropsOp::add_classes("btn btn-primary")) /// .with_prop(PropsOp::add_classes("btn btn-primary"))
/// .with_prop(PropsOp::add_classes("active")) /// .with_prop(PropsOp::add_classes("active"))
/// .with_prop(PropsOp::replace_classes("btn-primary", "btn-secondary")); /// .with_prop(PropsOp::replace_classes("btn-primary", "btn-secondary"));
/// ///
/// let markup = html! { button (props.unpack(&cx)) { "OK" } }; /// let markup = html! { button (props.unpack(&mut cx)) { "OK" } };
/// assert_eq!(markup.into_string(), r#"<button class="btn btn-secondary active">OK</button>"#); /// assert_eq!(markup.into_string(), r#"<button class="btn btn-secondary active">OK</button>"#);
/// ``` /// ```
/// ///
@ -90,14 +90,14 @@ use std::panic::Location;
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// # let cx = Context::default(); /// # let mut cx = Context::default();
/// let props = Props::default() /// let props = Props::default()
/// .with_prop(PropsOp::add_style("color", "red")) /// .with_prop(PropsOp::add_style("color", "red"))
/// .with_prop(PropsOp::add_style("font-weight", "bold")) /// .with_prop(PropsOp::add_style("font-weight", "bold"))
/// .with_prop(PropsOp::add_style("color", "blue")) /// .with_prop(PropsOp::add_style("color", "blue"))
/// .with_prop(PropsOp::remove_style("font-weight")); /// .with_prop(PropsOp::remove_style("font-weight"));
/// ///
/// let markup = html! { button (props.unpack(&cx)) { "OK" } }; /// let markup = html! { button (props.unpack(&mut cx)) { "OK" } };
/// assert_eq!(markup.into_string(), r#"<button style="color: blue">OK</button>"#); /// assert_eq!(markup.into_string(), r#"<button style="color: blue">OK</button>"#);
/// ``` /// ```
/// ///
@ -111,10 +111,10 @@ use std::panic::Location;
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// # let cx = Context::default(); /// # let mut cx = Context::default();
/// let props = Props::default().with_prop(PropsOp::set("title", "de Props")); /// let props = Props::default().with_prop(PropsOp::set("title", "de Props"));
/// ///
/// let markup = html! { span title="literal" (props.unpack(&cx)) { "OK" } }; /// let markup = html! { span title="literal" (props.unpack(&mut cx)) { "OK" } };
/// ///
/// // El atributo literal prevalece; `Props` omite su propio "title" en vez de duplicarlo. /// // El atributo literal prevalece; `Props` omite su propio "title" en vez de duplicarlo.
/// assert_eq!(markup.into_string(), r#"<span title="literal">OK</span>"#); /// assert_eq!(markup.into_string(), r#"<span title="literal">OK</span>"#);
@ -553,21 +553,54 @@ impl Props {
// **< Props RENDER >*************************************************************************** // **< Props RENDER >***************************************************************************
/// Extrae `Props` en la posición de atributos de [`html!`](crate::html::html) usando el /// Extrae `Props` en la posición de atributos de [`html!`] usando el `Context` activo:
/// `Context` activo: `button (self.props().unpack(cx)) { ... }`. /// `button (self.props().unpack(cx)) { ... }`.
/// ///
/// `Props` no implementa [`RenderAttrs`] directamente. Obliga a pasar siempre el `Context` /// `Props` no implementa [`RenderAttrs`] directamente. Obliga a pasar siempre el `Context`
/// vigente en el punto donde se renderiza, aunque no lo necesite ningún atributo. /// vigente en el punto donde se renderiza, aunque no lo necesite ningún atributo propio. Recibe
/// `&mut Context` porque aquí, en el momento de extraer los atributos, es donde se resuelve
/// [`FlexItem`]: las clases que devuelve se añaden a las del propio componente al escribir el
/// atributo `class`.
///
/// Si el propio elemento actúa además como contenedor [`Flex`], utiliza [`unpack_with_flex()`]
/// en su lugar.
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// # let cx = Context::default(); /// # let mut cx = Context::default();
/// let props = Props::default().with_id("example"); /// let props = Props::default().with_id("example");
/// let markup = html! { button (props.unpack(&cx)) { "OK" } }; /// let markup = html! { button (props.unpack(&mut cx)) { "OK" } };
/// assert_eq!(markup.into_string(), r#"<button id="example">OK</button>"#); /// assert_eq!(markup.into_string(), r#"<button id="example">OK</button>"#);
/// ``` /// ```
pub fn unpack<'a>(&'a self, cx: &'a Context) -> impl RenderAttrs + 'a { ///
PropsUnpack { props: self, cx } /// [`html!`]: crate::html::html
/// [`Flex`]: crate::html::flex::Flex
/// [`FlexItem`]: crate::html::flex::FlexItem
/// [`unpack_with_flex()`]: Self::unpack_with_flex
pub fn unpack<'a>(&'a self, cx: &mut Context) -> impl RenderAttrs + 'a {
PropsUnpack {
props: self,
classes: self.flex_item.apply(cx),
}
}
/// Igual que [`unpack()`], pero además resuelve `flex` con el posicionamiento [`Flex`].
///
/// A diferencia de [`FlexItem`] (que se acumula con [`PropsOp::FlexItem`] porque cualquier
/// componente ajeno puede necesitarlo sin tener un campo propio para ello), `Flex` sólo tiene
/// sentido en los contenedores que ya declaran su propio campo `flex: Flex` (`Container`,
/// `Navbar`...): se les pasa aquí directamente, ya resuelto (`self.flex()`), sin pasar por
/// `PropsOp`.
///
/// [`unpack()`]: Self::unpack
/// [`Flex`]: crate::html::flex::Flex
/// [`FlexItem`]: crate::html::flex::FlexItem
/// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem
pub fn unpack_with_flex<'a>(&'a self, cx: &mut Context, flex: Flex) -> impl RenderAttrs + 'a {
PropsUnpack {
props: self,
classes: util::join_pair!(flex.apply(cx), " ", self.flex_item.apply(cx)),
}
} }
// **< Props PRIVATE >************************************************************************** // **< Props PRIVATE >**************************************************************************
@ -675,7 +708,7 @@ impl Props {
// `#[track_caller]`, heredado desde `PropsUnpack::render_attrs_to()`) para facilitar la // `#[track_caller]`, heredado desde `PropsUnpack::render_attrs_to()`) para facilitar la
// localización del problema. // localización del problema.
#[track_caller] #[track_caller]
fn write_attrs(&self, _cx: &Context, w: &mut String, exclude: &[&str]) { fn write_attrs(&self, classes: &str, w: &mut String, exclude: &[&str]) {
if let Some(id) = self.id.as_deref() { if let Some(id) = self.id.as_deref() {
if exclude.contains(&"id") { if exclude.contains(&"id") {
trace::debug!( trace::debug!(
@ -690,12 +723,15 @@ impl Props {
w.push('"'); w.push('"');
} }
} }
if let Some((first, rest)) = self.classes.split_first() { // Clases propias del componente más las que aplica `Props::unpack()`/`unpack_with_flex()`.
let mut all_classes: Vec<&str> = self.classes.iter().map(String::as_str).collect();
all_classes.extend(classes.split_ascii_whitespace());
if let Some((first, rest)) = all_classes.split_first() {
if exclude.contains(&"class") { if exclude.contains(&"class") {
trace::debug!( trace::debug!(
caller = %Location::caller(), caller = %Location::caller(),
attribute = "class", attribute = "class",
discarded = %self.classes.join(" "), discarded = %all_classes.join(" "),
id = %self.id.as_deref().unwrap_or("<none>"), id = %self.id.as_deref().unwrap_or("<none>"),
"Ignoring Props attribute already set as a literal on the same element" "Ignoring Props attribute already set as a literal on the same element"
); );
@ -756,16 +792,18 @@ impl Props {
// **< PropsUnpack >******************************************************************************** // **< PropsUnpack >********************************************************************************
// Devuelto por `Props::unpack()`. // Devuelto por `Props::unpack()`/`Props::unpack_with_flex()`. `classes` son las clases resueltas
// por `FlexItem::apply()`/`Flex::apply()` (desde el propio `unpack*()` usando el `&mut Context`),
// pendientes sólo de añadir a las del componente (ver `Props::write_attrs()`).
struct PropsUnpack<'a> { struct PropsUnpack<'a> {
props: &'a Props, props: &'a Props,
cx: &'a Context, classes: String,
} }
#[doc(hidden)] #[doc(hidden)]
impl RenderAttrs for PropsUnpack<'_> { impl RenderAttrs for PropsUnpack<'_> {
#[track_caller] #[track_caller]
fn render_attrs_to(&self, w: &mut String, exclude: &[&str]) { fn render_attrs_to(&self, w: &mut String, exclude: &[&str]) {
self.props.write_attrs(self.cx, w, exclude); self.props.write_attrs(&self.classes, w, exclude);
} }
} }

View file

@ -247,7 +247,7 @@ impl Page {
head { head {
(head) (head)
} }
body (self.body_props().unpack(&self.context)) { body (self.body_props().clone().unpack(&mut self.context)) {
(body) (body)
} }
} }

View file

@ -49,92 +49,97 @@ async fn without_flex_no_style_attribute_is_added() {
#[pagetop::test] #[pagetop::test]
async fn default_flex_adds_only_display_flex() { async fn default_flex_adds_only_display_flex() {
let mut cx = Context::default();
let mut container = Container::new() let mut container = Container::new()
.with_flex(Flex::row()) .with_flex(Flex::new())
.with_child(Lc::n("x")); .with_child(Lc::n("x"));
let html = container let html = container.render(&mut cx).await.into_string();
.render(&mut Context::default()) let assets = cx.render_assets().into_string();
.await
.into_string();
assert!(html.contains(r#"style="display: flex""#)); assert!(html.contains(r#"class="_flex_""#));
assert!(assets.contains("_flex_{display:flex}"));
} }
#[pagetop::test] #[pagetop::test]
async fn column_direction_adds_flex_direction_style() { async fn column_direction_adds_flex_direction_style() {
let mut cx = Context::default();
let mut container = Container::new() let mut container = Container::new()
.with_flex(Flex::column()) .with_flex(Flex::new().with_direction(flex::Direction::Column))
.with_child(Lc::n("x")); .with_child(Lc::n("x"));
let html = container let html = container.render(&mut cx).await.into_string();
.render(&mut Context::default()) let assets = cx.render_assets().into_string();
.await
.into_string();
assert!(html.contains("display: flex")); assert!(html.contains("_flex_"));
assert!(html.contains("flex-direction: column")); assert!(html.contains("_flex-direction_column_"));
assert!(assets.contains("_flex_{display:flex}"));
assert!(assets.contains("_flex-direction_column_{flex-direction:column}"));
} }
#[pagetop::test] #[pagetop::test]
async fn wrap_justify_and_align_add_their_matching_styles() { async fn wrap_justify_and_align_add_their_matching_styles() {
let mut cx = Context::default();
let mut container = Container::new() let mut container = Container::new()
.with_flex( .with_flex(
Flex::row() Flex::new()
.with_wrap(flex::Behavior::Wrap) .with_wrap(flex::Behavior::Wrap)
.with_justify(flex::ContentJustify::Center) .with_justify(flex::ContentJustify::Center)
.with_align(flex::Align::Center) .with_align(flex::Align::Center)
.with_align_content(flex::AlignContent::SpaceBetween), .with_align_content(flex::AlignContent::SpaceBetween),
) )
.with_child(Lc::n("x")); .with_child(Lc::n("x"));
let html = container let html = container.render(&mut cx).await.into_string();
.render(&mut Context::default()) let assets = cx.render_assets().into_string();
.await
.into_string();
assert!(html.contains("flex-wrap: wrap")); assert!(html.contains("_flex-wrap_wrap_"));
assert!(html.contains("justify-content: center")); assert!(html.contains("_flex-justify_center_"));
assert!(html.contains("align-items: center")); assert!(html.contains("_flex-align-items_center_"));
assert!(html.contains("align-content: space-between")); assert!(html.contains("_flex-align-content_space-between_"));
assert!(assets.contains("_flex-wrap_wrap_{flex-wrap:wrap}"));
assert!(assets.contains("_flex-justify_center_{justify-content:center}"));
assert!(assets.contains("_flex-align-items_center_{align-items:center}"));
assert!(assets.contains("_flex-align-content_space-between_{align-content:space-between}"));
} }
#[pagetop::test] #[pagetop::test]
async fn gap_both_adds_a_single_gap_style() { async fn gap_both_adds_a_single_gap_style() {
let mut cx = Context::default();
let mut container = Container::new() let mut container = Container::new()
.with_flex(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))) .with_flex(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
.with_child(Lc::n("x")); .with_child(Lc::n("x"));
let html = container let html = container.render(&mut cx).await.into_string();
.render(&mut Context::default()) let assets = cx.render_assets().into_string();
.await
.into_string();
assert!(html.contains("gap: 0.5rem")); assert!(html.contains("_flex-gap_0_5rem_"));
assert!(assets.contains("_flex-gap_0_5rem_{gap:0.5rem}"));
} }
#[pagetop::test] #[pagetop::test]
async fn gap_distinct_adds_row_and_column_gap_styles() { async fn gap_distinct_adds_row_and_column_gap_styles() {
let mut cx = Context::default();
let mut container = Container::new() let mut container = Container::new()
.with_flex(Flex::row().with_gap(flex::Gap::Distinct { .with_flex(Flex::new().with_gap(flex::Gap::Distinct {
row: UnitValue::Px(4), row: UnitValue::Px(4),
column: UnitValue::Px(8), column: UnitValue::Px(8),
})) }))
.with_child(Lc::n("x")); .with_child(Lc::n("x"));
let html = container let html = container.render(&mut cx).await.into_string();
.render(&mut Context::default()) let assets = cx.render_assets().into_string();
.await
.into_string();
assert!(html.contains("row-gap: 4px")); assert!(html.contains("_flex-row-gap_4px_"));
assert!(html.contains("column-gap: 8px")); assert!(html.contains("_flex-column-gap_8px_"));
assert!(assets.contains("_flex-row-gap_4px_{row-gap:4px}"));
assert!(assets.contains("_flex-column-gap_8px_{column-gap:8px}"));
} }
#[pagetop::test] #[pagetop::test]
async fn gap_none_adds_no_gap_style() { async fn gap_none_adds_no_gap_style() {
let mut cx = Context::default();
let mut container = Container::new() let mut container = Container::new()
.with_flex(Flex::row()) .with_flex(Flex::new())
.with_child(Lc::n("x")); .with_child(Lc::n("x"));
let html = container let html = container.render(&mut cx).await.into_string();
.render(&mut Context::default()) let assets = cx.render_assets().into_string();
.await
.into_string();
assert!(!html.contains("gap:")); assert!(!html.contains("gap"));
assert!(!assets.contains("gap"));
} }

View file

@ -17,7 +17,7 @@ async fn is_not_rendered_when_empty() {
#[pagetop::test] #[pagetop::test]
async fn nav_root_class_is_unaffected_by_content_flex() { async fn nav_root_class_is_unaffected_by_content_flex() {
let mut navbar = Navbar::simple() let mut navbar = Navbar::simple()
.with_flex(Flex::row().with_justify(flex::ContentJustify::End)) .with_flex(Flex::new().with_justify(flex::ContentJustify::End))
.with_item(navbar::Item::nav(one_link_nav())); .with_item(navbar::Item::nav(one_link_nav()));
let html = navbar.render(&mut Context::default()).await.into_string(); let html = navbar.render(&mut Context::default()).await.into_string();
@ -37,32 +37,42 @@ async fn without_flex_content_area_has_no_style_attribute() {
#[pagetop::test] #[pagetop::test]
async fn flex_adds_its_styles_to_the_content_area() { async fn flex_adds_its_styles_to_the_content_area() {
let mut cx = Context::default();
let mut navbar = Navbar::simple() let mut navbar = Navbar::simple()
.with_flex(Flex::row().with_justify(flex::ContentJustify::End)) .with_flex(Flex::new().with_justify(flex::ContentJustify::End))
.with_item(navbar::Item::nav(one_link_nav())); .with_item(navbar::Item::nav(one_link_nav()));
let html = navbar.render(&mut Context::default()).await.into_string(); let html = navbar.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains(r#"class="navbar-content""#)); assert!(html.contains("navbar-content"));
assert!(html.contains("display: flex")); assert!(html.contains("_flex_"));
assert!(html.contains("justify-content: flex-end")); assert!(html.contains("_flex-justify_flex-end_"));
assert!(assets.contains("_flex_{display:flex}"));
assert!(assets.contains("_flex-justify_flex-end_{justify-content:flex-end}"));
} }
#[pagetop::test] #[pagetop::test]
async fn flex_gap_adds_a_style_to_the_content_area() { async fn flex_gap_adds_a_style_to_the_content_area() {
let mut cx = Context::default();
let mut navbar = Navbar::simple() let mut navbar = Navbar::simple()
.with_flex(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))) .with_flex(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
.with_item(navbar::Item::nav(one_link_nav())); .with_item(navbar::Item::nav(one_link_nav()));
let html = navbar.render(&mut Context::default()).await.into_string(); let html = navbar.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains("gap: 0.5rem")); assert!(html.contains("_flex-gap_0_5rem_"));
assert!(assets.contains("_flex-gap_0_5rem_{gap:0.5rem}"));
} }
// **< Navbar + FlexItem::push_end >**************************************************************** // **< Navbar + FlexItem::push_end >****************************************************************
#[pagetop::test] #[pagetop::test]
async fn push_end_adds_an_automatic_start_margin() { async fn push_end_adds_an_automatic_start_margin() {
let mut nav = one_link_nav().with_prop(FlexItem::push_end()); let mut cx = Context::default();
let html = nav.render(&mut Context::default()).await.into_string(); let mut nav = one_link_nav().with_prop(FlexItem::push_end().into());
let html = nav.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains(r#"style="margin-inline-start: auto""#)); assert!(html.contains("_flex-item-offset_auto_"));
assert!(assets.contains("_flex-item-offset_auto_{margin-inline-start:auto}"));
} }

View file

@ -4,9 +4,9 @@ use pagetop::prelude::*;
#[pagetop::test] #[pagetop::test]
async fn props_default_renders_nothing() { async fn props_default_renders_nothing() {
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (Props::default().unpack(&cx)) {} }.into_string(), html! { span (Props::default().unpack(&mut cx)) {} }.into_string(),
"<span></span>" "<span></span>"
); );
} }
@ -45,9 +45,9 @@ async fn props_set_replaces_existing_value() {
async fn props_set_does_not_create_duplicate_key() { async fn props_set_does_not_create_duplicate_key() {
// Reassigning the same key must replace the value, not add a duplicate entry. // Reassigning the same key must replace the value, not add a duplicate entry.
let p = Props::new("key", "v1").with_prop(PropsOp::set("key", "v2")); let p = Props::new("key", "v1").with_prop(PropsOp::set("key", "v2"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(), html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span key="v2"></span>"# r#"<span key="v2"></span>"#
); );
} }
@ -57,9 +57,9 @@ async fn props_set_preserves_insertion_order() {
let p = Props::new("a", "1") let p = Props::new("a", "1")
.with_prop(PropsOp::set("b", "2")) .with_prop(PropsOp::set("b", "2"))
.with_prop(PropsOp::set("c", "3")); .with_prop(PropsOp::set("c", "3"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(), html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span a="1" b="2" c="3"></span>"# r#"<span a="1" b="2" c="3"></span>"#
); );
} }
@ -85,9 +85,9 @@ async fn props_remove_nonexistent_key_is_noop() {
#[pagetop::test] #[pagetop::test]
async fn props_renders_nothing_after_removing_last_attr() { async fn props_renders_nothing_after_removing_last_attr() {
let p = Props::new("only", "one").with_prop(PropsOp::remove("only")); let p = Props::new("only", "one").with_prop(PropsOp::remove("only"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(), html! { span (p.unpack(&mut cx)) {} }.into_string(),
"<span></span>" "<span></span>"
); );
} }
@ -97,9 +97,9 @@ async fn props_renders_nothing_after_removing_last_attr() {
#[pagetop::test] #[pagetop::test]
async fn props_escapes_ampersand_and_angle_brackets_in_value() { async fn props_escapes_ampersand_and_angle_brackets_in_value() {
let p = Props::new("data-info", "a&b<c>d"); let p = Props::new("data-info", "a&b<c>d");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(), html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span data-info="a&amp;b&lt;c&gt;d"></span>"# r#"<span data-info="a&amp;b&lt;c&gt;d"></span>"#
); );
} }
@ -107,9 +107,9 @@ async fn props_escapes_ampersand_and_angle_brackets_in_value() {
#[pagetop::test] #[pagetop::test]
async fn props_escapes_double_quotes_in_value() { async fn props_escapes_double_quotes_in_value() {
let p = Props::new("data-label", r#"say "hello""#); let p = Props::new("data-label", r#"say "hello""#);
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(), html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span data-label="say &quot;hello&quot;"></span>"# r#"<span data-label="say &quot;hello&quot;"></span>"#
); );
} }
@ -120,9 +120,9 @@ async fn props_escapes_double_quotes_in_value() {
async fn props_empty_in_html_macro_produces_no_attributes() { async fn props_empty_in_html_macro_produces_no_attributes() {
// An empty Props must not emit even an extra blank space. // An empty Props must not emit even an extra blank space.
let p = Props::default(); let p = Props::default();
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (p.unpack(&cx)) { "x" } }.into_string(), html! { button (p.unpack(&mut cx)) { "x" } }.into_string(),
"<button>x</button>" "<button>x</button>"
); );
} }
@ -130,9 +130,9 @@ async fn props_empty_in_html_macro_produces_no_attributes() {
#[pagetop::test] #[pagetop::test]
async fn props_single_attr_in_html_macro() { async fn props_single_attr_in_html_macro() {
let p = Props::new("hx-get", "/api"); let p = Props::new("hx-get", "/api");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (p.unpack(&cx)) { "Load" } }.into_string(), html! { button (p.unpack(&mut cx)) { "Load" } }.into_string(),
r#"<button hx-get="/api">Load</button>"# r#"<button hx-get="/api">Load</button>"#
); );
} }
@ -142,9 +142,9 @@ async fn props_multiple_attrs_preserve_order_in_html_macro() {
let p = Props::new("hx-get", "/api") let p = Props::new("hx-get", "/api")
.with_prop(PropsOp::set("hx-target", "#result")) .with_prop(PropsOp::set("hx-target", "#result"))
.with_prop(PropsOp::set("hx-swap", "outerHTML")); .with_prop(PropsOp::set("hx-swap", "outerHTML"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (p.unpack(&cx)) {} }.into_string(), html! { button (p.unpack(&mut cx)) {} }.into_string(),
r##"<button hx-get="/api" hx-target="#result" hx-swap="outerHTML"></button>"## r##"<button hx-get="/api" hx-target="#result" hx-swap="outerHTML"></button>"##
); );
} }
@ -153,9 +153,9 @@ async fn props_multiple_attrs_preserve_order_in_html_macro() {
async fn props_alongside_class_and_id_in_html_macro() { async fn props_alongside_class_and_id_in_html_macro() {
// The unpack is always emitted after class and id, regardless of the order they are written in. // The unpack is always emitted after class and id, regardless of the order they are written in.
let p = Props::new("hx-get", "/api"); let p = Props::new("hx-get", "/api");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button #mybtn .btn (p.unpack(&cx)) { "Go" } }.into_string(), html! { button #mybtn .btn (p.unpack(&mut cx)) { "Go" } }.into_string(),
r#"<button class="btn" id="mybtn" hx-get="/api">Go</button>"# r#"<button class="btn" id="mybtn" hx-get="/api">Go</button>"#
); );
} }
@ -163,9 +163,9 @@ async fn props_alongside_class_and_id_in_html_macro() {
#[pagetop::test] #[pagetop::test]
async fn props_alongside_named_attr_renders_after_it() { async fn props_alongside_named_attr_renders_after_it() {
let p = Props::new("hx-get", "/api"); let p = Props::new("hx-get", "/api");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button type="button" (p.unpack(&cx)) {} }.into_string(), html! { button type="button" (p.unpack(&mut cx)) {} }.into_string(),
r#"<button type="button" hx-get="/api"></button>"# r#"<button type="button" hx-get="/api"></button>"#
); );
} }
@ -176,31 +176,31 @@ async fn props_combined_via_chaining_instead_of_multiple_splices() {
// is a compile error); values from separate sources are combined by chaining `with_prop()` // is a compile error); values from separate sources are combined by chaining `with_prop()`
// on one `Props`, not by splicing two of them. // on one `Props`, not by splicing two of them.
let p = Props::new("hx-get", "/api").with_prop(PropsOp::set("hx-swap", "outerHTML")); let p = Props::new("hx-get", "/api").with_prop(PropsOp::set("hx-swap", "outerHTML"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (p.unpack(&cx)) {} }.into_string(), html! { button (p.unpack(&mut cx)) {} }.into_string(),
r#"<button hx-get="/api" hx-swap="outerHTML"></button>"# r#"<button hx-get="/api" hx-swap="outerHTML"></button>"#
); );
} }
#[pagetop::test] #[pagetop::test]
async fn props_inline_construction_in_html_macro() { async fn props_inline_construction_in_html_macro() {
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (Props::new("hx-get", "/api").unpack(&cx)) { "Go" } }.into_string(), html! { button (Props::new("hx-get", "/api").unpack(&mut cx)) { "Go" } }.into_string(),
r#"<button hx-get="/api">Go</button>"# r#"<button hx-get="/api">Go</button>"#
); );
} }
#[pagetop::test] #[pagetop::test]
async fn props_conditional_expression_in_html_macro() { async fn props_conditional_expression_in_html_macro() {
let cx = Context::default(); let mut cx = Context::default();
for (active, expected) in [ for (active, expected) in [
(true, r#"<button hx-get="/api">x</button>"#), (true, r#"<button hx-get="/api">x</button>"#),
(false, "<button>x</button>"), (false, "<button>x</button>"),
] { ] {
let markup = html! { let markup = html! {
button (if active { Props::new("hx-get", "/api") } else { Props::default() }.unpack(&cx)) { "x" } button (if active { Props::new("hx-get", "/api") } else { Props::default() }.unpack(&mut cx)) { "x" }
}; };
assert_eq!(markup.into_string(), expected); assert_eq!(markup.into_string(), expected);
} }
@ -219,9 +219,9 @@ async fn props_id_collision_with_literal_omits_props_id() {
// A literal `#id` on the element takes precedence; `Props`'s own id is silently omitted instead // A literal `#id` on the element takes precedence; `Props`'s own id is silently omitted instead
// of producing a duplicate `id` attribute. // of producing a duplicate `id` attribute.
let p = Props::default().with_id("from-props"); let p = Props::default().with_id("from-props");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { div #fixed (p.unpack(&cx)) {} }.into_string(), html! { div #fixed (p.unpack(&mut cx)) {} }.into_string(),
r#"<div id="fixed"></div>"# r#"<div id="fixed"></div>"#
); );
} }
@ -229,9 +229,9 @@ async fn props_id_collision_with_literal_omits_props_id() {
#[pagetop::test] #[pagetop::test]
async fn props_class_collision_with_literal_omits_props_classes() { async fn props_class_collision_with_literal_omits_props_classes() {
let p = Props::classes("from-props-a from-props-b"); let p = Props::classes("from-props-a from-props-b");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { div.fixed (p.unpack(&cx)) {} }.into_string(), html! { div.fixed (p.unpack(&mut cx)) {} }.into_string(),
r#"<div class="fixed"></div>"# r#"<div class="fixed"></div>"#
); );
} }
@ -241,9 +241,9 @@ async fn props_style_collision_with_literal_omits_props_styles() {
let p = Props::default() let p = Props::default()
.with_prop(PropsOp::add_style("color", "red")) .with_prop(PropsOp::add_style("color", "red"))
.with_prop(PropsOp::add_style("font-weight", "bold")); .with_prop(PropsOp::add_style("font-weight", "bold"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { div style="color: blue" (p.unpack(&cx)) {} }.into_string(), html! { div style="color: blue" (p.unpack(&mut cx)) {} }.into_string(),
r#"<div style="color: blue"></div>"# r#"<div style="color: blue"></div>"#
); );
} }
@ -251,9 +251,9 @@ async fn props_style_collision_with_literal_omits_props_styles() {
#[pagetop::test] #[pagetop::test]
async fn props_named_attr_collision_with_literal_omits_props_value() { async fn props_named_attr_collision_with_literal_omits_props_value() {
let p = Props::default().with_prop(PropsOp::set("title", "from-props")); let p = Props::default().with_prop(PropsOp::set("title", "from-props"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span title="literal" (p.unpack(&cx)) {} }.into_string(), html! { span title="literal" (p.unpack(&mut cx)) {} }.into_string(),
r#"<span title="literal"></span>"# r#"<span title="literal"></span>"#
); );
} }
@ -321,9 +321,9 @@ async fn get_prop_id_matches_get_id() {
async fn props_hx_target_value_with_hash_renders_correctly() { async fn props_hx_target_value_with_hash_renders_correctly() {
// Regression: r#"..."# used to close prematurely when it found `"#list"`. // Regression: r#"..."# used to close prematurely when it found `"#list"`.
let p = Props::new("hx-target", "#list"); let p = Props::new("hx-target", "#list");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (p.unpack(&cx)) {} }.into_string(), html! { button (p.unpack(&mut cx)) {} }.into_string(),
r##"<button hx-target="#list"></button>"## r##"<button hx-target="#list"></button>"##
); );
} }
@ -331,9 +331,9 @@ async fn props_hx_target_value_with_hash_renders_correctly() {
#[pagetop::test] #[pagetop::test]
async fn props_with_empty_value_renders_attr_with_empty_value() { async fn props_with_empty_value_renders_attr_with_empty_value() {
let p = Props::new("data-expanded", ""); let p = Props::new("data-expanded", "");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(), html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span data-expanded=""></span>"# r#"<span data-expanded=""></span>"#
); );
} }
@ -348,9 +348,9 @@ async fn props_chained_set_and_remove_yields_expected_state() {
assert_eq!(p.get_prop("a"), Some("updated".to_string())); assert_eq!(p.get_prop("a"), Some("updated".to_string()));
assert_eq!(p.get_prop("b"), None); assert_eq!(p.get_prop("b"), None);
assert_eq!(p.get_prop("c"), Some("3".to_string())); assert_eq!(p.get_prop("c"), Some("3".to_string()));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(), html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span a="updated" c="3"></span>"# r#"<span a="updated" c="3"></span>"#
); );
} }
@ -359,9 +359,9 @@ async fn props_chained_set_and_remove_yields_expected_state() {
async fn props_with_empty_attr_name_renders_without_validation() { async fn props_with_empty_attr_name_renders_without_validation() {
// Documented behavior: names are not validated; the resulting HTML is not standard. // Documented behavior: names are not validated; the resulting HTML is not standard.
let p = Props::new("", "val"); let p = Props::new("", "val");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(), html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span ="val"></span>"# r#"<span ="val"></span>"#
); );
} }

View file

@ -299,9 +299,9 @@ async fn get_prop_class_matches_get_classes() {
#[pagetop::test] #[pagetop::test]
async fn props_classes_renders_class_attribute() { async fn props_classes_renders_class_attribute() {
let p = Props::classes("btn btn-primary"); let p = Props::classes("btn btn-primary");
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (p.unpack(&cx)) { "OK" } }.into_string(), html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button class="btn btn-primary">OK</button>"# r#"<button class="btn btn-primary">OK</button>"#
); );
} }
@ -309,9 +309,9 @@ async fn props_classes_renders_class_attribute() {
#[pagetop::test] #[pagetop::test]
async fn props_classes_can_be_extended_with_add_classes() { async fn props_classes_can_be_extended_with_add_classes() {
let p = Props::classes("btn").with_prop(PropsOp::add_classes("active")); let p = Props::classes("btn").with_prop(PropsOp::add_classes("active"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (p.unpack(&cx)) { "OK" } }.into_string(), html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button class="btn active">OK</button>"# r#"<button class="btn active">OK</button>"#
); );
} }

View file

@ -113,9 +113,9 @@ async fn extras_not_emitted_in_html() {
let props = Props::default() let props = Props::default()
.with_prop(PropsOp::set_extra("ext.flag", true)) .with_prop(PropsOp::set_extra("ext.flag", true))
.with_prop(PropsOp::add_classes("btn")); .with_prop(PropsOp::add_classes("btn"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (props.unpack(&cx)) { "OK" } }.into_string(), html! { button (props.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button class="btn">OK</button>"# r#"<button class="btn">OK</button>"#
); );
} }

View file

@ -2,93 +2,121 @@ use pagetop::prelude::*;
#[pagetop::test] #[pagetop::test]
async fn default_flex_item_adds_nothing() { async fn default_flex_item_adds_nothing() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(FlexItem::new())); let props = Props::default().with_prop(PropsOp::flex_item(FlexItem::new()));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
assert_eq!(props.get_classes(), None); assert_eq!(html, "<span></span>");
assert_eq!(props.get_styles(), None); assert!(cx.render_assets().into_string().is_empty());
} }
#[pagetop::test] #[pagetop::test]
async fn grow_adds_flex_grow_style() { async fn grow_adds_flex_grow_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item( let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_grow(flex::ItemGrow::Is1), FlexItem::new().with_grow(flex::ItemGrow::Is1),
)); ));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("flex-grow: 1".to_string())); assert!(html.contains(r#"class="_flex-item-grow_1_""#));
assert!(assets.contains("_flex-item-grow_1_{flex-grow:1}"));
} }
#[pagetop::test] #[pagetop::test]
async fn shrink_adds_flex_shrink_style() { async fn shrink_adds_flex_shrink_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item( let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_shrink(flex::ItemShrink::Is0), FlexItem::new().with_shrink(flex::ItemShrink::Is0),
)); ));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("flex-shrink: 0".to_string())); assert!(html.contains(r#"class="_flex-item-shrink_0_""#));
assert!(assets.contains("_flex-item-shrink_0_{flex-shrink:0}"));
} }
#[pagetop::test] #[pagetop::test]
async fn align_self_adds_matching_style() { async fn align_self_adds_matching_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item( let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_align_self(flex::ItemAlign::Center), FlexItem::new().with_align_self(flex::ItemAlign::Center),
)); ));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("align-self: center".to_string())); assert!(html.contains(r#"class="_flex-item-align_center_""#));
assert!(assets.contains("_flex-item-align_center_{align-self:center}"));
} }
#[pagetop::test] #[pagetop::test]
async fn order_adds_matching_style() { async fn order_adds_matching_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item( let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_order(flex::ItemOrder::First), FlexItem::new().with_order(flex::ItemOrder::First),
)); ));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("order: -129".to_string())); assert!(html.contains(r#"class="_flex-item-order_-129_""#));
assert!(assets.contains("_flex-item-order_-129_{order:-129}"));
} }
#[pagetop::test] #[pagetop::test]
async fn size_percent_adds_flex_basis_style() { async fn size_percent_adds_flex_basis_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item( let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_size(flex::ItemSize::Percent33), FlexItem::new().with_size(flex::ItemSize::Percent33),
)); ));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("flex-basis: 33.3333%".to_string())); assert!(html.contains(r#"class="_flex-item-basis_33_3333pct_""#));
assert!(assets.contains("_flex-item-basis_33_3333pct_{flex-basis:33.3333%}"));
} }
#[pagetop::test] #[pagetop::test]
async fn size_custom_adds_flex_basis_style() { async fn size_custom_adds_flex_basis_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item( let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::Zero)), FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::Zero)),
)); ));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_classes(), None); assert!(html.contains(r#"class="_flex-item-basis_0_""#));
assert_eq!(props.get_styles(), Some("flex-basis: 0".to_string())); assert!(assets.contains("_flex-item-basis_0_{flex-basis:0}"));
} }
#[pagetop::test] #[pagetop::test]
async fn offset_percent_adds_margin_inline_start_style() { async fn offset_percent_adds_margin_inline_start_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item( let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_offset(flex::ItemOffset::Percent33), FlexItem::new().with_offset(flex::ItemOffset::Percent33),
)); ));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!( assert!(html.contains(r#"class="_flex-item-offset_33_3333pct_""#));
props.get_styles(), assert!(assets.contains("_flex-item-offset_33_3333pct_{margin-inline-start:33.3333%}"));
Some("margin-inline-start: 33.3333%".to_string())
);
} }
#[pagetop::test] #[pagetop::test]
async fn offset_custom_adds_margin_inline_start_style() { async fn offset_custom_adds_margin_inline_start_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item( let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_offset(flex::ItemOffset::Custom(UnitValue::Px(16))), FlexItem::new().with_offset(flex::ItemOffset::Custom(UnitValue::Px(16))),
)); ));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!( assert!(html.contains(r#"class="_flex-item-offset_16px_""#));
props.get_styles(), assert!(assets.contains("_flex-item-offset_16px_{margin-inline-start:16px}"));
Some("margin-inline-start: 16px".to_string())
);
} }
#[pagetop::test] #[pagetop::test]
async fn combines_several_facets_in_one_call() { async fn combines_several_facets_in_one_call() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item( let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new() FlexItem::new()
.with_grow(flex::ItemGrow::Is1) .with_grow(flex::ItemGrow::Is1)
@ -98,22 +126,31 @@ async fn combines_several_facets_in_one_call() {
.with_size(flex::ItemSize::Custom(UnitValue::Zero)) .with_size(flex::ItemSize::Custom(UnitValue::Zero))
.with_offset(flex::ItemOffset::Percent10), .with_offset(flex::ItemOffset::Percent10),
)); ));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!( assert!(html.contains("_flex-item-grow_1_"));
props.get_styles(), assert!(html.contains("_flex-item-shrink_0_"));
Some( assert!(html.contains("_flex-item-align_flex-start_"));
"flex-grow: 1; flex-shrink: 0; align-self: flex-start; order: 2; flex-basis: 0; \ assert!(html.contains("_flex-item-order_2_"));
margin-inline-start: 10%" assert!(html.contains("_flex-item-basis_0_"));
.to_string() assert!(html.contains("_flex-item-offset_10pct_"));
) assert!(assets.contains("_flex-item-grow_1_{flex-grow:1}"));
); assert!(assets.contains("_flex-item-shrink_0_{flex-shrink:0}"));
assert_eq!(props.get_classes(), None); assert!(assets.contains("_flex-item-align_flex-start_{align-self:flex-start}"));
assert!(assets.contains("_flex-item-order_2_{order:2}"));
assert!(assets.contains("_flex-item-basis_0_{flex-basis:0}"));
assert!(assets.contains("_flex-item-offset_10pct_{margin-inline-start:10%}"));
} }
#[pagetop::test] #[pagetop::test]
async fn from_flex_item_for_props_op() { async fn from_flex_item_for_props_op() {
let mut cx = Context::default();
let item = FlexItem::new().with_grow(flex::ItemGrow::Is1); let item = FlexItem::new().with_grow(flex::ItemGrow::Is1);
let props = Props::default().with_prop(item.into()); let props = Props::default().with_prop(item.into());
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("flex-grow: 1".to_string())); assert!(html.contains(r#"class="_flex-item-grow_1_""#));
assert!(assets.contains("_flex-item-grow_1_{flex-grow:1}"));
} }

View file

@ -275,9 +275,9 @@ async fn props_styles_renders_style_attribute() {
let p = Props::default() let p = Props::default()
.with_prop(PropsOp::add_style("color", "red")) .with_prop(PropsOp::add_style("color", "red"))
.with_prop(PropsOp::add_style("font-weight", "bold")); .with_prop(PropsOp::add_style("font-weight", "bold"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (p.unpack(&cx)) { "OK" } }.into_string(), html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button style="color: red; font-weight: bold">OK</button>"# r#"<button style="color: red; font-weight: bold">OK</button>"#
); );
} }
@ -289,9 +289,9 @@ async fn props_styles_render_after_class_and_before_other_attrs() {
.with_prop(PropsOp::add_classes("btn")) .with_prop(PropsOp::add_classes("btn"))
.with_prop(PropsOp::add_style("color", "red")) .with_prop(PropsOp::add_style("color", "red"))
.with_prop(PropsOp::set("data-x", "1")); .with_prop(PropsOp::set("data-x", "1"));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { button (p.unpack(&cx)) { "OK" } }.into_string(), html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button id="main" class="btn" style="color: red" data-x="1">OK</button>"# r#"<button id="main" class="btn" style="color: red" data-x="1">OK</button>"#
); );
} }
@ -299,9 +299,9 @@ async fn props_styles_render_after_class_and_before_other_attrs() {
#[pagetop::test] #[pagetop::test]
async fn props_styles_escapes_double_quotes_in_value() { async fn props_styles_escapes_double_quotes_in_value() {
let p = Props::default().with_prop(PropsOp::add_style("content", r#""hi""#)); let p = Props::default().with_prop(PropsOp::add_style("content", r#""hi""#));
let cx = Context::default(); let mut cx = Context::default();
assert_eq!( assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(), html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span style="content: &quot;hi&quot;"></span>"# r#"<span style="content: &quot;hi&quot;"></span>"#
); );
} }

View file

@ -256,7 +256,7 @@ async fn render_zero_min_width_breakpoint_has_no_media_query() {
let cx = Context::default(); let cx = Context::default();
let mut r = ResponsiveStyles::new(); let mut r = ResponsiveStyles::new();
r.add_style(Breakpoint::Xs, "col", "flex-basis", "100%"); r.add_style(Breakpoint::Xs, "col", "flex-basis", "100%");
assert_eq!(r.render(&cx).into_string(), ".col { flex-basis: 100% }"); assert_eq!(r.render(&cx).into_string(), ".col{flex-basis:100%}");
} }
#[pagetop::test] #[pagetop::test]
@ -264,7 +264,7 @@ async fn render_none_breakpoint_has_no_media_query() {
let cx = Context::default(); let cx = Context::default();
let mut r = ResponsiveStyles::new(); let mut r = ResponsiveStyles::new();
r.add_style(None, "col", "flex-basis", "100%"); r.add_style(None, "col", "flex-basis", "100%");
assert_eq!(r.render(&cx).into_string(), ".col { flex-basis: 100% }"); assert_eq!(r.render(&cx).into_string(), ".col{flex-basis:100%}");
} }
#[pagetop::test] #[pagetop::test]
@ -275,7 +275,7 @@ async fn render_none_comes_before_every_breakpoint() {
r.add_style(None, "row", "display", "flex"); r.add_style(None, "row", "display", "flex");
assert_eq!( assert_eq!(
r.render(&cx).into_string(), r.render(&cx).into_string(),
".row { display: flex }@media (min-width: 768px) { .col { flex-basis: 50% } }" ".row{display:flex}@media(min-width:768px){.col{flex-basis:50%}}"
); );
} }
@ -287,7 +287,7 @@ async fn render_non_zero_breakpoint_wraps_in_media_query() {
r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); r.add_style(Breakpoint::Md, "col", "flex-basis", "50%");
assert_eq!( assert_eq!(
r.render(&cx).into_string(), r.render(&cx).into_string(),
"@media (min-width: 768px) { .col { flex-basis: 50% } }" "@media(min-width:768px){.col{flex-basis:50%}}"
); );
} }
@ -299,7 +299,7 @@ async fn render_groups_multiple_properties_in_the_same_rule() {
r.add_style(Breakpoint::Md, "col", "margin-inline-start", "0"); r.add_style(Breakpoint::Md, "col", "margin-inline-start", "0");
assert_eq!( assert_eq!(
r.render(&cx).into_string(), r.render(&cx).into_string(),
"@media (min-width: 768px) { .col { flex-basis: 50%; margin-inline-start: 0 } }" "@media(min-width:768px){.col{flex-basis:50%;margin-inline-start:0}}"
); );
} }
@ -311,7 +311,7 @@ async fn render_concatenates_rules_of_different_selectors_in_the_same_breakpoint
r.add_style(Breakpoint::Md, "row", "display", "flex"); r.add_style(Breakpoint::Md, "row", "display", "flex");
assert_eq!( assert_eq!(
r.render(&cx).into_string(), r.render(&cx).into_string(),
"@media (min-width: 768px) { .col { flex-basis: 50% }.row { display: flex } }" "@media(min-width:768px){.col{flex-basis:50%}.row{display:flex}}"
); );
} }
@ -322,7 +322,7 @@ async fn render_converts_multiple_classes_into_a_compound_selector() {
r.add_style(Breakpoint::Md, "foo bar", "color", "red"); r.add_style(Breakpoint::Md, "foo bar", "color", "red");
assert_eq!( assert_eq!(
r.render(&cx).into_string(), r.render(&cx).into_string(),
"@media (min-width: 768px) { .foo.bar { color: red } }" "@media(min-width:768px){.foo.bar{color:red}}"
); );
} }
@ -335,9 +335,9 @@ async fn render_orders_breakpoints_mobile_first_regardless_of_insertion_order()
r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); r.add_style(Breakpoint::Md, "col", "flex-basis", "50%");
assert_eq!( assert_eq!(
r.render(&cx).into_string(), r.render(&cx).into_string(),
".col { flex-basis: 100% }\ ".col{flex-basis:100%}\
@media (min-width: 768px) { .col { flex-basis: 50% } }\ @media(min-width:768px){.col{flex-basis:50%}}\
@media (min-width: 992px) { .col { flex-basis: 33% } }" @media(min-width:992px){.col{flex-basis:33%}}"
); );
} }
@ -357,9 +357,9 @@ async fn render_has_no_line_breaks() {
async fn context_add_responsive_style_feeds_responsives() { async fn context_add_responsive_style_feeds_responsives() {
let cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle( let cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
Some(Breakpoint::Md), Some(Breakpoint::Md),
"col", "col".into(),
"flex-basis", "flex-basis".into(),
"50%", "50%".into(),
)); ));
assert_eq!( assert_eq!(
cx.responsive_styles().get_styles(Breakpoint::Md, "col"), cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
@ -372,15 +372,15 @@ async fn context_add_responsive_style_accumulates_across_calls() {
let cx = Context::default() let cx = Context::default()
.with_assets(AssetsOp::AddResponsiveStyle( .with_assets(AssetsOp::AddResponsiveStyle(
Some(Breakpoint::Md), Some(Breakpoint::Md),
"col", "col".into(),
"flex-basis", "flex-basis".into(),
"50%", "50%".into(),
)) ))
.with_assets(AssetsOp::AddResponsiveStyle( .with_assets(AssetsOp::AddResponsiveStyle(
Some(Breakpoint::Md), Some(Breakpoint::Md),
"col", "col".into(),
"margin-inline-start", "margin-inline-start".into(),
"0", "0".into(),
)); ));
assert_eq!( assert_eq!(
cx.responsive_styles().get_styles(Breakpoint::Md, "col"), cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
@ -399,13 +399,13 @@ async fn context_default_has_no_responsive_styles() {
async fn render_assets_includes_style_tag_with_responsive_styles() { async fn render_assets_includes_style_tag_with_responsive_styles() {
let mut cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle( let mut cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
Some(Breakpoint::Xs), Some(Breakpoint::Xs),
"col", "col".into(),
"flex-basis", "flex-basis".into(),
"100%", "100%".into(),
)); ));
assert_eq!( assert_eq!(
cx.render_assets().into_string(), cx.render_assets().into_string(),
"<style>.col { flex-basis: 100% }</style>" "<style>.col{flex-basis:100%}</style>"
); );
} }