From 682ed7cc45357b6aef993b45b28c67f103a44e55 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 17 Nov 2025 22:47:47 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8E=A8=20Protege=20el=20uso=20de=20`r?= =?UTF-8?q?ender`=20en=20PrepareMarkup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/html.rs | 17 +++-- tests/component_html.rs | 31 +++------ tests/component_poweredby.rs | 28 +++----- tests/html_pm.rs | 126 ++++++++++++++++++++++------------- 4 files changed, 112 insertions(+), 90 deletions(-) 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" ); } } -- 2.47.2 From dea994e8ca7e8ed192c54428cae54871c28c52ea Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 17 Nov 2025 22:50:56 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20Incorpora=20`is=5Frenderable`?= =?UTF-8?q?=20en=20`Component`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base/action/component.rs | 3 - src/base/action/component/is_renderable.rs | 96 ---------------------- src/core/component.rs | 53 ++++++++++++ src/core/component/definition.rs | 36 +++++--- 4 files changed, 78 insertions(+), 110 deletions(-) delete mode 100644 src/base/action/component/is_renderable.rs diff --git a/src/base/action/component.rs b/src/base/action/component.rs index aaef1ce..30c7ba4 100644 --- a/src/base/action/component.rs +++ b/src/base/action/component.rs @@ -1,8 +1,5 @@ //! Acciones que operan sobre componentes. -mod is_renderable; -pub use is_renderable::*; - mod before_render_component; pub use before_render_component::*; diff --git a/src/base/action/component/is_renderable.rs b/src/base/action/component/is_renderable.rs deleted file mode 100644 index 5a0e244..0000000 --- a/src/base/action/component/is_renderable.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::prelude::*; - -/// Tipo de función para determinar si un componente se renderiza o no. -/// -/// Se usa en la acción [`IsRenderable`] para controlar dinámicamente la visibilidad del componente -/// `component` según el contexto `cx`. El componente **no se renderiza** en cuanto una de las -/// funciones devuelva `false`. -pub type FnIsRenderable = fn(component: &C, cx: &Context) -> bool; - -/// Con la función [`FnIsRenderable`] se puede decidir si se renderiza o no un componente. -pub struct IsRenderable { - f: FnIsRenderable, - referer_type_id: Option, - referer_id: AttrId, - weight: Weight, -} - -/// Filtro para despachar [`FnIsRenderable`] para decidir si se renderiza o no un componente `C`. -impl ActionDispatcher for IsRenderable { - /// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`. - fn referer_type_id(&self) -> Option { - self.referer_type_id - } - - /// Devuelve el identificador del componente. - fn referer_id(&self) -> Option { - self.referer_id.get() - } - - /// Devuelve el peso para definir el orden de ejecución. - fn weight(&self) -> Weight { - self.weight - } -} - -impl IsRenderable { - /// Permite [registrar](Extension::actions) una nueva acción [`FnIsRenderable`]. - pub fn new(f: FnIsRenderable) -> Self { - IsRenderable { - f, - referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), - weight: 0, - } - } - - /// Afina el registro para ejecutar la acción [`FnIsRenderable`] sólo para el componente `C` - /// con identificador `id`. - pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - self.referer_id.alter_value(id); - self - } - - /// Opcional. Acciones con pesos más bajos se aplican antes. Se pueden usar valores negativos. - pub fn with_weight(mut self, value: Weight) -> Self { - self.weight = value; - self - } - - // Despacha las acciones. Se detiene en cuanto una [`FnIsRenderable`] devuelve `false`. - #[inline] - pub(crate) fn dispatch(component: &C, cx: &mut Context) -> bool { - let mut renderable = true; - dispatch_actions( - &ActionKey::new( - UniqueId::of::(), - None, - Some(UniqueId::of::()), - None, - ), - |action: &Self| { - if renderable && !(action.f)(component, cx) { - renderable = false; - } - }, - ); - if renderable { - if let Some(id) = component.id() { - dispatch_actions( - &ActionKey::new( - UniqueId::of::(), - None, - Some(UniqueId::of::()), - Some(id), - ), - |action: &Self| { - if renderable && !(action.f)(component, cx) { - renderable = false; - } - }, - ); - } - } - renderable - } -} diff --git a/src/core/component.rs b/src/core/component.rs index a7faa2f..9c9ade2 100644 --- a/src/core/component.rs +++ b/src/core/component.rs @@ -11,6 +11,59 @@ pub use children::{Typed, TypedOp}; mod context; pub use context::{Context, ContextError, ContextOp, Contextual}; +/// Alias de función (*callback*) para **determinar si un componente se renderiza o no**. +/// +/// Puede usarse para permitir que una instancia concreta de un tipo de componente dado decida +/// dinámicamente durante el proceso de renderizado ([`Component::is_renderable()`]) si se renderiza +/// o no. +/// +/// # Ejemplo +/// +/// ```rust +/// # use pagetop::prelude::*; +/// #[derive(AutoDefault)] +/// struct SampleComponent { +/// renderable: Option, +/// } +/// +/// impl Component for SampleComponent { +/// fn new() -> Self { +/// Self::default() +/// } +/// +/// fn is_renderable(&self, cx: &mut Context) -> bool { +/// // Si hay callback, se usa; en caso contrario, se renderiza por defecto. +/// self.renderable.map_or(true, |f| f(cx)) +/// } +/// +/// fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup { +/// PrepareMarkup::Escaped("Visible component".into()) +/// } +/// } +/// +/// impl SampleComponent { +/// /// Asigna una función que decidirá si el componente se renderiza o no. +/// #[builder_fn] +/// pub fn with_renderable(mut self, f: Option) -> Self { +/// self.renderable = f; +/// self +/// } +/// } +/// +/// fn sample() { +/// let mut cx = Context::default().with_param("user_logged_in", true); +/// +/// // Se instancia un componente que sólo se renderiza si `user_logged_in` es `true`. +/// let mut component = SampleComponent::new().with_renderable(Some(|cx: &Context| { +/// cx.param::("user_logged_in").copied().unwrap_or(false) +/// })); +/// +/// // Aquí simplemente se comprueba que compila y se puede invocar. +/// let _markup = component.render(&mut cx); +/// } +/// ``` +pub type FnIsRenderable = fn(cx: &Context) -> bool; + /// Alias de función (*callback*) para **resolver una URL** según el contexto de renderizado. /// /// Se usa para generar enlaces dinámicos en función del contexto (petición, idioma, etc.). Debe diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index c0573b4..13b0385 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -45,6 +45,20 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync { None } + /// Indica si el componente es renderizable. + /// + /// Por defecto, todos los componentes son renderizables (`true`). Sin embargo, este método + /// puede sobrescribirse para decidir dinámicamente si los componentes de este tipo se + /// renderizan o no en función del contexto de renderizado. + /// + /// También puede usarse junto con un alias de función como + /// ([`FnIsRenderable`](crate::core::component::FnIsRenderable)) para permitir que instancias + /// concretas del componente decidan si se renderizan o no. + #[allow(unused_variables)] + fn is_renderable(&self, cx: &mut Context) -> bool { + true + } + /// Configura el componente justo antes de preparar el renderizado. /// /// Este método puede sobrescribirse para modificar la estructura interna del componente o el @@ -72,30 +86,30 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync { /// Implementa [`render()`](ComponentRender::render) para todos los componentes. /// -/// Y para cada componente ejecuta la siguiente secuencia: +/// El proceso de renderizado de cada componente sigue esta secuencia: /// -/// 1. Despacha [`action::component::IsRenderable`](crate::base::action::component::IsRenderable) -/// para ver si se puede renderizar. Si no es así, devuelve un [`Markup`] vacío. +/// 1. Ejecuta [`is_renderable()`](Component::is_renderable) para ver si puede renderizarse en el +/// contexto actual. Si no es así, devuelve un [`Markup`] vacío. /// 2. Ejecuta [`setup_before_prepare()`](Component::setup_before_prepare) para que el componente /// pueda ajustar su estructura interna o modificar el contexto. /// 3. Despacha [`action::theme::BeforeRender`](crate::base::action::theme::BeforeRender) para -/// que el tema pueda hacer ajustes en el componente o el contexto. +/// permitir que el tema realice ajustes previos. /// 4. Despacha [`action::component::BeforeRender`](crate::base::action::component::BeforeRender) -/// para que otras extensiones puedan hacer ajustes. +/// para que otras extensiones puedan también hacer ajustes previos. /// 5. **Prepara el renderizado del componente**: /// - Despacha [`action::theme::PrepareRender`](crate::base::action::theme::PrepareRender) -/// para permitir al tema preparar un renderizado diferente al predefinido. -/// - Si no es así, ejecuta [`prepare_component()`](Component::prepare_component) para preparar -/// el renderizado predefinido del componente. +/// para permitir al tema generar un renderizado alternativo. +/// - Si el tema no lo modifica, llama a [`prepare_component()`](Component::prepare_component) +/// para obtener el renderizado por defecto del componente. /// 6. Despacha [`action::theme::AfterRender`](crate::base::action::theme::AfterRender) para -/// que el tema pueda hacer sus últimos ajustes. +/// que el tema pueda aplicar ajustes finales. /// 7. Despacha [`action::component::AfterRender`](crate::base::action::component::AfterRender) /// para que otras extensiones puedan hacer sus últimos ajustes. -/// 8. Finalmente devuelve un [`Markup`] del renderizado preparado en el paso 5. +/// 8. Devuelve el [`Markup`] generado en el paso 5. impl ComponentRender for C { fn render(&mut self, cx: &mut Context) -> Markup { // Si no es renderizable, devuelve un bloque HTML vacío. - if !action::component::IsRenderable::dispatch(self, cx) { + if !self.is_renderable(cx) { return html! {}; } -- 2.47.2 From d4be1362fc740627161ef83ad7f48b3de8291b84 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 17 Nov 2025 22:51:34 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=9A=9A=20Renombra=20`ThemeRegion`=20p?= =?UTF-8?q?or=20`DefaultRegions`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-aliner/src/lib.rs | 6 ++-- extensions/pagetop-bootsier/src/lib.rs | 4 +-- src/base/theme.rs | 2 +- src/base/theme/basic.rs | 4 +-- src/core/theme.rs | 11 ++++---- src/core/theme/definition.rs | 39 ++++++++++++++------------ src/core/theme/regions.rs | 16 +++++------ 7 files changed, 43 insertions(+), 39 deletions(-) diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index edbb504..cbc0f52 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -82,13 +82,13 @@ async fn homepage(request: HttpRequest) -> ResultPage { use pagetop::prelude::*; -/// El tema usa las mismas regiones predefinidas por [`ThemeRegion`]. -pub type AlinerRegion = ThemeRegion; +/// El tema usa las mismas regiones predefinidas por [`DefaultRegions`]. +pub type AlinerRegions = DefaultRegions; /// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML. /// /// Tema mínimo ideal para **pruebas y demos** que renderiza el **esqueleto HTML** con las mismas -/// regiones básicas definidas por [`ThemeRegion`]. No pretende ser un tema para producción, está +/// regiones básicas definidas por [`DefaultRegions`]. No pretende ser un tema para producción, está /// pensado para: /// /// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos. diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 4cb35a2..76f9d1e 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -101,8 +101,8 @@ pub mod prelude { pub use crate::theme::*; } -/// El tema usa las mismas regiones predefinidas por [`ThemeRegion`]. -pub type BootsierRegion = ThemeRegion; +/// El tema usa las mismas regiones predefinidas por [`DefaultRegions`]. +pub type BootsierRegions = DefaultRegions; /// Implementa el tema. pub struct Bootsier; diff --git a/src/base/theme.rs b/src/base/theme.rs index a4b2df5..1e5b1a8 100644 --- a/src/base/theme.rs +++ b/src/base/theme.rs @@ -1,4 +1,4 @@ //! Temas básicos soportados por PageTop. mod basic; -pub use basic::{Basic, BasicRegion}; +pub use basic::{Basic, BasicRegions}; diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index a671185..2d713e3 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -1,8 +1,8 @@ /// Es el tema básico que incluye PageTop por defecto. use crate::prelude::*; -/// El tema básico usa las mismas regiones predefinidas por [`ThemeRegion`]. -pub type BasicRegion = ThemeRegion; +/// El tema básico usa las mismas regiones predefinidas por [`DefaultRegions`]. +pub type BasicRegions = DefaultRegions; /// Tema básico por defecto que extiende el funcionamiento predeterminado de [`Theme`]. pub struct Basic; diff --git a/src/core/theme.rs b/src/core/theme.rs index 64f40f3..28638ba 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -9,13 +9,14 @@ //! tipografías, espaciados y cualquier otro detalle visual o de comportamiento (como animaciones, //! scripts de interfaz, etc.). //! -//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension); por -//! lo que se instancian, declaran sus dependencias y se inician igual que el resto de extensiones; -//! pero serán temas si además implementan [`theme()`](crate::core::extension::Extension::theme) y -//! [`Theme`]. +//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension), por +//! lo que se instancian, declaran dependencias y se inician igual que cualquier otra extensión. +//! También deben implementar [`Theme`] y sobrescribir el método +//! [`Extension::theme()`](crate::core::extension::Extension::theme) para que PageTop pueda +//! registrarlos como temas mod definition; -pub use definition::{Theme, ThemePage, ThemeRef, ThemeRegion}; +pub use definition::{Theme, ThemePage, ThemeRef, DefaultRegions}; mod regions; pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT}; diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 2a20c07..7d21c14 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -4,7 +4,7 @@ use crate::core::theme::{Region, RegionRef, REGION_CONTENT}; use crate::html::{html, Markup, StyleSheet}; use crate::locale::L10n; use crate::response::page::Page; -use crate::{global, join}; +use crate::{global, join, AutoDefault}; use std::sync::LazyLock; @@ -14,16 +14,17 @@ use std::sync::LazyLock; /// implementen [`Theme`] y, a su vez, [`Extension`]. pub type ThemeRef = &'static dyn Theme; -/// Conjunto de regiones que los temas pueden exponer para el renderizado. +/// Conjunto de regiones predefinidas que los temas pueden exponer para el renderizado. /// -/// `ThemeRegion` define un conjunto de regiones predefinidas para estructurar un documento HTML. +/// `DefaultRegions` define un conjunto de regiones predefinidas para estructurar un documento HTML. /// Proporciona **identificadores estables** (vía [`Region::key()`]) y **etiquetas localizables** /// (vía [`Region::label()`]) a las regiones donde se añadirán los componentes. /// /// Se usa por defecto en [`Theme::page_regions()`](crate::core::theme::Theme::page_regions) y sus /// variantes representan el conjunto mínimo recomendado para cualquier tema. Sin embargo, cada tema /// podría exponer su propio conjunto de regiones. -pub enum ThemeRegion { +#[derive(AutoDefault)] +pub enum DefaultRegions { /// Cabecera de la página. /// /// Clave: `"header"`. Suele contener *branding*, navegación principal o avisos globales. @@ -32,6 +33,7 @@ pub enum ThemeRegion { /// Contenido principal de la página (**obligatoria**). /// /// Clave: `"content"`. Es el destino por defecto para insertar componentes a nivel de página. + #[default] Content, /// Pie de página. @@ -40,12 +42,12 @@ pub enum ThemeRegion { Footer, } -impl Region for ThemeRegion { +impl Region for DefaultRegions { fn key(&self) -> &str { match self { - ThemeRegion::Header => "header", - ThemeRegion::Content => REGION_CONTENT, - ThemeRegion::Footer => "footer", + Self::Header => "header", + Self::Content => REGION_CONTENT, + Self::Footer => "footer", } } @@ -60,16 +62,17 @@ impl Region for ThemeRegion { /// implementa automáticamente para cualquier tipo que implemente [`Theme`], por lo que normalmente /// no requiere implementación explícita. /// -/// Si un tema **sobrescribe** uno o más de estos métodos de [`Theme`]: +/// Si un tema **sobrescribe** uno o más de los siguientes métodos de [`Theme`]: /// /// - [`render_page_region()`](Theme::render_page_region), /// - [`render_page_head()`](Theme::render_page_head), o /// - [`render_page_body()`](Theme::render_page_body); /// -/// es posible volver al comportamiento por defecto usando FQS (*Fully Qualified Syntax*): +/// puede volver al comportamiento por defecto con una llamada FQS (*Fully Qualified Syntax*) a: /// -/// - `::render_body(self, page, self.page_regions())` -/// - `::render_head(self, page)` +/// - `::render_region(self, page, region)`, +/// - `::render_body(self, page, self.page_regions())`, o +/// - `::render_head(self, page)`. pub trait ThemePage { /// Renderiza el **contenedor** de una región concreta del `` de la página. /// @@ -206,9 +209,9 @@ pub trait Theme: Extension + ThemePage + Send + Sync { /// fn page_regions(&self) -> &'static [RegionRef] { /// static REGIONS: LazyLock<[RegionRef; 4]> = LazyLock::new(|| { /// [ - /// &ThemeRegion::Header, - /// &ThemeRegion::Content, - /// &ThemeRegion::Footer, + /// &DefaultRegions::Header, + /// &DefaultRegions::Content, + /// &DefaultRegions::Footer, /// ] /// }); /// &*REGIONS @@ -217,9 +220,9 @@ pub trait Theme: Extension + ThemePage + Send + Sync { fn page_regions(&self) -> &'static [RegionRef] { static REGIONS: LazyLock<[RegionRef; 3]> = LazyLock::new(|| { [ - &ThemeRegion::Header, - &ThemeRegion::Content, - &ThemeRegion::Footer, + &DefaultRegions::Header, + &DefaultRegions::Content, + &DefaultRegions::Footer, ] }); &*REGIONS diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 8e386f5..17e1543 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -31,25 +31,25 @@ pub const REGION_CONTENT: &str = "content"; /// `aria-label` o en descripciones semánticas del contenedor). /// /// Las implementaciones típicas son *enumeraciones estáticas* declaradas por cada tema (ver como -/// ejemplo [`ThemeRegion`](crate::core::theme::ThemeRegion)), de modo que las claves y etiquetas -/// permanecen inmutables y fácilmente referenciables. +/// ejemplo [`DefaultRegions`](crate::core::theme::DefaultRegions)), de modo que las claves y +/// etiquetas permanecen inmutables y fácilmente referenciables. /// /// # Ejemplo /// /// ```rust /// # use pagetop::prelude::*; -/// pub enum MyThemeRegion { +/// pub enum MyThemeRegions { /// Header, /// Content, /// Footer, /// } /// -/// impl Region for MyThemeRegion { +/// impl Region for MyThemeRegions { /// fn key(&self) -> &str { /// match self { -/// MyThemeRegion::Header => "header", -/// MyThemeRegion::Content => "content", -/// MyThemeRegion::Footer => "footer", +/// Self::Header => "header", +/// Self::Content => "content", +/// Self::Footer => "footer", /// } /// } /// @@ -111,7 +111,7 @@ impl ChildrenInRegions { } } -/// Punto de acceso para añadir componentes a regiones globales o específicas de un tema. +/// Permite añadir componentes a regiones globales o específicas de un tema. /// /// Según la variante, se pueden añadir componentes ([`add()`](Self::add)) que permanecerán /// disponibles durante toda la ejecución. -- 2.47.2