diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 49b44d09..a4c182fa 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -102,7 +102,7 @@ impl Extension for SuperMenu { )), )); - InRegion::Global(&DefaultRegions::Header).add( + InRegion::Global(&CoreRegion::Header).add( bs::Container::new() .with_width(bs::container::Width::FluidMax(UnitValue::RelRem(75.0))) .with_child(navbar_menu), diff --git a/src/base/component.rs b/src/base/component.rs index 10832ff1..fe3c734c 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -1,5 +1,7 @@ //! Componentes nativos proporcionados por PageTop. +pub mod layout; + mod block; pub use block::Block; diff --git a/src/base/component/layout.rs b/src/base/component/layout.rs new file mode 100644 index 00000000..b9cfdf0c --- /dev/null +++ b/src/base/component/layout.rs @@ -0,0 +1,7 @@ +//! Definiciones para la composición de documentos ([`Region`] y [`Template`]). + +mod region; +pub use region::Region; + +mod template; +pub use template::Template; diff --git a/src/base/component/layout/region.rs b/src/base/component/layout/region.rs new file mode 100644 index 00000000..72c6cc96 --- /dev/null +++ b/src/base/component/layout/region.rs @@ -0,0 +1,111 @@ +use crate::prelude::*; + +use std::fmt; + +/// Componente que renderiza una región del ``. +/// +/// No recibe ningún contenido de quien lo construye. Lo obtiene directamente del [`Context`] en el +/// momento de renderizarse (ver [`Context::render_region()`]). Si la región no tiene contenido, no +/// se renderiza nada. +/// +/// Si un tema necesita maquetar una región determinada de forma distinta, puede capturar este +/// componente en [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) y hacer +/// [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`RegionRef`] que devuelve +/// [`Self::region()`], para compararlo con la variante deseada. +/// +/// Como cualquier otro componente, participa también en el despacho de las +/// [acciones de componentes](crate::base::action::component) para que otras extensiones puedan +/// intervenir en su renderizado. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// struct Sidebar; +/// +/// impl RegionName for Sidebar { +/// fn name(&self) -> &'static str { +/// "sidebar" +/// } +/// +/// fn label(&self) -> L10n { +/// L10n::n("Sidebar") +/// } +/// } +/// +/// let header = layout::Region::header(); +/// let sidebar = layout::Region::of(&Sidebar); +/// ``` +#[derive(Clone, Getters)] +pub struct Region { + /// Devuelve la región subyacente. + #[getters(copy)] + region: RegionRef, +} + +impl fmt::Debug for Region { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Region") + .field("region", &self.region().name()) + .finish() + } +} + +impl Default for Region { + fn default() -> Self { + Region { + region: &CoreRegion::Content, + } + } +} + +#[async_trait] +impl Component for Region { + fn new() -> Self { + Self::default() + } + + /// Devuelve el nombre de la región subyacente como identificador del componente. + fn id(&self) -> Option { + Some(self.region().name().to_owned()) + } + + async fn prepare(&self, cx: &mut Context) -> Result { + let name = self.region().name(); + let content = cx.render_region(self.region()).await; + Ok(html! { + @if !content.is_empty() { + div + id=[self.id()] + class=(util::join!("region region-", name)) + role="region" + aria-label=[self.region().label().lookup(cx)] + { + (content) + } + } + }) + } +} + +impl Region { + /// Define el componente que renderizará [`CoreRegion::Header`]. + pub fn header() -> Self { + Region { + region: &CoreRegion::Header, + } + } + + /// Define el componente que renderizará [`CoreRegion::Footer`]. + pub fn footer() -> Self { + Region { + region: &CoreRegion::Footer, + } + } + + /// Define el componente que renderizará la región indicada. + pub fn of(region: RegionRef) -> Self { + Region { region } + } +} diff --git a/src/base/component/layout/template.rs b/src/base/component/layout/template.rs new file mode 100644 index 00000000..54fd3f64 --- /dev/null +++ b/src/base/component/layout/template.rs @@ -0,0 +1,81 @@ +use crate::prelude::*; + +use std::fmt; + +/// Componente que renderiza el cuerpo de una plantilla de regiones. +/// +/// La composición por defecto usa el componente [`Region`](crate::base::component::layout::Region) +/// para mostrar, en este orden, las regiones [`CoreRegion::Header`], [`CoreRegion::Content`] y +/// [`CoreRegion::Footer`]. +/// +/// No incluye las regiones reservadas +/// [`ReservedRegion::PageTop`](crate::response::ReservedRegion::PageTop) y +/// [`ReservedRegion::PageBottom`](crate::response::ReservedRegion::PageBottom) porque el propio +/// [`Page::render()`](crate::response::Page::render) las añade antes y después del resultado de +/// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para que se +/// rendericen siempre, independientemente de la plantilla que se use. +/// +/// Si un tema necesita maquetar una plantilla determinada de forma distinta, puede capturar este +/// componente en [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) y hacer +/// [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`TemplateRef`] que devuelve +/// [`Self::template()`], para compararlo con la variante deseada. +/// +/// Como cualquier otro componente, participa también en el despacho de las +/// [acciones de componentes](crate::base::action::component) para que otras extensiones puedan +/// intervenir en su renderizado. +#[derive(Clone, Getters)] +pub struct Template { + /// Devuelve la plantilla subyacente. + #[getters(copy)] + template: TemplateRef, +} + +impl fmt::Debug for Template { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Template") + .field("template", &self.template().name()) + .finish() + } +} + +impl Default for Template { + fn default() -> Self { + Template { + template: &CoreTemplate::Standard, + } + } +} + +#[async_trait] +impl Component for Template { + fn new() -> Self { + Self::default() + } + + /// Devuelve el nombre de la plantilla subyacente como identificador del componente. + fn id(&self) -> Option { + Some(self.template().name().to_owned()) + } + + async fn prepare(&self, cx: &mut Context) -> Result { + Ok(html! { + (layout::Region::header().render(cx).await) + (layout::Region::default().render(cx).await) + (layout::Region::footer().render(cx).await) + }) + } +} + +impl Template { + /// Define el componente que renderizará [`CoreTemplate::Admin`]. + pub fn admin() -> Self { + Template { + template: &CoreTemplate::Admin, + } + } + + /// Define el componente que renderizará la plantilla indicada. + pub fn of(template: TemplateRef) -> Self { + Template { template } + } +} diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 71aebdd0..a1929526 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -25,7 +25,7 @@ impl Theme for Basic { .with_weight(-99), )) .alter_child_in( - &DefaultRegions::Footer, + &CoreRegion::Footer, ChildOp::AddIfEmpty(PoweredBy::new().into()), ); } diff --git a/src/core/component/context.rs b/src/core/component/context.rs index d9dc70a9..b6e95457 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -2,7 +2,8 @@ use crate::auth::CurrentUser; use crate::core::TypeInfo; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; -use crate::core::theme::{ChildrenInRegions, DefaultRegions, RegionRef, TemplateRef, ThemeRef}; +use crate::core::theme::{ChildrenInRegions, CoreRegion, CoreTemplate}; +use crate::core::theme::{RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet}; use crate::html::{Markup, Props, PropsOp, RoutePath, html}; use crate::locale::L10n; @@ -77,7 +78,7 @@ pub enum ContextError { /// fn prepare_context(cx: C) -> C { /// cx.with_langid(&Locale::resolve("es-ES")) /// .with_theme(&Aliner) -/// .with_template(&DefaultTemplates::Standard) +/// .with_template(&CoreTemplate::Standard) /// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) /// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css"))) /// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js"))) @@ -137,7 +138,7 @@ pub trait Contextual: LangId { /// Añade un componente o aplica una operación [`ChildOp`] en una región específica del /// documento. #[builder_fn] - fn with_child_in(self, region_ref: RegionRef, op: impl Into) -> Self; + fn with_child_in(self, region: RegionRef, op: impl Into) -> Self; // **< Contextual GETTERS >********************************************************************* @@ -236,28 +237,6 @@ pub trait Contextual: LangId { fn remove_param(&mut self, key: &'static str) -> bool; } -// Cómo obtener la plantilla activa del contexto: la del tema (por defecto o de administración), o -// una fijada explícitamente. Se resuelve contra el tema activo al leer `Context::template()`, no al -// asignarla, para que un cambio de tema posterior con `with_theme()` se refleje automáticamente. -enum TemplateSource { - // Plantilla por defecto. - Default, - // Plantilla de administración del tema activo. - Admin, - // Plantilla fijada explícitamente con `with_template()`. - Explicit(TemplateRef), -} - -impl TemplateSource { - fn resolve(&self, theme: ThemeRef) -> TemplateRef { - match self { - Self::Default => theme.default_template(), - Self::Admin => theme.admin_template(), - Self::Explicit(template) => *template, - } - } -} - /// Implementa un **contexto de renderizado** para un documento HTML. /// /// Se crea una sola vez por petición usando [`Context::new()`] (típicamente a través de @@ -278,9 +257,8 @@ impl TemplateSource { /// identificadores HTML únicos por tipo de componente. /// - [`push_message()`](Self::push_message)/[`messages()`](Self::messages) para acumular /// [`StatusMessage`] que mostrar en algún momento del renderizado. -/// - [`render_assets()`](Self::render_assets)/[`render_region_named()`](Self::render_region_named), -/// usados internamente por [`Page`](crate::response::Page) para producir el HTML final del -/// documento. +/// - [`render_assets()`](Self::render_assets)/[`render_region()`](Self::render_region), usados +/// internamente por [`Page`](crate::response::Page) para producir el HTML final del documento. /// /// # Ejemplos /// @@ -332,7 +310,7 @@ pub struct Context { locale : RequestLocale, // Idioma asociado a la petición. current_user: CurrentUser, // Identidad del usuario actual. theme : ThemeRef, // Referencia al tema usado para renderizar. - template : TemplateSource, // Plantilla usada para renderizar. + template : TemplateRef, // Plantilla usada para renderizar. favicon : Option, // Favicon, si se ha definido. preloads : Assets, // Recursos para precarga. stylesheets : Assets, // Hojas de estilo CSS. @@ -364,7 +342,7 @@ impl Context { locale, current_user, theme : *DEFAULT_THEME, - template : TemplateSource::Default, + template : &CoreTemplate::Standard, favicon : None, preloads : Assets::::new(), stylesheets: Assets::::new(), @@ -386,13 +364,6 @@ impl Context { .unwrap_or(CurrentUser::Anonymous) } - // Fuerza la plantilla de administración del tema activo (usada por `Page::admin()`). Se - // resuelve dinámicamente contra `self.theme`, igual que `TemplateSource::Default`, así que - // sigue reflejando cualquier cambio de tema posterior con `with_theme()`. - pub(crate) fn use_admin_template(&mut self) { - self.template = TemplateSource::Admin; - } - // **< Context RENDER >************************************************************************* /// Renderiza los recursos del contexto. @@ -426,9 +397,13 @@ impl Context { } /// Renderiza los componentes de una región. - pub async fn render_region_named(&mut self, region_name: &str) -> Markup { + /// + /// Combina los componentes registrados para esta región en la petición actual con los + /// prototipos globales añadidos vía [`InRegion`](crate::core::theme::InRegion) (comunes o + /// específicos del tema activo). + pub async fn render_region(&mut self, region: RegionRef) -> Markup { self.regions - .assemble_region(self.theme, region_name) + .assemble_region(self.theme, region) .render(self) .await } @@ -568,7 +543,7 @@ impl Contextual for Context { #[builder_fn] fn with_template(mut self, template: TemplateRef) -> Self { - self.template = TemplateSource::Explicit(template); + self.template = template; self } @@ -624,14 +599,13 @@ impl Contextual for Context { #[builder_fn] fn with_child(mut self, op: impl Into) -> Self { - self.regions - .alter_child_in(&DefaultRegions::Content, op.into()); + self.regions.alter_child_in(&CoreRegion::Content, op.into()); self } #[builder_fn] - fn with_child_in(mut self, region_ref: RegionRef, op: impl Into) -> Self { - self.regions.alter_child_in(region_ref, op.into()); + fn with_child_in(mut self, region: RegionRef, op: impl Into) -> Self { + self.regions.alter_child_in(region, op.into()); self } @@ -650,7 +624,7 @@ impl Contextual for Context { } fn template(&self) -> TemplateRef { - self.template.resolve(self.theme) + self.template } fn param(&self, key: &'static str) -> Result<&T, ContextError> { diff --git a/src/core/theme.rs b/src/core/theme.rs index 2dd31eef..fc54e3b3 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -1,14 +1,15 @@ //! API para añadir y gestionar nuevos temas. //! //! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos -//! interactivos. Para ello utiliza plantillas ([`Template`]) que describen cómo maquetar el cuerpo -//! del documento a partir de regiones ([`Region`]). Cada región es un contenedor lógico -//! identificado por un nombre para agrupar y renderizar componentes. +//! interactivos. Usa plantillas ([`Template`](crate::base::component::layout::Template)) para +//! maquetar los contenidos en base a regiones ([`Region`](crate::base::component::layout::Region)). +//! Cada región es un contenedor lógico identificado por un nombre para agrupar y renderizar +//! componentes. //! //! Una página ([`Page`](crate::response::Page)) es un documento HTML completo. Implementa -//! [`Contextual`](crate::core::component::Contextual) para gestionar su propio [`Context`], donde -//! mantiene el tema activo, la plantilla seleccionada y los componentes asociados a cada región a -//! renderizar. +//! [`Contextual`](crate::core::component::Contextual) para gestionar su propio +//! [`Context`](crate::core::component::Context), donde mantiene el tema activo, la plantilla +//! seleccionada y los componentes asociados a cada región a renderizar. //! //! Además, PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre. Un //! tema hijo hereda automáticamente todos los métodos del padre y puede sobrescribirlos @@ -26,24 +27,34 @@ //! Un tema puede personalizarse en tres pasos, cada uno necesario sólo si lo que ofrece PageTop por //! defecto no basta: //! -//! 1. **Definir regiones propias**. Por defecto PageTop define [`DefaultRegions`], con tres -//! regiones (`Header`, `Content` y `Footer`) que usan la implementación por defecto de -//! [`Region::render()`]. Un tema puede definir un *enum* propio que implemente [`Region`] para -//! exponer sus propias regiones (por ejemplo, una barra lateral) o para cambiar cómo se muestra -//! el contenido de una región ya existente (identificada por su nombre). -//! 2. **Definir plantillas propias**. Por defecto existe [`DefaultTemplates`], con dos plantillas -//! (`Standard` y `Admin`) que usan la implementación por defecto de [`Template::render()`] para -//! renderizar [`DefaultRegions::Header`], [`DefaultRegions::Content`] y -//! [`DefaultRegions::Footer`], en este orden. Un tema puede definir un *enum* propio que -//! implemente [`Template`] para crear nuevas plantillas, maquetar las regiones de otra forma, -//! cambiar su orden o envolverlas en contenedores adicionales. -//! 3. **Elegir las plantillas predeterminadas**. Por un lado, la plantilla por defecto vía -//! [`Theme::default_template()`] y, por otro, la plantilla para las páginas de administración, -//! [`Theme::admin_template()`]. De esta forma, las páginas creadas con `Page::new()` usarán -//! automáticamente la plantilla `default_template()` del tema activo, y las páginas creadas con -//! `Page::admin()` usarán la de `admin_template()`, sin tener que llamar manualmente a -//! [`with_template()`](crate::core::component::Contextual::with_template). Un tema que no -//! sobrescriba estos métodos sigue usando las plantillas por defecto de PageTop. +//! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegion`] (`Header`, `Content`, +//! `Footer`) como las regiones de plantilla que se asumen siempre disponibles, y +//! [`ReservedRegion`](crate::response::ReservedRegion) (`PageTop`, `PageBottom`) como las +//! regiones reservadas que se renderizan al margen de cualquier plantilla. Un tema puede definir +//! su propio *enum* que implemente [`RegionName`] para **añadir** nuevas regiones que PageTop no +//! ofrece (por ejemplo, una barra lateral). No es necesario redefinir las de [`CoreRegion`] ni +//! las de [`ReservedRegion`](crate::response::ReservedRegion), que ya existen y se asume que +//! cualquier tema respeta. +//! 2. **Definir plantillas nuevas**. Por defecto existe [`CoreTemplate`], con las plantillas +//! `Standard` y `Admin` que usan `Page::new()` y `Page::admin()` respectivamente, y que son +//! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada +//! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para +//! **añadir** plantillas que PageTop no ofrece, y no para redefinir `Standard`/`Admin`. +//! 3. **Cambiar cómo se renderiza** una región, una plantilla o un componente ya existente, se hace +//! capturando el componente ([`Region`](crate::base::component::layout::Region) o +//! [`Template`](crate::base::component::layout::Template), o el componente que sea) en +//! [`Theme::handle_component()`]. En el caso de regiones y plantillas, para distinguir *qué* +//! región o plantilla concreta envuelve el componente, sin comparar cadenas, basta con encadenar +//! el *getter* correspondiente +//! ([`Region::region()`](crate::base::component::layout::Region::region) o +//! [`Template::template()`](crate::base::component::layout::Template::template)) con +//! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por +//! ejemplo, [`CoreTemplate`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente +//! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias +//! variantes de plantilla. +//! +//! Para forzar una plantilla completamente distinta en una página concreta, se puede llamar +//! manualmente a [`with_template()`](crate::core::component::Contextual::with_template). //! //! Las páginas de error (403, 404, y otros errores fatales) no tienen una plantilla propia: se //! renderizan con la plantilla ya activa en la página, para que el usuario no pierda el contexto de @@ -51,9 +62,8 @@ //! [`Theme::error_403()`], [`Theme::error_404()`] o [`Theme::error_fatal()`], sin necesidad de una //! plantilla distinta. //! -//! El resto del comportamiento de un tema (renderizado del ``, o intervención en el -//! renderizado de componentes concretos con [`Theme::handle_component()`]) se sobrescribe de forma -//! independiente de estos tres pasos y no es necesario para tener un tema funcional. +//! El resto del comportamiento de un tema (por ejemplo, el renderizado del ``) se sobrescribe +//! de forma independiente de estos tres pasos y no es necesario para tener un tema funcional. //! //! # Componentes que se procesan en todas las páginas //! @@ -69,7 +79,7 @@ //! //! ```rust,no_run //! # use pagetop::prelude::*; -//! InRegion::Global(&DefaultRegions::Footer).add(PoweredBy::new()); +//! InRegion::Global(&CoreRegion::Footer).add(PoweredBy::new()); //! ``` //! //! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del @@ -82,90 +92,66 @@ //! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o //! si el usuario actual está autenticado. -use crate::async_trait; -use crate::core::component::Context; -use crate::html::{Markup, html}; +use crate::AutoDefault; +use crate::core::AnyInfo; use crate::locale::L10n; -use crate::{AutoDefault, util}; -// **< Region >************************************************************************************* +// **< RegionName >********************************************************************************* -/// Interfaz común para las regiones lógicas de un documento. +/// Interfaz común para las regiones lógicas del ``. /// -/// Una `Region` representa un contenedor lógico identificado por un nombre de región. Su contenido -/// se obtiene del [`Context`], donde los componentes suelen registrarse usando implementaciones de -/// métodos como [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in). +/// Una `RegionName` representa un contenedor lógico identificado por un nombre de región. Su +/// contenido se obtiene del [`Context`](crate::core::component::Context), donde los componentes +/// suelen registrarse usando implementaciones de métodos como +/// [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in). /// /// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas -/// implementaciones de [`Region`] que devuelvan el mismo nombre compartirán el mismo conjunto de -/// componentes registrados en el [`Context`], aunque cada región puede renderizar ese contenido de -/// forma diferente. Por ejemplo, [`DefaultRegions::Header`] y `BootsierRegions::Header` mostrarían -/// los mismos componentes si ambas devuelven el nombre `"header"`, pero podrían maquetarse de -/// manera distinta. +/// implementaciones de [`RegionName`] que devuelvan el mismo nombre comparten el mismo conjunto de +/// componentes registrados en el [`Context`](crate::core::component::Context). Un *enum* propio que +/// implemente [`RegionName`] está pensado para **añadir** regiones que PageTop no ofrece (con un +/// nombre propio que no colisione con los de [`CoreRegion`] o +/// [`ReservedRegion`](crate::response::ReservedRegion)). /// -/// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla -/// ([`Template`]) al renderizar la página ([`Page`](crate::response::Page)). -#[async_trait] -pub trait Region: Send + Sync { +/// El tema decide qué regiones mostrar en el ``, normalmente usando una plantilla +/// ([`TemplateName`]) al renderizar la página ([`Page`](crate::response::Page)). +/// +/// Requiere [`AnyInfo`] para que un [`RegionRef`] pueda recuperarse mediante +/// [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia su tipo concreto (por +/// ejemplo, para que un tema distinga en +/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) qué variante +/// concreta está renderizando el componente [`Region`](crate::base::component::layout::Region)). +pub trait RegionName: Send + Sync + AnyInfo { /// Devuelve el nombre de la región. /// - /// Este nombre es el identificador lógico de la región y se usa como clave en el [`Context`] - /// para recuperar y renderizar el contenido registrado bajo ese nombre. Cualquier - /// implementación de [`Region`] que devuelva el mismo nombre compartirá el mismo conjunto de - /// componentes. - /// - /// En la implementación predeterminada de [`Self::render()`] también se utiliza para construir - /// las clases del contenedor de la región (`"region region-"`). + /// Este nombre es el identificador lógico de la región y se usa como clave en el + /// [`Context`](crate::core::component::Context) para recuperar y renderizar el contenido + /// registrado bajo ese nombre. Cualquier implementación de [`RegionName`] que devuelva el mismo + /// nombre compartirá el mismo conjunto de componentes. fn name(&self) -> &'static str; /// Devuelve un *texto localizado* como etiqueta de accesibilidad asociada a la región. /// - /// En la implementación predeterminada de [`Self::render()`], este valor se usa como - /// `aria-label` del contenedor de la región. + /// En la implementación predeterminada de [`Region`](crate::base::component::layout::Region), + /// este valor se usa como `aria-label` del contenedor de la región. fn label(&self) -> L10n; - - /// Renderiza el contenedor de la región. - /// - /// Por defecto, recupera del [`Context`] el contenido de la región y, si no está vacío, lo - /// envuelve en un `
` con clases `"region region-"` y un `aria-label` basado en el - /// *texto localizado* de la etiqueta asociada a la región: - /// - /// ```html - ///
- /// - ///
- /// ``` - /// - /// Se puede sobrescribir este método para modificar la estructura del contenedor, las clases - /// utilizadas o la semántica del marcado generado para cada región. - async fn render(&self, cx: &mut Context) -> Markup { - html! { - @let region = cx.render_region_named(self.name()).await; - @if !region.is_empty() { - div - class=(util::join!("region region-", self.name())) - role="region" - aria-label=[self.label().lookup(cx)] - { - (region) - } - } - } - } } /// Referencia estática a una región. -pub type RegionRef = &'static dyn Region; +pub type RegionRef = &'static dyn RegionName; -// **< DefaultRegions >***************************************************************************** +// **< CoreRegion >********************************************************************************* /// Regiones básicas que PageTop proporciona por defecto. /// -/// Estas regiones comparten sus nombres (`"header"`, `"content"`, `"footer"`) con cualquier región -/// equivalente definida por otros temas, por lo que comparten también el contenido registrado bajo -/// esos nombres. +/// Comparten sus nombres (`"header"`, `"content"`, `"footer"`) con otras regiones que implementen +/// [`RegionName`], por lo que comparten también el contenido registrado bajo esos nombres. Por +/// defecto, son las regiones usadas por [`Template`](crate::base::component::layout::Template). +/// +/// A estas regiones hay que sumar también las regiones internas reservadas por +/// [`ReservedRegion`](crate::response::ReservedRegion) (`"page-top"` y `"page-bottom"`), que +/// [`Page::render()`](crate::response::Page::render) renderiza en cualquier caso. #[derive(AutoDefault)] -pub enum DefaultRegions { +pub enum CoreRegion { /// Región estándar para la **cabecera** del documento, de nombre `"header"`. /// /// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc. @@ -184,7 +170,7 @@ pub enum DefaultRegions { Footer, } -impl Region for DefaultRegions { +impl RegionName for CoreRegion { #[inline] fn name(&self) -> &'static str { match self { @@ -204,63 +190,64 @@ impl Region for DefaultRegions { } } -// **< Template >*********************************************************************************** +// **< TemplateName >******************************************************************************* -/// Interfaz común para definir plantillas de contenido. +/// Interfaz común para las plantillas lógicas de una página. /// -/// Una `Template` puede proporcionar una o más variantes para decidir la composición del `` -/// de una página ([`Page`](crate::response::Page)). El tema utiliza esta información para -/// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden. -#[async_trait] -pub trait Template: Send + Sync { - /// Renderiza el contenido de la plantilla. - /// - /// Por defecto, renderiza las regiones básicas de [`DefaultRegions`] en este orden: - /// [`DefaultRegions::Header`], [`DefaultRegions::Content`] y [`DefaultRegions::Footer`]. - /// - /// Se puede sobrescribir este método para: - /// - /// - Cambiar el conjunto de regiones que se renderizan según variantes de la plantilla. - /// - Alterar el orden de dichas regiones. - /// - Envolver las regiones en contenedores adicionales. - /// - Implementar distribuciones específicas (por ejemplo, con barras laterales). - /// - /// Este método se invoca normalmente desde [`Theme::render_page_body()`] para generar el - /// contenido del `` de una página según la plantilla devuelta por el contexto de la - /// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())). - async fn render(&self, cx: &mut Context) -> Markup { - html! { - (DefaultRegions::Header.render(cx).await) - (DefaultRegions::Content.render(cx).await) - (DefaultRegions::Footer.render(cx).await) - } - } +/// Representa una variante identificada por un nombre. Un tema puede usar este nombre para decidir +/// la composición del cuerpo de una página ([`Page`](crate::response::Page)), es decir, qué +/// regiones ([`RegionName`]) renderizar y en qué orden. +/// +/// Requiere [`AnyInfo`] por el mismo motivo que [`RegionName`], para que un [`TemplateRef`] pueda +/// recuperarse mediante [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia su +/// tipo concreto (por ejemplo, para que un tema distinga en +/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) qué variante concreta +/// está renderizando el componente [`Template`](crate::base::component::layout::Template)). +pub trait TemplateName: Send + Sync + AnyInfo { + /// Devuelve el nombre de la plantilla. + fn name(&self) -> &'static str; + + /// Devuelve un *texto localizado* como etiqueta descriptiva de la plantilla. + fn label(&self) -> L10n; } /// Referencia estática a una plantilla. -pub type TemplateRef = &'static dyn Template; +pub type TemplateRef = &'static dyn TemplateName; -// **< DefaultTemplates >*************************************************************************** +// **< CoreTemplate >******************************************************************************* /// Plantillas que PageTop proporciona por defecto. #[derive(AutoDefault)] -pub enum DefaultTemplates { - /// Plantilla predeterminada. +pub enum CoreTemplate { + /// Plantilla predeterminada, de nombre `"standard"`. /// - /// Utiliza la implementación por defecto de [`Template::render()`] y se emplea cuando no se - /// selecciona ninguna otra plantilla explícitamente. + /// Se emplea cuando no se selecciona ninguna otra plantilla explícitamente. #[default] Standard, - /// Plantilla para la **interfaz de administración**. + /// Plantilla para la **interfaz de administración**, de nombre `"admin"`. /// - /// Se utiliza para páginas de administración o paneles de control. Por defecto utiliza la misma - /// implementación de [`Template::render()`] que [`Self::Standard`]. + /// Se utiliza para páginas de administración o paneles de control. Admin, } -#[async_trait] -impl Template for DefaultTemplates {} +impl TemplateName for CoreTemplate { + #[inline] + fn name(&self) -> &'static str { + match self { + Self::Standard => "standard", + Self::Admin => "admin", + } + } + + #[inline] + fn label(&self) -> L10n { + match self { + Self::Standard => L10n::l("template-standard"), + Self::Admin => L10n::l("template-admin"), + } + } +} // **< render_component! >************************************************************************** diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index b0e05ee2..3a120c4e 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -1,8 +1,9 @@ use crate::async_trait; -use crate::base::component::{Html, Intro, IntroOpening}; -use crate::core::component::{ChildOp, Component, ComponentError, Context, Contextual}; +use crate::base::component::{Html, Intro, IntroOpening, layout}; +use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender}; +use crate::core::component::{Context, Contextual}; use crate::core::extension::Extension; -use crate::core::theme::{DefaultRegions, DefaultTemplates, TemplateRef}; +use crate::core::theme::CoreRegion; use crate::global; use crate::html::{Markup, html}; use crate::locale::L10n; @@ -13,10 +14,10 @@ use crate::web::http::StatusCode; /// /// Un tema es una [`Extension`](crate::core::extension::Extension) que define el aspecto general de /// las páginas: cómo se renderiza el ``, cómo se presenta el `` usando plantillas -/// ([`Template`](crate::core::theme::Template)) que maquetan regiones -/// ([`Region`](crate::core::theme::Region)) y qué contenido mostrar en las páginas de error. El -/// contenido de cada región depende del [`Context`](crate::core::component::Context) y de su nombre -/// lógico. +/// ([`TemplateName`](crate::core::theme::TemplateName)) que maquetan regiones +/// ([`RegionName`](crate::core::theme::RegionName)) y qué contenido mostrar en las páginas de +/// error. El contenido de cada región depende del [`Context`](crate::core::component::Context) y de +/// su nombre lógico. /// /// Todos los métodos de este *trait* tienen una implementación por defecto, por lo que pueden /// sobrescribirse selectivamente para crear nuevos temas con comportamientos distintos a los @@ -59,34 +60,6 @@ pub trait Theme: Extension + Send + Sync { None } - /// Devuelve la plantilla ([`Template`](crate::core::theme::Template)) que el propio tema - /// propone como predeterminada. - /// - /// Se utiliza al inicializar un [`Context`](crate::core::component::Context) o una página - /// ([`Page`](crate::response::Page)) por si no se elige ninguna otra plantilla con - /// [`Contextual::with_template()`](crate::core::component::Contextual::with_template). - /// - /// La implementación por defecto devuelve la plantilla [`DefaultTemplates::Standard`] con una - /// estructura básica para la página. Los temas pueden sobrescribir este método para seleccionar - /// otra plantilla predeterminada o una plantilla propia. - #[inline] - fn default_template(&self) -> TemplateRef { - self.parent() - .map_or(&DefaultTemplates::Standard, |p| p.default_template()) - } - - /// Devuelve la plantilla ([`Template`](crate::core::theme::Template)) que el tema propone para - /// la interfaz de administración. - /// - /// La implementación por defecto devuelve la plantilla [`DefaultTemplates::Admin`] con una - /// estructura básica para la interfaz de administración. Los temas pueden sobrescribir este - /// método para seleccionar otra plantilla predeterminada o una plantilla propia. - #[inline] - fn admin_template(&self) -> TemplateRef { - self.parent() - .map_or(&DefaultTemplates::Admin, |p| p.admin_template()) - } - /// Acciones específicas del tema antes de renderizar el `` de la página. /// /// Es un buen lugar para inicializar o ajustar recursos en función del contexto de la página, @@ -107,14 +80,13 @@ pub trait Theme: Extension + Send + Sync { /// Renderiza el contenido del `` de la página. /// /// La implementación predeterminada delega en la plantilla asociada a la página, obtenida desde - /// su [`Context`](crate::core::component::Context), y llama a - /// [`Template::render()`](crate::core::theme::Template::render) para componer el `` a - /// partir de las regiones. + /// su [`Context`](crate::core::component::Context), para componer el `` a partir de las + /// regiones. /// /// Con la configuración por defecto, la plantilla estándar utiliza las regiones - /// [`DefaultRegions::Header`](crate::core::theme::DefaultRegions::Header), - /// [`DefaultRegions::Content`](crate::core::theme::DefaultRegions::Content) y - /// [`DefaultRegions::Footer`](crate::core::theme::DefaultRegions::Footer) en ese orden. + /// [`CoreRegion::Header`](crate::core::theme::CoreRegion::Header), + /// [`CoreRegion::Content`](crate::core::theme::CoreRegion::Content) y + /// [`CoreRegion::Footer`](crate::core::theme::CoreRegion::Footer) en ese orden. /// /// Los temas pueden sobrescribir este método para: /// @@ -127,7 +99,8 @@ pub trait Theme: Extension + Send + Sync { if let Some(parent) = self.parent() { parent.render_page_body(page).await } else { - page.template().render(page.context()).await + let template = page.template(); + layout::Template::of(template).render(page.context()).await } } @@ -247,16 +220,16 @@ pub trait Theme: Extension + Send + Sync { /// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado). /// - /// Normalmente se renderiza con la plantilla predeterminada del tema (por defecto suele ser - /// [`DefaultTemplates::Standard`]), para que el usuario no pierda el contexto de navegación del - /// sitio. Los temas pueden sobrescribir este método para personalizar completamente el diseño y - /// el contenido de la página de error. + /// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo + /// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)), para que el usuario + /// no pierda el contexto de navegación del sitio. Los temas pueden sobrescribir este método + /// para personalizar completamente el diseño y el contenido de la página de error. fn error_403(&self, page: &mut Page) { if let Some(parent) = self.parent() { return parent.error_403(page); } page.alter_title(L10n::l("error403_title")).alter_child_in( - &DefaultRegions::Content, + &CoreRegion::Content, ChildOp::Prepend( Html::with(move |cx| { html! { @@ -273,15 +246,16 @@ pub trait Theme: Extension + Send + Sync { /// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado). /// - /// Normalmente se renderiza con la plantilla predeterminada del tema (por defecto suele ser - /// [`DefaultTemplates::Standard`]). Los temas pueden sobrescribir este método para personalizar - /// completamente el diseño y el contenido de la página de error. + /// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo + /// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)). Los temas pueden + /// sobrescribir este método para personalizar completamente el diseño y el contenido de la + /// página de error. fn error_404(&self, page: &mut Page) { if let Some(parent) = self.parent() { return parent.error_404(page); } page.alter_title(L10n::l("error404_title")).alter_child_in( - &DefaultRegions::Content, + &CoreRegion::Content, ChildOp::Prepend( Html::with(move |cx| { html! { @@ -307,9 +281,10 @@ pub trait Theme: Extension + Send + Sync { /// funcionan con normalidad. /// /// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya - /// activa en la página (normalmente [`DefaultTemplates::Standard`]) y muestra un componente - /// [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y - /// `help`) como descripción del error. + /// activa en la página (normalmente + /// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)) y muestra un + /// componente [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados + /// (`alert` y `help`) como descripción del error. /// /// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni /// [`Self::error_404()`], que definen su propio contenido específico. @@ -326,7 +301,7 @@ pub trait Theme: Extension + Send + Sync { return parent.error_fatal(page, code, title, alert, help); } page.alter_title(title).alter_child_in( - &DefaultRegions::Content, + &CoreRegion::Content, ChildOp::Prepend( Intro::new() .with_title(L10n::l("error_code").with_arg("code", code.to_string())) diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index abf19bfb..f6c00aed 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,5 +1,5 @@ use crate::core::component::{Child, ChildOp, Children, Component}; -use crate::core::theme::{DefaultRegions, RegionRef, ThemeRef}; +use crate::core::theme::{CoreRegion, RegionRef, ThemeRef}; use crate::{AutoDefault, UniqueId, builder_fn}; use parking_lot::RwLock; @@ -43,20 +43,19 @@ static COMMON_REGIONS: LazyLock> = pub(crate) struct ChildrenInRegions(HashMap); impl ChildrenInRegions { - pub fn with(region_ref: RegionRef, child: Child) -> Self { - Self::default().with_child_in(region_ref, child) + pub fn with(region: RegionRef, child: Child) -> Self { + Self::default().with_child_in(region, child) } #[builder_fn] - pub fn with_child_in(mut self, region_ref: RegionRef, op: impl Into) -> Self { + pub fn with_child_in(mut self, region: RegionRef, op: impl Into) -> Self { let child = op.into(); - if let Some(region) = self.0.get_mut(region_ref.name()) { + let region_name = region.name(); + if let Some(region) = self.0.get_mut(region_name) { region.alter_child(child); } else { - self.0.insert( - region_ref.name().to_owned(), - Children::new().with_child(child), - ); + let children = Children::new().with_child(child); + self.0.insert(region_name.to_owned(), children); } self } @@ -71,7 +70,8 @@ impl ChildrenInRegions { /// lugar de clonarse, ya que son de un único uso. /// 3. Prototipos del tema activo, exclusivos del tema en curso. También se clonan para asegurar /// que llegan a `setup()` con el mismo estado inicial. - pub fn assemble_region(&mut self, theme_ref: ThemeRef, region_name: &str) -> Children { + pub fn assemble_region(&mut self, theme: ThemeRef, region: RegionRef) -> Children { + let region_name = region.name(); let common = COMMON_REGIONS.read(); let themed = THEME_REGIONS.read(); @@ -90,7 +90,7 @@ impl ChildrenInRegions { } } // 3. Prototipos del tema activo. - if let Some(theme_map) = themed.get(&theme_ref.type_id()) { + if let Some(theme_map) = themed.get(&theme.type_id()) { if let Some(protos) = theme_map.get(region_name) { for proto in protos { result.add(proto.as_child()); @@ -118,31 +118,27 @@ impl ChildrenInRegions { /// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" })); /// /// // Texto en la cabecera, visible en todos los temas. -/// InRegion::Global(&DefaultRegions::Header).add(Html::with(|_| html! { "Publicidad" })); +/// InRegion::Global(&CoreRegion::Header).add(Html::with(|_| html! { "Publicidad" })); /// ``` pub enum InRegion { /// Región principal de **contenido** por defecto. /// - /// Añade el componente a la región lógica de contenido principal de la aplicación. Por - /// convención, esta región corresponde a [`DefaultRegions::Content`], cuyo nombre es - /// `"content"`. Cualquier tema que renderice esa misma región de contenido, ya sea usando - /// directamente [`DefaultRegions::Content`] o cualquier otra implementación de - /// [`Region`](crate::core::theme::Region) que devuelva ese mismo nombre, mostrará los - /// componentes registrados aquí, aunque lo harán según su propio método de renderizado - /// ([`Region::render()`](crate::core::theme::Region::render)). + /// Añade el componente a la región lógica de contenido principal de la aplicación. Internamente + /// equivale a `InRegion::Global(&CoreRegion::Content)`. Content, /// Región global compartida por todos los temas. /// /// Los componentes añadidos aquí se asocian al nombre de la región indicado por [`RegionRef`], - /// es decir, al valor devuelto por [`Region::name()`](crate::core::theme::Region::name) para - /// esa región. Se mostrarán en cualquier tema cuya plantilla renderice una región que devuelva - /// ese mismo nombre. + /// es decir, al valor devuelto por + /// [`RegionName::name()`](crate::core::theme::RegionName::name) para esa región. Se mostrarán + /// en cualquier tema que renderice la región que devuelva ese nombre. Global(RegionRef), /// Región asociada a un tema concreto. /// - /// Los componentes sólo se renderizarán cuando el documento se procese con el tema indicado y - /// se utilice la región referenciada. Resulta útil para añadir contenido específico en un tema - /// sin afectar a otros. + /// Los componentes sólo se renderizarán cuando el documento se procese exactamente con el tema + /// indicado (no sirve un tema hijo que lo herede), y se utilice la región referenciada. A + /// diferencia del resto de comportamiento de `Theme`, este registro no sigue la cadena + /// `parent()`. Resulta útil para añadir contenido específico en un tema sin afectar a otros. ForTheme(ThemeRef, RegionRef), } @@ -163,26 +159,26 @@ impl InRegion { /// })); /// /// // Texto en la cabecera. - /// InRegion::Global(&DefaultRegions::Header).add(Html::with(|_| { + /// InRegion::Global(&CoreRegion::Header).add(Html::with(|_| { /// html! { "Publicidad" } /// })); /// /// // Contenido sólo para la región del pie de página en un tema concreto. - /// InRegion::ForTheme(&theme::Basic, &DefaultRegions::Footer).add(Html::with(|_| { + /// InRegion::ForTheme(&theme::Basic, &CoreRegion::Footer).add(Html::with(|_| { /// html! { "Aviso legal" } /// })); /// ``` pub fn add(&self, component: impl Component + Clone + 'static) -> &Self { let proto: Arc = Arc::new(component); match self { - InRegion::Content => Self::add_to_common(&DefaultRegions::Content, proto), - InRegion::Global(region_ref) => Self::add_to_common(*region_ref, proto), - InRegion::ForTheme(theme_ref, region_ref) => { + InRegion::Content => Self::add_to_common(&CoreRegion::Content, proto), + InRegion::Global(region) => Self::add_to_common(*region, proto), + InRegion::ForTheme(theme, region) => { THEME_REGIONS .write() - .entry(theme_ref.type_id()) + .entry(theme.type_id()) .or_default() - .entry((*region_ref).name().to_owned()) + .entry((*region).name().to_owned()) .or_default() .push(proto); } @@ -191,10 +187,10 @@ impl InRegion { } #[inline] - fn add_to_common(region_ref: RegionRef, proto: Arc) { + fn add_to_common(region: RegionRef, proto: Arc) { COMMON_REGIONS .write() - .entry(region_ref.name().to_owned()) + .entry(region.name().to_owned()) .or_default() .push(proto); } diff --git a/src/response/page.rs b/src/response/page.rs index aa59130c..cec7c776 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -2,16 +2,17 @@ //! //! Este módulo define [`Page`], que representa una página HTML lista para renderizar. Cada página //! se construye a partir de un [`Context`] propio, donde se registran el tema activo, la plantilla -//! ([`Template`](crate::core::theme::Template)) que define la disposición de las regiones -//! ([`Region`]), los componentes asociados y los recursos adicionales (hojas de estilo, scripts, -//! *favicon*, etc.). +//! ([`TemplateName`](crate::core::theme::TemplateName)) que define la disposición de las regiones +//! ([`RegionName`]), los componentes asociados y los recursos adicionales (hojas de estilo, +//! scripts, *favicon*, etc.). //! //! El renderizado ([`Page::render()`]) delega en el tema ([`Theme`](crate::core::theme::Theme)) la //! composición del `` y del ``, y se ejecutan las acciones registradas por las //! extensiones antes y después de generar los contenidos. //! -//! También introduce regiones internas reservadas ([`ReservedRegion`]) que actúan como puntos de -//! anclaje globales al inicio y al final del documento. +//! También define las regiones internas reservadas ([`ReservedRegion`]) que actúan como puntos de +//! anclaje globales al inicio y al final del ``, fuera de las regiones que maqueta la +//! plantilla activa. mod error; pub use error::ErrorPage; @@ -19,8 +20,10 @@ pub(crate) use error::{render_error_pages, response_for_panic, route_not_found}; use crate::auth::CurrentUser; use crate::base::action; -use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual}; -use crate::core::theme::{DefaultRegions, Region, RegionRef, TemplateRef, ThemeRef}; +use crate::base::component::layout; +use crate::core::component::{AssetsOp, ChildOp, ComponentRender}; +use crate::core::component::{Context, ContextError, Contextual}; +use crate::core::theme::{CoreRegion, CoreTemplate, RegionName, RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{Attr, Props, PropsOp}; use crate::html::{DOCTYPE, Markup, html}; @@ -32,37 +35,35 @@ use crate::{AutoDefault, builder_fn}; /// Regiones internas reservadas como puntos de anclaje globales. /// -/// Representan contenedores especiales situados al inicio y al final de un documento. Están -/// pensadas para proporcionar regiones donde inyectar contenido global o técnico. No suelen usarse -/// como regiones visibles en los temas. +/// Representan contenedores especiales situados al inicio y al final del ``, fuera de las +/// regiones que maqueta la plantilla activa. Las renderiza directamente [`Page::render()`], +/// envolviendo el resultado de +/// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body). **No suelen usarse +/// como regiones "visibles" en los temas**, sino para inyectar contenido global o técnico. +#[derive(AutoDefault)] pub enum ReservedRegion { - /// Región interna situada al **inicio del documento**. + /// Región interna situada al **inicio del ``**, de nombre `"page-top"`. /// - /// Su función es proporcionar un contenedor donde las extensiones puedan inyectar contenido - /// global antes del resto de regiones principales (cabecera, contenido, etc.). - /// - /// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual, - /// sino como punto de anclaje para elementos auxiliares, marcadores técnicos, inicializadores o - /// contenido de depuración que deban situarse en la parte superior del documento. + /// Proporciona un contenedor donde las extensiones puedan inyectar elementos auxiliares antes + /// del resto de regiones (cabecera, contenido, etc.), como marcadores técnicos, inicializadores + /// o contenido de depuración. /// /// Se considera una región **reservada** para este tipo de usos globales. + #[default] PageTop, - /// Región interna situada al **final del documento**. + /// Región interna situada al **final del ``**, de nombre `"page-bottom"`. /// - /// Pensada para proporcionar un contenedor donde las extensiones puedan inyectar contenido - /// global después del resto de regiones principales (cabecera, contenido, etc.). - /// - /// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual, - /// sino como punto de anclaje para elementos auxiliares asociados a comportamientos dinámicos - /// que deban situarse en la parte inferior del documento. + /// Proporciona un contenedor donde las extensiones puedan inyectar contenido global después del + /// resto de regiones (cabecera, contenido, etc.), como elementos auxiliares asociados a + /// comportamientos dinámicos. /// /// Igual que [`Self::PageTop`], se considera una región **reservada** para este tipo de usos /// globales. PageBottom, } -impl Region for ReservedRegion { +impl RegionName for ReservedRegion { #[inline] fn name(&self) -> &'static str { match self { @@ -101,22 +102,23 @@ impl Page { /// [`CurrentUser`] inyectado por middleware en sus extensiones (ver /// [`Context::new`](crate::core::component::Context::new)). Cualquier handler tiene acceso al /// usuario actual desde el momento en que se crea la página, sin llamadas adicionales. - #[rustfmt::skip] pub fn new(request: HttpRequest) -> Self { Page { - title : Attr::::default(), - description : Attr::::default(), - metadata : Vec::default(), - properties : Vec::default(), - context : Context::new(Some(request)), + context: Context::new(Some(request)), + ..Default::default() } } - /// Crea una nueva instancia de página con la plantilla de administración del tema activo. + /// Crea una nueva instancia de página con la plantilla [`CoreTemplate::Admin`]. + /// + /// Cada tema puede maquetarla de forma distinta capturando + /// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la + /// plantilla en sí es la misma constante para cualquier tema. pub fn admin(request: HttpRequest) -> Self { - let mut page = Page::new(request); - page.context().use_admin_template(); - page + Page { + context: Context::new(Some(request)).with_template(&CoreTemplate::Admin), + ..Default::default() + } } // **< Page BUILDER >*************************************************************************** @@ -217,9 +219,9 @@ impl Page { // Renderiza el . let body = html! { - (ReservedRegion::PageTop.render(&mut self.context).await) + (layout::Region::of(&ReservedRegion::PageTop).render(&mut self.context).await) (self.context.theme().render_page_body(self).await) - (ReservedRegion::PageBottom.render(&mut self.context).await) + (layout::Region::of(&ReservedRegion::PageBottom).render(&mut self.context).await) }; // Acciones específicas del tema después de renderizar el . @@ -310,14 +312,13 @@ impl Contextual for Page { #[builder_fn] fn with_child(mut self, op: impl Into) -> Self { - self.context - .alter_child_in(&DefaultRegions::Content, op.into()); + self.context.alter_child_in(&CoreRegion::Content, op.into()); self } #[builder_fn] - fn with_child_in(mut self, region_ref: RegionRef, op: impl Into) -> Self { - self.context.alter_child_in(region_ref, op.into()); + fn with_child_in(mut self, region: RegionRef, op: impl Into) -> Self { + self.context.alter_child_in(region, op.into()); self } diff --git a/tests/component_template.rs b/tests/component_template.rs new file mode 100644 index 00000000..cc61b8ce --- /dev/null +++ b/tests/component_template.rs @@ -0,0 +1,118 @@ +use pagetop::prelude::*; + +/// Initializes PageTop (locale, extensions...) once for the whole suite. +/// +/// Rendering a `Region`/`Template` looks up localized labels (`aria-label`, etc.), so tests that +/// render them need the localization subsystem loaded. +async fn setup() { + Application::new().await; +} + +// **< A theme that intercepts the `Template` component >******************************************* + +/// Replaces the default `Template` composition (`Header` + `Content` + `Footer`) with a fixed +/// marker string, for both `CoreTemplate::Standard` and `CoreTemplate::Admin`. Mirrors how +/// a real theme (e.g. `pagetop-bootsier`) tells its own layout apart from PageTop's default: by +/// intercepting the `Template` component in `handle_component()`, not by swapping which +/// `TemplateRef` gets resolved. +struct MarkerTheme; + +#[async_trait] +impl Extension for MarkerTheme { + fn theme(&self) -> Option { + Some(&Self) + } +} + +#[async_trait] +impl Theme for MarkerTheme { + async fn handle_component( + &self, + component: &mut dyn Component, + _cx: &mut Context, + ) -> Option> { + let template = (&*component).downcast_ref::()?; + template.template().downcast_ref::()?; + Some(Ok(html! { "marker-template-output" })) + } +} + +// **< Default/Admin template identity is independent of the active theme >************************* +// +// `Theme::default_template()`/`admin_template()` were removed: `Context::template()` always +// resolves `Default`/`Admin` to the core `CoreTemplate::Standard`/`Admin` identity, regardless +// of which theme is active. Themes customize the actual rendering by intercepting the `Template` +// component in `handle_component()` instead (see the tests further below). + +#[pagetop::test] +async fn default_template_identity_is_independent_of_theme() { + let cx = Context::new(None); + assert_eq!(cx.template().name(), "standard"); + + let cx = Context::new(None).with_theme(&MarkerTheme); + assert_eq!(cx.template().name(), "standard"); +} + +#[pagetop::test] +async fn admin_template_identity_is_independent_of_theme() { + let mut page = Page::admin(web::test::TestRequest::get().to_http_request()); + assert_eq!(page.context().template().name(), "admin"); + + let mut page = + Page::admin(web::test::TestRequest::get().to_http_request()).with_theme(&MarkerTheme); + assert_eq!(page.context().template().name(), "admin"); +} + +#[pagetop::test] +async fn explicit_template_is_not_overridden_by_a_later_with_theme() { + // A template explicitly set with `with_template()` prevails even if `with_theme()` is called + // afterwards. + let cx = Context::new(None) + .with_template(&CoreTemplate::Admin) + .with_theme(&pagetop::base::theme::Basic); + + assert_eq!(cx.template().name(), "admin"); +} + +// **< A theme customizes rendering via `handle_component()` >************************************** + +#[pagetop::test] +async fn without_a_matching_theme_the_default_composition_is_used() { + setup().await; + + // With no theme intercepting it, and no content registered in any region, the default + // composition (Header + Content + Footer) renders empty. + let mut template = layout::Template::default(); + let html = template.render(&mut Context::default()).await.into_string(); + + assert!(html.is_empty()); +} + +#[pagetop::test] +async fn theme_replaces_template_rendering_via_handle_component() { + setup().await; + + let mut template = layout::Template::default(); + let mut cx = Context::default().with_theme(&MarkerTheme); + let html = template.render(&mut cx).await.into_string(); + + assert_eq!(html, "marker-template-output"); +} + +// **< Page::render() reaches the active theme's `handle_component()` >***************************** + +#[pagetop::test] +async fn page_admin_render_reflects_the_active_theme_template() { + setup().await; + + let request = web::test::TestRequest::get().to_http_request(); + let mut page = Page::admin(request).with_theme(&MarkerTheme); + + let html = page + .render() + .await + .expect("page should render") + .into_string(); + + assert!(html.contains("marker-template-output")); +} diff --git a/tests/theme_template.rs b/tests/theme_template.rs deleted file mode 100644 index 032527e9..00000000 --- a/tests/theme_template.rs +++ /dev/null @@ -1,83 +0,0 @@ -use pagetop::prelude::*; - -// **< Theme with its own template >**************************************************************** - -struct MarkerTemplate; - -#[async_trait] -impl Template for MarkerTemplate { - async fn render(&self, _cx: &mut Context) -> Markup { - html! { "marker-template-output" } - } -} - -struct MarkerTheme; - -#[async_trait] -impl Extension for MarkerTheme { - fn theme(&self) -> Option { - Some(&Self) - } -} - -#[async_trait] -impl Theme for MarkerTheme { - fn default_template(&self) -> TemplateRef { - &MarkerTemplate - } - - fn admin_template(&self) -> TemplateRef { - &MarkerTemplate - } -} - -async fn render_active_template(cx: &mut Context) -> String { - let template = cx.template(); - template.render(cx).await.into_string() -} - -// **< Context::template() follows the active theme >*********************************************** - -#[pagetop::test] -async fn with_theme_updates_the_effective_template() { - // Without changing theme, the active template is not `MarkerTheme`'s. - let mut cx = Context::new(None); - assert_ne!( - render_active_template(&mut cx).await, - "marker-template-output" - ); - - // After changing theme with `with_theme()`, the active template becomes that theme's, with no - // need to call `with_template()` explicitly. - let mut cx = Context::new(None).with_theme(&MarkerTheme); - assert_eq!( - render_active_template(&mut cx).await, - "marker-template-output" - ); -} - -#[pagetop::test] -async fn explicit_template_is_not_overridden_by_a_later_with_theme() { - // A template explicitly set with `with_template()` prevails even if `with_theme()` is called - // afterwards, regardless of order. - let mut cx = Context::new(None) - .with_template(&MarkerTemplate) - .with_theme(&pagetop::base::theme::Basic); - - assert_eq!( - render_active_template(&mut cx).await, - "marker-template-output" - ); -} - -// **< Page::admin() follows the active theme >***************************************************** - -#[pagetop::test] -async fn page_admin_template_follows_a_later_with_theme() { - let request = web::test::TestRequest::get().to_http_request(); - - let mut page = Page::admin(request).with_theme(&MarkerTheme); - let markup = page.context().template().render(page.context()).await; - - assert_eq!(markup.into_string(), "marker-template-output"); -}