diff --git a/src/html.rs b/src/html.rs
index 5f5b833..82fdcd7 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -104,11 +104,11 @@ pub use unit::UnitValue;
/// # use pagetop::prelude::*;
/// // Texto normal, se escapa automáticamente para evitar inyección de HTML.
/// let fragment = PrepareMarkup::Escaped("Hola mundo".to_string());
-/// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>");
+/// assert_eq!(fragment.into_string(), "Hola <b>mundo</b>");
///
/// // HTML literal, se inserta directamente, sin escapado adicional.
/// let raw_html = PrepareMarkup::Raw("negrita".to_string());
-/// assert_eq!(raw_html.render().into_string(), "negrita");
+/// assert_eq!(raw_html.into_string(), "negrita");
///
/// // Fragmento ya preparado con la macro `html!`.
/// let prepared = PrepareMarkup::With(html! {
@@ -116,11 +116,11 @@ pub use unit::UnitValue;
/// p { "Este es un párrafo con contenido dinámico." }
/// });
/// assert_eq!(
-/// prepared.render().into_string(),
+/// prepared.into_string(),
/// "
Título de ejemplo
Este es un párrafo con contenido dinámico.
"
/// );
/// ```
-#[derive(AutoDefault)]
+#[derive(AutoDefault, Clone)]
pub enum PrepareMarkup {
/// No se genera contenido HTML (equivale a `html! {}`).
#[default]
@@ -152,8 +152,13 @@ impl PrepareMarkup {
}
}
- /// Integra el renderizado fácilmente en la macro [`html!`].
- pub fn render(&self) -> Markup {
+ /// Convierte el contenido en una cadena HTML renderizada. Usar sólo para pruebas o depuración.
+ pub fn into_string(&self) -> String {
+ self.render().into_string()
+ }
+
+ // Integra el renderizado fácilmente en la macro [`html!`].
+ pub(crate) fn render(&self) -> Markup {
match self {
PrepareMarkup::None => html! {},
PrepareMarkup::Escaped(text) => html! { (text) },
diff --git a/tests/component_html.rs b/tests/component_html.rs
index 851315a..06d77ec 100644
--- a/tests/component_html.rs
+++ b/tests/component_html.rs
@@ -2,32 +2,28 @@ use pagetop::prelude::*;
#[pagetop::test]
async fn component_html_renders_static_markup() {
- let component = Html::with(|_| {
+ let mut component = Html::with(|_| {
html! {
p { "Test" }
}
});
- let markup = component
- .prepare_component(&mut Context::new(None))
- .render();
-
+ let markup = component.render(&mut Context::default());
assert_eq!(markup.0, "Test
");
}
#[pagetop::test]
async fn component_html_renders_using_context_param() {
- let mut cx = Context::new(None).with_param("username", "Alice".to_string());
+ let mut cx = Context::default().with_param("username", "Alice".to_string());
- let component = Html::with(|cx| {
+ let mut component = Html::with(|cx| {
let name = cx.param::("username").cloned().unwrap_or_default();
html! {
span { (name) }
}
});
- let markup = component.prepare_component(&mut cx).render();
-
+ let markup = component.render(&mut cx);
assert_eq!(markup.0, "Alice");
}
@@ -37,21 +33,15 @@ async fn component_html_allows_replacing_render_function() {
component.alter_fn(|_| html! { div { "Modified" } });
- let markup = component
- .prepare_component(&mut Context::new(None))
- .render();
-
+ let markup = component.render(&mut Context::default());
assert_eq!(markup.0, "Modified
");
}
#[pagetop::test]
async fn component_html_default_renders_empty_markup() {
- let component = Html::default();
-
- let markup = component
- .prepare_component(&mut Context::new(None))
- .render();
+ let mut component = Html::default();
+ let markup = component.render(&mut Context::default());
assert_eq!(markup.0, "");
}
@@ -60,7 +50,7 @@ async fn component_html_can_access_http_method() {
let req = service::test::TestRequest::with_uri("/").to_http_request();
let mut cx = Context::new(Some(req));
- let component = Html::with(|cx| {
+ let mut component = Html::with(|cx| {
let method = cx
.request()
.map(|r| r.method().to_string())
@@ -68,7 +58,6 @@ async fn component_html_can_access_http_method() {
html! { span { (method) } }
});
- let markup = component.prepare_component(&mut cx).render();
-
+ let markup = component.render(&mut cx);
assert_eq!(markup.0, "GET");
}
diff --git a/tests/component_poweredby.rs b/tests/component_poweredby.rs
index 27683d9..7e5a062 100644
--- a/tests/component_poweredby.rs
+++ b/tests/component_poweredby.rs
@@ -4,8 +4,8 @@ use pagetop::prelude::*;
async fn poweredby_default_shows_only_pagetop_recognition() {
let _app = service::test::init_service(Application::new().test()).await;
- let p = PoweredBy::default();
- let html = render_component(&p);
+ let mut p = PoweredBy::default();
+ let html = p.render(&mut Context::default());
// Debe mostrar el bloque de reconocimiento a PageTop.
assert!(html.as_str().contains("poweredby__pagetop"));
@@ -18,8 +18,8 @@ async fn poweredby_default_shows_only_pagetop_recognition() {
async fn poweredby_new_includes_current_year_and_app_name() {
let _app = service::test::init_service(Application::new().test()).await;
- let p = PoweredBy::new();
- let html = render_component(&p);
+ let mut p = PoweredBy::new();
+ let html = p.render(&mut Context::default());
let year = Utc::now().format("%Y").to_string();
assert!(
@@ -43,8 +43,8 @@ async fn poweredby_with_copyright_overrides_text() {
let _app = service::test::init_service(Application::new().test()).await;
let custom = "2001 © FooBar Inc.";
- let p = PoweredBy::default().with_copyright(Some(custom));
- let html = render_component(&p);
+ let mut p = PoweredBy::default().with_copyright(Some(custom));
+ let html = p.render(&mut Context::default());
assert!(html.as_str().contains(custom));
assert!(html.as_str().contains("poweredby__copyright"));
@@ -54,8 +54,8 @@ async fn poweredby_with_copyright_overrides_text() {
async fn poweredby_with_copyright_none_hides_text() {
let _app = service::test::init_service(Application::new().test()).await;
- let p = PoweredBy::new().with_copyright(None::);
- let html = render_component(&p);
+ let mut p = PoweredBy::new().with_copyright(None::);
+ let html = p.render(&mut Context::default());
assert!(!html.as_str().contains("poweredby__copyright"));
// El reconocimiento a PageTop siempre debe aparecer.
@@ -66,8 +66,8 @@ async fn poweredby_with_copyright_none_hides_text() {
async fn poweredby_link_points_to_crates_io() {
let _app = service::test::init_service(Application::new().test()).await;
- let p = PoweredBy::default();
- let html = render_component(&p);
+ let mut p = PoweredBy::default();
+ let html = p.render(&mut Context::default());
assert!(
html.as_str().contains("https://pagetop.cillero.es"),
@@ -89,11 +89,3 @@ async fn poweredby_getter_reflects_internal_state() {
assert!(c1.contains(&Utc::now().format("%Y").to_string()));
assert!(c1.contains(&global::SETTINGS.app.name));
}
-
-// **< HELPERS >************************************************************************************
-
-fn render_component(c: &C) -> Markup {
- let mut cx = Context::default();
- let pm = c.prepare_component(&mut cx);
- pm.render()
-}
diff --git a/tests/html_pm.rs b/tests/html_pm.rs
index ae4517b..615ea47 100644
--- a/tests/html_pm.rs
+++ b/tests/html_pm.rs
@@ -1,70 +1,69 @@
use pagetop::prelude::*;
-#[pagetop::test]
-async fn prepare_markup_render_none_is_empty_string() {
- assert_eq!(PrepareMarkup::None.render().as_str(), "");
+/// Componente mínimo para probar `PrepareMarkup` pasando por el ciclo real
+/// de renderizado de componentes (`ComponentRender`).
+#[derive(AutoDefault)]
+struct TestPrepareComponent {
+ pm: PrepareMarkup,
+}
+
+impl Component for TestPrepareComponent {
+ fn new() -> Self {
+ Self {
+ pm: PrepareMarkup::None,
+ }
+ }
+
+ fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
+ self.pm.clone()
+ }
+}
+
+impl TestPrepareComponent {
+ fn render_pm(pm: PrepareMarkup) -> String {
+ let mut c = TestPrepareComponent { pm };
+ c.render(&mut Context::default()).into_string()
+ }
}
#[pagetop::test]
-async fn prepare_markup_render_escaped_escapes_html_and_ampersands() {
+async fn prepare_markup_none_is_empty_string() {
+ assert_eq!(PrepareMarkup::None.into_string(), "");
+}
+
+#[pagetop::test]
+async fn prepare_markup_escaped_escapes_html_and_ampersands() {
let pm = PrepareMarkup::Escaped("& \" ' ".to_string());
- assert_eq!(pm.render().as_str(), "<b>& " ' </b>");
+ assert_eq!(pm.into_string(), "<b>& " ' </b>");
}
#[pagetop::test]
-async fn prepare_markup_render_raw_is_inserted_verbatim() {
+async fn prepare_markup_raw_is_inserted_verbatim() {
let pm = PrepareMarkup::Raw("bold".to_string());
- assert_eq!(pm.render().as_str(), "bold");
+ assert_eq!(pm.into_string(), "bold");
}
#[pagetop::test]
-async fn prepare_markup_render_with_keeps_structure() {
+async fn prepare_markup_with_keeps_structure() {
let pm = PrepareMarkup::With(html! {
h2 { "Sample title" }
- p { "This is a paragraph." }
+ p { "This is a paragraph." }
});
assert_eq!(
- pm.render().as_str(),
+ pm.into_string(),
"Sample title
This is a paragraph.
"
);
}
-#[pagetop::test]
-async fn prepare_markup_does_not_double_escape_when_wrapped_in_html_macro() {
- // Escaped: dentro de `html!` no debe volver a escaparse.
- let escaped = PrepareMarkup::Escaped("x".into());
- let wrapped_escaped = html! { div { (escaped.render()) } };
- assert_eq!(
- wrapped_escaped.into_string(),
- "<i>x</i>
"
- );
-
- // Raw: tampoco debe escaparse al integrarlo.
- let raw = PrepareMarkup::Raw("x".into());
- let wrapped_raw = html! { div { (raw.render()) } };
- assert_eq!(wrapped_raw.into_string(), "x
");
-
- // With: debe incrustar el Markup tal cual.
- let with = PrepareMarkup::With(html! { span.title { "ok" } });
- let wrapped_with = html! { div { (with.render()) } };
- assert_eq!(
- wrapped_with.into_string(),
- "ok
"
- );
-}
-
#[pagetop::test]
async fn prepare_markup_unicode_is_preserved() {
// Texto con acentos y emojis debe conservarse (salvo el escape HTML de signos).
let esc = PrepareMarkup::Escaped("Hello, tomorrow coffee ☕ & donuts!".into());
- assert_eq!(
- esc.render().as_str(),
- "Hello, tomorrow coffee ☕ & donuts!"
- );
+ assert_eq!(esc.into_string(), "Hello, tomorrow coffee ☕ & donuts!");
// Raw debe pasar íntegro.
let raw = PrepareMarkup::Raw("Title — section © 2025".into());
- assert_eq!(raw.render().as_str(), "Title — section © 2025");
+ assert_eq!(raw.into_string(), "Title — section © 2025");
}
#[pagetop::test]
@@ -88,7 +87,36 @@ async fn prepare_markup_is_empty_semantics() {
}
#[pagetop::test]
-async fn prepare_markup_equivalence_between_render_and_inline_in_html_macro() {
+async fn prepare_markup_does_not_double_escape_when_markup_is_reinjected_in_html_macro() {
+ let mut cx = Context::default();
+
+ // Escaped: dentro de `html!` no debe volver a escaparse.
+ let mut comp = TestPrepareComponent {
+ pm: PrepareMarkup::Escaped("x".into()),
+ };
+ let markup = comp.render(&mut cx); // Markup
+ let wrapped_escaped = html! { div { (markup) } }.into_string();
+ assert_eq!(wrapped_escaped, "<i>x</i>
");
+
+ // Raw: tampoco debe escaparse al integrarlo.
+ let mut comp = TestPrepareComponent {
+ pm: PrepareMarkup::Raw("x".into()),
+ };
+ let markup = comp.render(&mut cx);
+ let wrapped_raw = html! { div { (markup) } }.into_string();
+ assert_eq!(wrapped_raw, "x
");
+
+ // With: debe incrustar el Markup tal cual.
+ let mut comp = TestPrepareComponent {
+ pm: PrepareMarkup::With(html! { span.title { "ok" } }),
+ };
+ let markup = comp.render(&mut cx);
+ let wrapped_with = html! { div { (markup) } }.into_string();
+ assert_eq!(wrapped_with, "ok
");
+}
+
+#[pagetop::test]
+async fn prepare_markup_equivalence_between_component_render_and_markup_reinjected_in_html_macro() {
let cases = [
PrepareMarkup::None,
PrepareMarkup::Escaped("x".into()),
@@ -97,12 +125,20 @@ async fn prepare_markup_equivalence_between_render_and_inline_in_html_macro() {
];
for pm in cases {
- let rendered = pm.render();
- let in_macro = html! { (rendered) }.into_string();
+ // Vía 1: renderizamos y obtenemos directamente el String.
+ let via_component = TestPrepareComponent::render_pm(pm.clone());
+
+ // Vía 2: renderizamos, reinyectamos el Markup en `html!` y volvemos a obtener String.
+ let via_macro = {
+ let mut cx = Context::default();
+ let mut comp = TestPrepareComponent { pm };
+ let markup = comp.render(&mut cx);
+ html! { (markup) }.into_string()
+ };
+
assert_eq!(
- rendered.as_str(),
- in_macro,
- "The output of Render and (pm) inside html! must match"
+ via_component, via_macro,
+ "The output of component render and (Markup) inside html! must match"
);
}
}