WIP: Simplifica gestión de regiones y plantillas en los temas #9
4 changed files with 112 additions and 90 deletions
17
src/html.rs
17
src/html.rs
|
|
@ -104,11 +104,11 @@ pub use unit::UnitValue;
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// // Texto normal, se escapa automáticamente para evitar inyección de HTML.
|
/// // Texto normal, se escapa automáticamente para evitar inyección de HTML.
|
||||||
/// let fragment = PrepareMarkup::Escaped("Hola <b>mundo</b>".to_string());
|
/// let fragment = PrepareMarkup::Escaped("Hola <b>mundo</b>".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.
|
/// // HTML literal, se inserta directamente, sin escapado adicional.
|
||||||
/// let raw_html = PrepareMarkup::Raw("<b>negrita</b>".to_string());
|
/// let raw_html = PrepareMarkup::Raw("<b>negrita</b>".to_string());
|
||||||
/// assert_eq!(raw_html.render().into_string(), "<b>negrita</b>");
|
/// assert_eq!(raw_html.into_string(), "<b>negrita</b>");
|
||||||
///
|
///
|
||||||
/// // Fragmento ya preparado con la macro `html!`.
|
/// // Fragmento ya preparado con la macro `html!`.
|
||||||
/// let prepared = PrepareMarkup::With(html! {
|
/// let prepared = PrepareMarkup::With(html! {
|
||||||
|
|
@ -116,11 +116,11 @@ pub use unit::UnitValue;
|
||||||
/// p { "Este es un párrafo con contenido dinámico." }
|
/// p { "Este es un párrafo con contenido dinámico." }
|
||||||
/// });
|
/// });
|
||||||
/// assert_eq!(
|
/// assert_eq!(
|
||||||
/// prepared.render().into_string(),
|
/// prepared.into_string(),
|
||||||
/// "<h2>Título de ejemplo</h2><p>Este es un párrafo con contenido dinámico.</p>"
|
/// "<h2>Título de ejemplo</h2><p>Este es un párrafo con contenido dinámico.</p>"
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault, Clone)]
|
||||||
pub enum PrepareMarkup {
|
pub enum PrepareMarkup {
|
||||||
/// No se genera contenido HTML (equivale a `html! {}`).
|
/// No se genera contenido HTML (equivale a `html! {}`).
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -152,8 +152,13 @@ impl PrepareMarkup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Integra el renderizado fácilmente en la macro [`html!`].
|
/// Convierte el contenido en una cadena HTML renderizada. Usar sólo para pruebas o depuración.
|
||||||
pub fn render(&self) -> Markup {
|
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 {
|
match self {
|
||||||
PrepareMarkup::None => html! {},
|
PrepareMarkup::None => html! {},
|
||||||
PrepareMarkup::Escaped(text) => html! { (text) },
|
PrepareMarkup::Escaped(text) => html! { (text) },
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,28 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn component_html_renders_static_markup() {
|
async fn component_html_renders_static_markup() {
|
||||||
let component = Html::with(|_| {
|
let mut component = Html::with(|_| {
|
||||||
html! {
|
html! {
|
||||||
p { "Test" }
|
p { "Test" }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let markup = component
|
let markup = component.render(&mut Context::default());
|
||||||
.prepare_component(&mut Context::new(None))
|
|
||||||
.render();
|
|
||||||
|
|
||||||
assert_eq!(markup.0, "<p>Test</p>");
|
assert_eq!(markup.0, "<p>Test</p>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn component_html_renders_using_context_param() {
|
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::<String>("username").cloned().unwrap_or_default();
|
let name = cx.param::<String>("username").cloned().unwrap_or_default();
|
||||||
html! {
|
html! {
|
||||||
span { (name) }
|
span { (name) }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let markup = component.prepare_component(&mut cx).render();
|
let markup = component.render(&mut cx);
|
||||||
|
|
||||||
assert_eq!(markup.0, "<span>Alice</span>");
|
assert_eq!(markup.0, "<span>Alice</span>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,21 +33,15 @@ async fn component_html_allows_replacing_render_function() {
|
||||||
|
|
||||||
component.alter_fn(|_| html! { div { "Modified" } });
|
component.alter_fn(|_| html! { div { "Modified" } });
|
||||||
|
|
||||||
let markup = component
|
let markup = component.render(&mut Context::default());
|
||||||
.prepare_component(&mut Context::new(None))
|
|
||||||
.render();
|
|
||||||
|
|
||||||
assert_eq!(markup.0, "<div>Modified</div>");
|
assert_eq!(markup.0, "<div>Modified</div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn component_html_default_renders_empty_markup() {
|
async fn component_html_default_renders_empty_markup() {
|
||||||
let component = Html::default();
|
let mut component = Html::default();
|
||||||
|
|
||||||
let markup = component
|
|
||||||
.prepare_component(&mut Context::new(None))
|
|
||||||
.render();
|
|
||||||
|
|
||||||
|
let markup = component.render(&mut Context::default());
|
||||||
assert_eq!(markup.0, "");
|
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 req = service::test::TestRequest::with_uri("/").to_http_request();
|
||||||
let mut cx = Context::new(Some(req));
|
let mut cx = Context::new(Some(req));
|
||||||
|
|
||||||
let component = Html::with(|cx| {
|
let mut component = Html::with(|cx| {
|
||||||
let method = cx
|
let method = cx
|
||||||
.request()
|
.request()
|
||||||
.map(|r| r.method().to_string())
|
.map(|r| r.method().to_string())
|
||||||
|
|
@ -68,7 +58,6 @@ async fn component_html_can_access_http_method() {
|
||||||
html! { span { (method) } }
|
html! { span { (method) } }
|
||||||
});
|
});
|
||||||
|
|
||||||
let markup = component.prepare_component(&mut cx).render();
|
let markup = component.render(&mut cx);
|
||||||
|
|
||||||
assert_eq!(markup.0, "<span>GET</span>");
|
assert_eq!(markup.0, "<span>GET</span>");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use pagetop::prelude::*;
|
||||||
async fn poweredby_default_shows_only_pagetop_recognition() {
|
async fn poweredby_default_shows_only_pagetop_recognition() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
let p = PoweredBy::default();
|
let mut p = PoweredBy::default();
|
||||||
let html = render_component(&p);
|
let html = p.render(&mut Context::default());
|
||||||
|
|
||||||
// Debe mostrar el bloque de reconocimiento a PageTop.
|
// Debe mostrar el bloque de reconocimiento a PageTop.
|
||||||
assert!(html.as_str().contains("poweredby__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() {
|
async fn poweredby_new_includes_current_year_and_app_name() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
let p = PoweredBy::new();
|
let mut p = PoweredBy::new();
|
||||||
let html = render_component(&p);
|
let html = p.render(&mut Context::default());
|
||||||
|
|
||||||
let year = Utc::now().format("%Y").to_string();
|
let year = Utc::now().format("%Y").to_string();
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -43,8 +43,8 @@ async fn poweredby_with_copyright_overrides_text() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
let custom = "2001 © FooBar Inc.";
|
let custom = "2001 © FooBar Inc.";
|
||||||
let p = PoweredBy::default().with_copyright(Some(custom));
|
let mut p = PoweredBy::default().with_copyright(Some(custom));
|
||||||
let html = render_component(&p);
|
let html = p.render(&mut Context::default());
|
||||||
|
|
||||||
assert!(html.as_str().contains(custom));
|
assert!(html.as_str().contains(custom));
|
||||||
assert!(html.as_str().contains("poweredby__copyright"));
|
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() {
|
async fn poweredby_with_copyright_none_hides_text() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
let p = PoweredBy::new().with_copyright(None::<String>);
|
let mut p = PoweredBy::new().with_copyright(None::<String>);
|
||||||
let html = render_component(&p);
|
let html = p.render(&mut Context::default());
|
||||||
|
|
||||||
assert!(!html.as_str().contains("poweredby__copyright"));
|
assert!(!html.as_str().contains("poweredby__copyright"));
|
||||||
// El reconocimiento a PageTop siempre debe aparecer.
|
// 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() {
|
async fn poweredby_link_points_to_crates_io() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
let p = PoweredBy::default();
|
let mut p = PoweredBy::default();
|
||||||
let html = render_component(&p);
|
let html = p.render(&mut Context::default());
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
html.as_str().contains("https://pagetop.cillero.es"),
|
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(&Utc::now().format("%Y").to_string()));
|
||||||
assert!(c1.contains(&global::SETTINGS.app.name));
|
assert!(c1.contains(&global::SETTINGS.app.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< HELPERS >************************************************************************************
|
|
||||||
|
|
||||||
fn render_component<C: Component>(c: &C) -> Markup {
|
|
||||||
let mut cx = Context::default();
|
|
||||||
let pm = c.prepare_component(&mut cx);
|
|
||||||
pm.render()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
124
tests/html_pm.rs
124
tests/html_pm.rs
|
|
@ -1,70 +1,69 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
#[pagetop::test]
|
/// Componente mínimo para probar `PrepareMarkup` pasando por el ciclo real
|
||||||
async fn prepare_markup_render_none_is_empty_string() {
|
/// de renderizado de componentes (`ComponentRender`).
|
||||||
assert_eq!(PrepareMarkup::None.render().as_str(), "");
|
#[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]
|
#[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("<b>& \" ' </b>".to_string());
|
let pm = PrepareMarkup::Escaped("<b>& \" ' </b>".to_string());
|
||||||
assert_eq!(pm.render().as_str(), "<b>& " ' </b>");
|
assert_eq!(pm.into_string(), "<b>& " ' </b>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn prepare_markup_render_raw_is_inserted_verbatim() {
|
async fn prepare_markup_raw_is_inserted_verbatim() {
|
||||||
let pm = PrepareMarkup::Raw("<b>bold</b><script>1<2</script>".to_string());
|
let pm = PrepareMarkup::Raw("<b>bold</b><script>1<2</script>".to_string());
|
||||||
assert_eq!(pm.render().as_str(), "<b>bold</b><script>1<2</script>");
|
assert_eq!(pm.into_string(), "<b>bold</b><script>1<2</script>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn prepare_markup_render_with_keeps_structure() {
|
async fn prepare_markup_with_keeps_structure() {
|
||||||
let pm = PrepareMarkup::With(html! {
|
let pm = PrepareMarkup::With(html! {
|
||||||
h2 { "Sample title" }
|
h2 { "Sample title" }
|
||||||
p { "This is a paragraph." }
|
p { "This is a paragraph." }
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
pm.render().as_str(),
|
pm.into_string(),
|
||||||
"<h2>Sample title</h2><p>This is a paragraph.</p>"
|
"<h2>Sample title</h2><p>This is a paragraph.</p>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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("<i>x</i>".into());
|
|
||||||
let wrapped_escaped = html! { div { (escaped.render()) } };
|
|
||||||
assert_eq!(
|
|
||||||
wrapped_escaped.into_string(),
|
|
||||||
"<div><i>x</i></div>"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Raw: tampoco debe escaparse al integrarlo.
|
|
||||||
let raw = PrepareMarkup::Raw("<i>x</i>".into());
|
|
||||||
let wrapped_raw = html! { div { (raw.render()) } };
|
|
||||||
assert_eq!(wrapped_raw.into_string(), "<div><i>x</i></div>");
|
|
||||||
|
|
||||||
// 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(),
|
|
||||||
"<div><span class=\"title\">ok</span></div>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn prepare_markup_unicode_is_preserved() {
|
async fn prepare_markup_unicode_is_preserved() {
|
||||||
// Texto con acentos y emojis debe conservarse (salvo el escape HTML de signos).
|
// Texto con acentos y emojis debe conservarse (salvo el escape HTML de signos).
|
||||||
let esc = PrepareMarkup::Escaped("Hello, tomorrow coffee ☕ & donuts!".into());
|
let esc = PrepareMarkup::Escaped("Hello, tomorrow coffee ☕ & donuts!".into());
|
||||||
assert_eq!(
|
assert_eq!(esc.into_string(), "Hello, tomorrow coffee ☕ & donuts!");
|
||||||
esc.render().as_str(),
|
|
||||||
"Hello, tomorrow coffee ☕ & donuts!"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Raw debe pasar íntegro.
|
// Raw debe pasar íntegro.
|
||||||
let raw = PrepareMarkup::Raw("Title — section © 2025".into());
|
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]
|
#[pagetop::test]
|
||||||
|
|
@ -88,7 +87,36 @@ async fn prepare_markup_is_empty_semantics() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[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("<i>x</i>".into()),
|
||||||
|
};
|
||||||
|
let markup = comp.render(&mut cx); // Markup
|
||||||
|
let wrapped_escaped = html! { div { (markup) } }.into_string();
|
||||||
|
assert_eq!(wrapped_escaped, "<div><i>x</i></div>");
|
||||||
|
|
||||||
|
// Raw: tampoco debe escaparse al integrarlo.
|
||||||
|
let mut comp = TestPrepareComponent {
|
||||||
|
pm: PrepareMarkup::Raw("<i>x</i>".into()),
|
||||||
|
};
|
||||||
|
let markup = comp.render(&mut cx);
|
||||||
|
let wrapped_raw = html! { div { (markup) } }.into_string();
|
||||||
|
assert_eq!(wrapped_raw, "<div><i>x</i></div>");
|
||||||
|
|
||||||
|
// 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, "<div><span class=\"title\">ok</span></div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn prepare_markup_equivalence_between_component_render_and_markup_reinjected_in_html_macro() {
|
||||||
let cases = [
|
let cases = [
|
||||||
PrepareMarkup::None,
|
PrepareMarkup::None,
|
||||||
PrepareMarkup::Escaped("<b>x</b>".into()),
|
PrepareMarkup::Escaped("<b>x</b>".into()),
|
||||||
|
|
@ -97,12 +125,20 @@ async fn prepare_markup_equivalence_between_render_and_inline_in_html_macro() {
|
||||||
];
|
];
|
||||||
|
|
||||||
for pm in cases {
|
for pm in cases {
|
||||||
let rendered = pm.render();
|
// Vía 1: renderizamos y obtenemos directamente el String.
|
||||||
let in_macro = html! { (rendered) }.into_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!(
|
assert_eq!(
|
||||||
rendered.as_str(),
|
via_component, via_macro,
|
||||||
in_macro,
|
"The output of component render and (Markup) inside html! must match"
|
||||||
"The output of Render and (pm) inside html! must match"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue