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");
-}