From 59268e9dddae907c6f7a959ba04d3f81860ee99f Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 22 Nov 2025 09:11:16 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[theme]=20A=C3=B1ade=20componentes?= =?UTF-8?q?=20`Region`=20y=20`Template`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Incluye un componente base `Template` para gestionar la estructura del documento y sus regiones (`Region`). - Actualiza el *trait* `Contextual` para permitir la selección de la plantilla de renderizado. - Modifica `Page` y `Context`, y refactoriza el manejo de temas, para dar soporte al nuevo sistema de plantillas y eliminar la gestión obsoleta de regiones. --- examples/navbar-menus.rs | 2 +- extensions/pagetop-aliner/README.md | 3 +- extensions/pagetop-aliner/src/lib.rs | 11 +- extensions/pagetop-bootsier/README.md | 3 +- extensions/pagetop-bootsier/src/lib.rs | 6 +- .../pagetop-bootsier/src/theme}/icon.rs | 0 src/base/component.rs | 81 ++-- src/base/component/poweredby.rs | 2 +- src/base/component/region.rs | 150 ++++++++ src/base/component/template.rs | 84 +++++ src/base/theme.rs | 4 +- src/base/theme/basic.rs | 3 - src/core/action/all.rs | 2 +- src/core/component/children.rs | 2 +- src/core/component/context.rs | 77 ++-- src/core/theme.rs | 21 +- src/core/theme/definition.rs | 356 ++++++------------ src/core/theme/regions.rs | 130 ++----- src/response/page.rs | 38 +- src/response/page/error.rs | 6 +- 20 files changed, 506 insertions(+), 475 deletions(-) rename {src/base/component => extensions/pagetop-bootsier/src/theme}/icon.rs (100%) create mode 100644 src/base/component/region.rs create mode 100644 src/base/component/template.rs diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 071d24b1..341d394a 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -95,7 +95,7 @@ impl Extension for SuperMenu { })), )); - InRegion::Key("header").add(Child::with( + InRegion::Named("header").add(Child::with( Container::new() .with_width(container::Width::FluidMax(UnitValue::RelRem(75.0))) .add_child(navbar_menu), diff --git a/extensions/pagetop-aliner/README.md b/extensions/pagetop-aliner/README.md index 43fb65a5..f4670aae 100644 --- a/extensions/pagetop-aliner/README.md +++ b/extensions/pagetop-aliner/README.md @@ -63,10 +63,11 @@ theme = "Aliner" ```rust,no_run use pagetop::prelude::*; +use pagetop_aliner::Aliner; async fn homepage(request: HttpRequest) -> ResultPage { Page::new(request) - .with_theme("Aliner") + .with_theme(&Aliner) .add_child( Block::new() .with_title(L10n::l("sample_title")) diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index cbc0f526..4ae4121e 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -64,10 +64,11 @@ theme = "Aliner" ```rust,no_run use pagetop::prelude::*; +use pagetop_aliner::Aliner; async fn homepage(request: HttpRequest) -> ResultPage { Page::new(request) - .with_theme("Aliner") + .with_theme(&Aliner) .add_child( Block::new() .with_title(L10n::l("sample_title")) @@ -82,15 +83,11 @@ async fn homepage(request: HttpRequest) -> ResultPage { use pagetop::prelude::*; -/// El tema usa las mismas regiones predefinidas por [`DefaultRegions`]. -pub type AlinerRegions = DefaultRegions; - /// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML. /// -/// Tema mínimo ideal para **pruebas y demos** que renderiza el **esqueleto HTML** con las mismas -/// regiones básicas definidas por [`DefaultRegions`]. No pretende ser un tema para producción, está -/// pensado para: +/// Define un tema mínimo útil para: /// +/// - Comprobar el funcionamiento de temas, plantillas y regiones. /// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos. /// - Realizar pruebas de renderizado rápido con salida estable y predecible. /// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias. diff --git a/extensions/pagetop-bootsier/README.md b/extensions/pagetop-bootsier/README.md index 84e11b57..d6e1666a 100644 --- a/extensions/pagetop-bootsier/README.md +++ b/extensions/pagetop-bootsier/README.md @@ -63,10 +63,11 @@ theme = "Bootsier" ```rust,no_run use pagetop::prelude::*; +use pagetop_bootsier::Bootsier; async fn homepage(request: HttpRequest) -> ResultPage { Page::new(request) - .with_theme("Bootsier") + .with_theme(&Bootsier) .add_child( Block::new() .with_title(L10n::l("sample_title")) diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 76f9d1e2..0bf94f47 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -64,10 +64,11 @@ theme = "Bootsier" ```rust,no_run use pagetop::prelude::*; +use pagetop_bootsier::Bootsier; async fn homepage(request: HttpRequest) -> ResultPage { Page::new(request) - .with_theme("Bootsier") + .with_theme(&Bootsier) .add_child( Block::new() .with_title(L10n::l("sample_title")) @@ -101,9 +102,6 @@ pub mod prelude { pub use crate::theme::*; } -/// El tema usa las mismas regiones predefinidas por [`DefaultRegions`]. -pub type BootsierRegions = DefaultRegions; - /// Implementa el tema. pub struct Bootsier; diff --git a/src/base/component/icon.rs b/extensions/pagetop-bootsier/src/theme/icon.rs similarity index 100% rename from src/base/component/icon.rs rename to extensions/pagetop-bootsier/src/theme/icon.rs diff --git a/src/base/component.rs b/src/base/component.rs index bdab35c6..fa9ed2ad 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -1,48 +1,46 @@ //! Componentes nativos proporcionados por PageTop. - -use crate::prelude::*; - -// **< FontSize >*********************************************************************************** - -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum FontSize { - ExtraLarge, - XxLarge, - XLarge, - Large, - Medium, - #[default] - Normal, - Small, - XSmall, - XxSmall, - ExtraSmall, -} - -#[rustfmt::skip] -impl FontSize { - #[inline] - pub const fn as_str(self) -> &'static str { - match self { - FontSize::ExtraLarge => "fs__x3l", - FontSize::XxLarge => "fs__x2l", - FontSize::XLarge => "fs__xl", - FontSize::Large => "fs__l", - FontSize::Medium => "fs__m", - FontSize::Normal => "", - FontSize::Small => "fs__s", - FontSize::XSmall => "fs__xs", - FontSize::XxSmall => "fs__x2s", - FontSize::ExtraSmall => "fs__x3s", - } - } -} - -// ************************************************************************************************* +//! +//! Conviene destacar que PageTop distingue entre: +//! +//! - **Componentes estructurales** que definen el esqueleto de un documento HTML, como [`Template`] +//! y [`Region`], utilizados por [`Page`](crate::response::page::Page) para generar la estructura +//! final. +//! - **Componentes de contenido** (menús, barras, tarjetas, etc.), que se incluyen en las regiones +//! gestionadas por los componentes estructurales. +//! +//! El componente [`Template`] describe cómo maquetar el cuerpo del documento a partir de varias +//! regiones lógicas ([`Region`]). En función de la plantilla seleccionada, determina qué regiones +//! se renderizan y en qué orden. Por ejemplo, la plantilla predeterminada [`Template::DEFAULT`] +//! utiliza las regiones [`Region::HEADER`], [`Region::CONTENT`] y [`Region::FOOTER`]. +//! +//! Un componente [`Region`] es un contenedor lógico asociado a un nombre de región. Su contenido se +//! obtiene del [`Context`](crate::core::component::Context), donde los componentes se registran +//! mediante [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in) y +//! otros mecanismos similares, y se integra en el documento a través de [`Template`]. +//! +//! Por su parte, una página ([`Page`](crate::response::page::Page)) representa un documento HTML +//! completo. Implementa [`Contextual`](crate::core::component::Contextual) para mantener su propio +//! [`Context`](crate::core::component::Context), donde gestiona el tema activo, la plantilla +//! seleccionada y los componentes asociados a cada región, y se encarga de generar la estructura +//! final de la página. +//! +//! De este modo, temas y extensiones colaboran sobre una estructura común: las aplicaciones +//! registran componentes en el [`Context`](crate::core::component::Context), las plantillas +//! organizan las regiones y las páginas generan el documento HTML resultante. +//! +//! Los temas pueden sobrescribir [`Template`] para exponer nuevas plantillas o adaptar las +//! predeterminadas, y lo mismo con [`Region`] para añadir regiones adicionales o personalizar su +//! representación. mod html; pub use html::Html; +mod region; +pub use region::Region; + +mod template; +pub use template::Template; + mod block; pub use block::Block; @@ -51,6 +49,3 @@ pub use intro::{Intro, IntroOpening}; mod poweredby; pub use poweredby::PoweredBy; - -mod icon; -pub use icon::{Icon, IconKind}; diff --git a/src/base/component/poweredby.rs b/src/base/component/poweredby.rs index 51ab79d8..797253dc 100644 --- a/src/base/component/poweredby.rs +++ b/src/base/component/poweredby.rs @@ -3,7 +3,7 @@ use crate::prelude::*; // Enlace a la página oficial de PageTop. const LINK: &str = "PageTop"; -/// Componente que renderiza la sección 'Powered by' (*Funciona con*) típica del pie de página. +/// Componente que informa del 'Powered by' (*Funciona con*) típica del pie de página. /// /// Por defecto, usando [`default()`](Self::default) sólo se muestra un reconocimiento a PageTop. /// Sin embargo, se puede usar [`new()`](Self::new) para crear una instancia con un texto de diff --git a/src/base/component/region.rs b/src/base/component/region.rs new file mode 100644 index 00000000..5dfa25ce --- /dev/null +++ b/src/base/component/region.rs @@ -0,0 +1,150 @@ +use crate::prelude::*; + +/// Componente estructural que renderiza el contenido de una región del documento. +/// +/// `Region` actúa como un contenedor lógico asociado a un nombre de región. Su contenido se obtiene +/// del contexto de renderizado ([`Context`]), donde los componentes suelen registrarse con métodos +/// como [`Contextual::with_child_in()`]. Cada región puede integrarse posteriormente en el cuerpo +/// del documento mediante [`Template`], normalmente desde una página ([`Page`]). +#[derive(AutoDefault)] +pub struct Region { + #[default(AttrName::new(Self::DEFAULT))] + name: AttrName, + #[default(L10n::l("region-content"))] + label: L10n, +} + +impl Component for Region { + fn new() -> Self { + Region::default() + } + + fn id(&self) -> Option { + self.name.get() + } + + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + let Some(name) = self.name().get() else { + return PrepareMarkup::None; + }; + let output = cx.render_region(&name); + if output.is_empty() { + return PrepareMarkup::None; + } + PrepareMarkup::With(html! { + div + id=[self.id()] + class=(join!("region region-", &name)) + role="region" + aria-label=[self.label().lookup(cx)] + { + (output) + } + }) + } +} + +impl Region { + /// Región especial situada al **inicio del documento**. + /// + /// Su función es proporcionar un punto estable donde las extensiones puedan inyectar contenido + /// global antes de renderizar el 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. + /// + /// Se considera una región **reservada** para este tipo de usos globales. + pub const PAGETOP: &str = "page-top"; + + /// Región estándar para la **cabecera** del documento. + /// + /// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc. + pub const HEADER: &str = "header"; + + /// Región principal de **contenido**. + /// + /// Es la región donde se espera que se renderice el contenido principal de la página (p. ej. + /// cuerpo de la ruta actual, bloques centrales, vistas principales, etc.). En muchos temas será + /// la región mínima imprescindible para que la página tenga sentido. + pub const CONTENT: &str = "content"; + + /// Región estándar para el **pie de página**. + /// + /// Suele contener información legal, enlaces secundarios, créditos, etc. + pub const FOOTER: &str = "footer"; + + /// Región especial situada al **final del documento**. + /// + /// Pensada para proporcionar un punto estable donde las extensiones puedan inyectar contenido + /// global después de renderizar el 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. + /// + /// Igual que [`Self::PAGETOP`], se considera una región **reservada** para este tipo de usos + /// globales. + pub const PAGEBOTTOM: &str = "page-bottom"; + + /// Región por defecto que se asigna cuando no se especifica ningún nombre. + /// + /// Por diseño, la región por defecto es la de contenido principal ([`Self::CONTENT`]), de + /// manera que un tema sencillo pueda limitarse a definir una sola región funcional. + pub const DEFAULT: &str = Self::CONTENT; + + /// Prepara una región para el nombre indicado. + /// + /// El valor de `name` se utiliza como nombre de la región y como identificador (`id`) del + /// contenedor. Al renderizarse, este componente mostrará el contenido registrado en el contexto + /// bajo ese nombre. + pub fn named(name: impl AsRef) -> Self { + Region { + name: AttrName::new(name), + label: L10n::default(), + } + } + + /// Prepara una región para el nombre indicado con una etiqueta de accesibilidad. + /// + /// El valor de `name` se utiliza como nombre de la región y como identificador (`id`) del + /// contenedor, mientras que `label` será el texto localizado que se usará como `aria-label` del + /// contenedor. + pub fn labeled(name: impl AsRef, label: L10n) -> Self { + Region { + name: AttrName::new(name), + label, + } + } + + // **< Region BUILDER >************************************************************************* + + /// Establece o modifica el nombre de la región. + #[builder_fn] + pub fn with_name(mut self, name: impl AsRef) -> Self { + self.name.alter_value(name); + self + } + + /// Establece la etiqueta localizada de la región. + /// + /// Esta etiqueta se utiliza como `aria-label` del contenedor predefinido `
`, + /// lo que mejora la accesibilidad para lectores de pantalla y otras tecnologías de apoyo. + #[builder_fn] + pub fn with_label(mut self, label: L10n) -> Self { + self.label = label; + self + } + + // **< Region GETTERS >************************************************************************* + + /// Devuelve el nombre de la región. + pub fn name(&self) -> &AttrName { + &self.name + } + + /// Devuelve la etiqueta localizada asociada a la región. + pub fn label(&self) -> &L10n { + &self.label + } +} diff --git a/src/base/component/template.rs b/src/base/component/template.rs new file mode 100644 index 00000000..6c70d00e --- /dev/null +++ b/src/base/component/template.rs @@ -0,0 +1,84 @@ +use crate::prelude::*; + +/// Componente estructural para renderizar plantillas de contenido. +/// +/// `Template` describe cómo se compone el cuerpo del documento a partir de varias regiones lógicas +/// ([`Region`]). En función de su nombre, decide qué regiones se renderizan y en qué orden. +/// +/// Normalmente se invoca desde una página ([`Page`]), que consulta el nombre de plantilla guardado +/// en el [`Context`] y delega en `Template` la composición de las regiones que forman el cuerpo del +/// documento. +/// +/// Los temas pueden sobrescribir este componente para exponer sus propias plantillas o adaptar las +/// plantillas predeterminadas. +#[derive(AutoDefault)] +pub struct Template { + #[default(AttrName::new(Self::DEFAULT))] + name: AttrName, +} + +impl Component for Template { + fn new() -> Self { + Template::default() + } + + fn id(&self) -> Option { + self.name.get() + } + + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + let Some(name) = self.name().get() else { + return PrepareMarkup::None; + }; + match name.as_str() { + Self::DEFAULT | Self::ERROR => PrepareMarkup::With(html! { + (Region::labeled(Region::HEADER, L10n::l("region-header")).render(cx)) + (Region::default().render(cx)) + (Region::labeled(Region::FOOTER, L10n::l("region-footer")).render(cx)) + }), + _ => PrepareMarkup::None, + } + } +} + +impl Template { + /// Nombre de la plantilla predeterminada. + /// + /// Por defecto define una estructura básica con las regiones [`Region::HEADER`], + /// [`Region::CONTENT`] y [`Region::FOOTER`], en ese orden. Esta plantilla se usa cuando no se + /// selecciona ninguna otra de forma explícita (ver [`Contextual::with_template()`]). + pub const DEFAULT: &str = "default"; + + /// Nombre de la plantilla de error. + /// + /// Se utiliza para páginas de error u otros estados excepcionales. Por defecto reutiliza + /// la misma estructura que [`Self::DEFAULT`], pero permite a temas y extensiones distinguir + /// el contexto de error para aplicar estilos o contenidos específicos. + pub const ERROR: &str = "error"; + + /// Selecciona la plantilla asociada al nombre indicado. + /// + /// El valor de `name` se utiliza como nombre de la plantilla y como identificador (`id`) del + /// componente. + pub fn named(name: impl AsRef) -> Self { + Template { + name: AttrName::new(name), + } + } + + // **< Template BUILDER >*********************************************************************** + + /// Establece o modifica el nombre de la plantilla seleccionada. + #[builder_fn] + pub fn with_name(mut self, name: impl AsRef) -> Self { + self.name.alter_value(name); + self + } + + // **< Template GETTERS >*********************************************************************** + + /// Devuelve el nombre de la plantilla seleccionada. + pub fn name(&self) -> &AttrName { + &self.name + } +} diff --git a/src/base/theme.rs b/src/base/theme.rs index 1e5b1a85..4a13a4e4 100644 --- a/src/base/theme.rs +++ b/src/base/theme.rs @@ -1,4 +1,4 @@ -//! Temas básicos soportados por PageTop. +//! Tema básico soportados por PageTop. mod basic; -pub use basic::{Basic, BasicRegions}; +pub use basic::Basic; diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 2d713e37..eb2274f6 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -1,9 +1,6 @@ /// Es el tema básico que incluye PageTop por defecto. use crate::prelude::*; -/// El tema básico usa las mismas regiones predefinidas por [`DefaultRegions`]. -pub type BasicRegions = DefaultRegions; - /// Tema básico por defecto que extiende el funcionamiento predeterminado de [`Theme`]. pub struct Basic; diff --git a/src/core/action/all.rs b/src/core/action/all.rs index fbbf8427..2a3dfd2d 100644 --- a/src/core/action/all.rs +++ b/src/core/action/all.rs @@ -19,7 +19,7 @@ static ACTIONS: LazyLock>> = // // Las extensiones llamarán a esta función durante su inicialización para instalar acciones // personalizadas que modifiquen el comportamiento del *core* o de otros componentes. -pub fn add_action(action: ActionBox) { +pub(crate) fn add_action(action: ActionBox) { let key = ActionKey::new( action.type_id(), action.theme_type_id(), diff --git a/src/core/component/children.rs b/src/core/component/children.rs index 3b8f2abf..b3670433 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -200,7 +200,7 @@ pub enum TypedOp { /// Esta lista permite añadir, modificar, renderizar y consultar componentes hijo en orden de /// inserción, soportando operaciones avanzadas como inserción relativa o reemplazo por /// identificador. -#[derive(Clone, Default)] +#[derive(AutoDefault, Clone)] pub struct Children(Vec); impl Children { diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 8c4e47e1..5cc5d2e8 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -1,5 +1,6 @@ +use crate::base::component::Template; use crate::core::component::ChildOp; -use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME}; +use crate::core::theme::all::DEFAULT_THEME; use crate::core::theme::{ChildrenInRegions, ThemeRef}; use crate::core::TypeInfo; use crate::html::{html, Markup}; @@ -13,19 +14,16 @@ use std::collections::HashMap; /// Operaciones para modificar recursos asociados al contexto ([`Context`]) de un documento. pub enum ContextOp { - // Favicon. /// Define el *favicon* del documento. Sobrescribe cualquier valor anterior. SetFavicon(Option), /// Define el *favicon* solo si no se ha establecido previamente. SetFaviconIfNone(Favicon), - // Stylesheets. /// Añade una hoja de estilos CSS al documento. AddStyleSheet(StyleSheet), /// Elimina una hoja de estilos por su ruta o identificador. RemoveStyleSheet(&'static str), - // JavaScripts. /// Añade un script JavaScript al documento. AddJavaScript(JavaScript), /// Elimina un script por su ruta o identificador. @@ -50,27 +48,27 @@ pub enum ContextError { /// Interfaz para gestionar el **contexto de renderizado** de un documento HTML. /// -/// `Contextual` extiende [`LangId`] y define los métodos para: +/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para: /// -/// - Establecer el **idioma** del documento. /// - Almacenar la **solicitud HTTP** de origen. -/// - Seleccionar **tema** y **composición** (*layout*) de renderizado. +/// - Seleccionar el **tema** y la **plantilla** de renderizado. /// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo /// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`ContextOp`]. /// - Leer y mantener **parámetros dinámicos tipados** de contexto. /// - Generar **identificadores únicos** por tipo de componente. /// -/// Lo implementan, típicamente, estructuras que representan el contexto de renderizado, como +/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como /// [`Context`](crate::core::component::Context) o [`Page`](crate::response::page::Page). /// /// # Ejemplo /// /// ```rust /// # use pagetop::prelude::*; +/// # use pagetop_aliner::Aliner; /// fn prepare_context(cx: C) -> C { /// cx.with_langid(&LangMatch::resolve("es-ES")) -/// .with_theme("aliner") -/// .with_layout("default") +/// .with_theme(&Aliner) +/// .with_template(Template::DEFAULT) /// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) /// .with_assets(ContextOp::AddStyleSheet(StyleSheet::from("/css/app.css"))) /// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/app.js"))) @@ -90,11 +88,11 @@ pub trait Contextual: LangId { /// Especifica el tema para renderizar el documento. #[builder_fn] - fn with_theme(self, theme_name: &'static str) -> Self; + fn with_theme(self, theme: ThemeRef) -> Self; - /// Especifica la composición para renderizar el documento. + /// Especifica la plantilla para renderizar el documento. #[builder_fn] - fn with_layout(self, layout_name: &'static str) -> Self; + fn with_template(self, template_name: &'static str) -> Self; /// Añade o modifica un parámetro dinámico del contexto. #[builder_fn] @@ -104,9 +102,9 @@ pub trait Contextual: LangId { #[builder_fn] fn with_assets(self, op: ContextOp) -> Self; - /// Opera con [`ChildOp`] en una región (`region_key`) de la página. + /// Opera con [`ChildOp`] en una región (`region_name`) del documento. #[builder_fn] - fn with_child_in(self, region_key: &'static str, op: ChildOp) -> Self; + fn with_child_in(self, region_name: impl AsRef, op: ChildOp) -> Self; // **< Contextual GETTERS >********************************************************************* @@ -116,8 +114,8 @@ pub trait Contextual: LangId { /// Devuelve el tema que se usará para renderizar el documento. fn theme(&self) -> ThemeRef; - /// Devuelve la composición para renderizar el documento. Por defecto es `"default"`. - fn layout(&self) -> &str; + /// Devuelve el nombre de la plantilla usada para renderizar el documento. + fn template(&self) -> &str; /// Recupera un parámetro como [`Option`]. fn param(&self, key: &'static str) -> Option<&T>; @@ -168,12 +166,13 @@ pub trait Contextual: LangId { /// /// ```rust /// # use pagetop::prelude::*; +/// # use pagetop_aliner::Aliner; /// fn new_context(request: HttpRequest) -> Context { /// Context::new(Some(request)) /// // Establece el idioma del documento a español. /// .with_langid(&LangMatch::resolve("es-ES")) -/// // Selecciona un tema (por su nombre corto). -/// .with_theme("aliner") +/// // Establece el tema para renderizar. +/// .with_theme(&Aliner) /// // Asigna un favicon. /// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) /// // Añade una hoja de estilo externa. @@ -208,8 +207,8 @@ pub trait Contextual: LangId { pub struct Context { request : Option, // Solicitud HTTP de origen. langid : &'static LanguageIdentifier, // Identificador de idioma. - theme : ThemeRef, // Referencia al tema para renderizar. - layout : &'static str, // Composición del documento para renderizar. + theme : ThemeRef, // Referencia al tema usado para renderizar. + template : &'static str, // Nombre de la plantilla usada para renderizar. favicon : Option, // Favicon, si se ha definido. stylesheets: Assets, // Hojas de estilo CSS. javascripts: Assets, // Scripts JavaScript. @@ -227,8 +226,8 @@ impl Default for Context { impl Context { /// Crea un nuevo contexto asociado a una solicitud HTTP. /// - /// El contexto inicializa el idioma, tema y composición por defecto, sin favicon ni recursos - /// cargados. + /// El contexto inicializa el idioma, el tema y la plantilla por defecto, sin favicon ni otros + /// recursos cargados. #[rustfmt::skip] pub fn new(request: Option) -> Self { // Se intenta DEFAULT_LANGID. @@ -249,7 +248,7 @@ impl Context { request, langid, theme : *DEFAULT_THEME, - layout : "default", + template : Template::DEFAULT, favicon : None, stylesheets: Assets::::new(), javascripts: Assets::::new(), @@ -287,10 +286,10 @@ impl Context { markup } - /// Renderiza los componentes de una región (`region_key`). - pub fn render_components_of(&mut self, region_key: &'static str) -> Markup { + /// Renderiza los componentes de la región `region_name`. + pub fn render_region(&mut self, region_name: impl AsRef) -> Markup { self.regions - .merge_all_components(self.theme, region_key) + .children_for(self.theme, region_name) .render(self) } @@ -364,7 +363,7 @@ impl Context { /// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó. /// - /// Devuelve `false` en caso contrario. Usar cuando solo interesa borrar la entrada. + /// Devuelve `false` en caso contrario. Usar cuando sólo interesa borrar la entrada. /// /// # Ejemplos /// @@ -411,19 +410,15 @@ impl Contextual for Context { self } - /// Asigna el tema para renderizar el documento. - /// - /// Localiza el tema por su [`short_name()`](crate::core::AnyInfo::short_name), y si no aplica - /// ninguno entonces usará el tema por defecto. #[builder_fn] - fn with_theme(mut self, theme_name: &'static str) -> Self { - self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); + fn with_theme(mut self, theme: ThemeRef) -> Self { + self.theme = theme; self } #[builder_fn] - fn with_layout(mut self, layout_name: &'static str) -> Self { - self.layout = layout_name; + fn with_template(mut self, template_name: &'static str) -> Self { + self.template = template_name; self } @@ -467,7 +462,7 @@ impl Contextual for Context { ContextOp::RemoveStyleSheet(path) => { self.stylesheets.remove(path); } - // JavaScripts. + // Scripts JavaScript. ContextOp::AddJavaScript(js) => { self.javascripts.add(js); } @@ -479,8 +474,8 @@ impl Contextual for Context { } #[builder_fn] - fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { - self.regions.alter_child_in(region_key, op); + fn with_child_in(mut self, region_name: impl AsRef, op: ChildOp) -> Self { + self.regions.alter_child_in(region_name, op); self } @@ -494,8 +489,8 @@ impl Contextual for Context { self.theme } - fn layout(&self) -> &str { - self.layout + fn template(&self) -> &str { + self.template } /// Recupera un parámetro como [`Option`], simplificando el acceso. diff --git a/src/core/theme.rs b/src/core/theme.rs index 28638ba6..8774276e 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -1,25 +1,24 @@ //! API para añadir y gestionar nuevos temas. //! -//! En PageTop un tema es la *piel* de la aplicación, decide cómo se muestra cada documento HTML, -//! especialmente las páginas de contenido ([`Page`](crate::response::page::Page)), sin alterar la -//! lógica interna de sus componentes. +//! En PageTop un tema es la *piel* de la aplicación. Es responsable último de los estilos, +//! tipografías, espaciados y cualquier otro detalle visual o interactivo (animaciones, scripts de +//! interfaz, etc.). //! -//! Un tema **declara las regiones** (*cabecera*, *barra lateral*, *pie*, etc.) que estarán -//! disponibles para colocar contenido. Los temas son responsables últimos de los estilos, -//! tipografías, espaciados y cualquier otro detalle visual o de comportamiento (como animaciones, -//! scripts de interfaz, etc.). +//! Un tema determina el aspecto final de un documento HTML sin alterar la lógica interna de los +//! componentes ni la estructura del documento, que queda definida por la plantilla +//! ([`Template`](crate::base::component::Template)) utilizada por cada página. //! //! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension), por //! lo que se instancian, declaran dependencias y se inician igual que cualquier otra extensión. //! También deben implementar [`Theme`] y sobrescribir el método //! [`Extension::theme()`](crate::core::extension::Extension::theme) para que PageTop pueda -//! registrarlos como temas +//! registrarlos como temas. mod definition; -pub use definition::{Theme, ThemePage, ThemeRef, DefaultRegions}; +pub use definition::{Theme, ThemeRef}; mod regions; -pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT}; -pub use regions::{InRegion, Region, RegionRef}; +pub(crate) use regions::ChildrenInRegions; +pub use regions::InRegion; pub(crate) mod all; diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 7d21c146..de11d1ba 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -1,129 +1,136 @@ -use crate::core::component::{ContextOp, Contextual}; +use crate::base::component::Template; +use crate::core::component::{ComponentRender, ContextOp, Contextual}; use crate::core::extension::Extension; -use crate::core::theme::{Region, RegionRef, REGION_CONTENT}; +use crate::global; use crate::html::{html, Markup, StyleSheet}; use crate::locale::L10n; use crate::response::page::Page; -use crate::{global, join, AutoDefault}; - -use std::sync::LazyLock; /// Referencia estática a un tema. /// /// Los temas son también extensiones. Por tanto, deben declararse como **instancias estáticas** que -/// implementen [`Theme`] y, a su vez, [`Extension`]. +/// implementen [`Theme`] y, a su vez, [`Extension`]. Estas instancias se exponen usando +/// [`Extension::theme()`](crate::core::extension::Extension::theme). pub type ThemeRef = &'static dyn Theme; -/// Conjunto de regiones predefinidas que los temas pueden exponer para el renderizado. +/// Interfaz común que debe implementar cualquier tema de PageTop. /// -/// `DefaultRegions` define un conjunto de regiones predefinidas para estructurar un documento HTML. -/// Proporciona **identificadores estables** (vía [`Region::key()`]) y **etiquetas localizables** -/// (vía [`Region::label()`]) a las regiones donde se añadirán los componentes. +/// 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 `` mediante plantillas +/// ([`Template`]) y qué contenido mostrar en las páginas de error. /// -/// Se usa por defecto en [`Theme::page_regions()`](crate::core::theme::Theme::page_regions) y sus -/// variantes representan el conjunto mínimo recomendado para cualquier tema. Sin embargo, cada tema -/// podría exponer su propio conjunto de regiones. -#[derive(AutoDefault)] -pub enum DefaultRegions { - /// Cabecera de la página. +/// 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 +/// predeterminados. +/// +/// El único método **obligatorio** de `Extension` para un tema es [`theme()`](Extension::theme), +/// que debe devolver una referencia estática al propio tema: +/// +/// ```rust +/// # use pagetop::prelude::*; +/// pub struct MyTheme; +/// +/// impl Extension for MyTheme { +/// fn name(&self) -> L10n { +/// L10n::n("My theme") +/// } +/// +/// fn description(&self) -> L10n { +/// L10n::n("A personal theme") +/// } +/// +/// fn theme(&self) -> Option { +/// Some(&Self) +/// } +/// } +/// +/// impl Theme for MyTheme {} +/// ``` +pub trait Theme: Extension + Send + Sync { + /// Acciones específicas del tema antes de renderizar el `` de la página. /// - /// Clave: `"header"`. Suele contener *branding*, navegación principal o avisos globales. - Header, - - /// Contenido principal de la página (**obligatoria**). + /// Se invoca antes de que se procese la plantilla ([`Template`]) asociada a la página + /// ([`Page::template()`](crate::response::page::Page::template)). Es un buen lugar para + /// inicializar o ajustar recursos en función del contexto de la página, por ejemplo: /// - /// Clave: `"content"`. Es el destino por defecto para insertar componentes a nivel de página. - #[default] - Content, + /// - Añadir metadatos o propiedades a la página. + /// - Preparar atributos compartidos. + /// - Registrar *assets* condicionales en el contexto. + #[allow(unused_variables)] + fn before_render_page_body(&self, page: &mut Page) {} - /// Pie de página. + /// Renderiza el contenido del `` de la página. /// - /// Clave: `"footer"`. Suele contener enlaces legales, créditos o navegación secundaria. - Footer, -} + /// Por defecto, delega en la plantilla ([`Template`]) asociada a la página + /// ([`Page::template()`](crate::response::page::Page::template)). La plantilla se encarga de + /// procesar las regiones y renderizar los componentes registrados en el contexto. + /// + /// Los temas pueden sobrescribir este método para: + /// + /// - Forzar una plantilla concreta en determinadas páginas. + /// - Envolver el contenido en marcadores adicionales. + /// - Implementar lógicas de composición alternativas. + #[inline] + fn render_page_body(&self, page: &mut Page) -> Markup { + Template::named(page.template()).render(page.context()) + } -impl Region for DefaultRegions { - fn key(&self) -> &str { - match self { - Self::Header => "header", - Self::Content => REGION_CONTENT, - Self::Footer => "footer", + /// Acciones específicas del tema después de renderizar el `` de la página. + /// + /// Se invoca tras la generación del contenido del ``. Es útil para: + /// + /// - Ajustar o registrar recursos en función de lo que se haya renderizado. + /// - Realizar *tracing* o recopilar métricas. + /// - Aplicar ajustes finales al estado de la página antes de producir el `` o la + /// respuesta final. + /// + /// La implementación por defecto añade una serie de hojas de estilo básicas (`normalize.css`, + /// `root.css`, `basic.css`) cuando el parámetro `include_basic_assets` de la página está + /// activado. + #[allow(unused_variables)] + fn after_render_page_body(&self, page: &mut Page) { + if page.param_or("include_basic_assets", false) { + let pkg_version = env!("CARGO_PKG_VERSION"); + + page.alter_assets(ContextOp::AddStyleSheet( + StyleSheet::from("/css/normalize.css") + .with_version("8.0.1") + .with_weight(-99), + )) + .alter_assets(ContextOp::AddStyleSheet( + StyleSheet::from("/css/root.css") + .with_version(pkg_version) + .with_weight(-99), + )) + .alter_assets(ContextOp::AddStyleSheet( + StyleSheet::from("/css/basic.css") + .with_version(pkg_version) + .with_weight(-99), + )); } } - fn label(&self) -> L10n { - L10n::l(join!("region_", self.key())) - } -} - -/// Métodos predefinidos de renderizado para las páginas de un tema. -/// -/// Contiene las implementaciones base para renderizar las **secciones** `` y ``. Se -/// implementa automáticamente para cualquier tipo que implemente [`Theme`], por lo que normalmente -/// no requiere implementación explícita. -/// -/// Si un tema **sobrescribe** uno o más de los siguientes métodos de [`Theme`]: -/// -/// - [`render_page_region()`](Theme::render_page_region), -/// - [`render_page_head()`](Theme::render_page_head), o -/// - [`render_page_body()`](Theme::render_page_body); -/// -/// puede volver al comportamiento por defecto con una llamada FQS (*Fully Qualified Syntax*) a: -/// -/// - `::render_region(self, page, region)`, -/// - `::render_body(self, page, self.page_regions())`, o -/// - `::render_head(self, page)`. -pub trait ThemePage { - /// Renderiza el **contenedor** de una región concreta del `` de la página. + /// Renderiza el contenido del `` de la página. /// - /// Obtiene los componentes asociados a `region.key()` desde el contexto de la página y, si hay - /// salida, envuelve el contenido en un contenedor `
` predefinido. + /// Aunque en una página el `` se encuentra antes del ``, internamente se renderiza + /// después para contar con los ajustes que hayan ido acumulando los componentes. Por ejemplo, + /// permitiría añadir un archivo de iconos sólo si se ha incluido un icono en la página. /// - /// Si la región **no produce contenido**, devuelve un `Markup` vacío. + /// Por defecto incluye: + /// + /// - La codificación (`charset="utf-8"`). + /// - El título, usando el título de la página si existe y, en caso contrario, sólo el nombre de + /// la aplicación. + /// - La descripción (``), si está definida. + /// - La etiqueta `viewport` básica para diseño adaptable. + /// - Los metadatos (`name`/`content`) y propiedades (`property`/`content`) declarados en la + /// página. + /// - Todos los *assets* registrados en el contexto de la página. + /// + /// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo, + /// *favicons* personalizados, manifest, etiquetas de analítica, etc.). #[inline] - fn render_region(&self, page: &mut Page, region: RegionRef) -> Markup { - html! { - @let key = region.key(); - @let output = page.context().render_components_of(key); - @if !output.is_empty() { - div - id=(key) - class={ "region region--" (key) } - role="region" - aria-label=[region.label().lookup(page)] - { - (output) - } - } - } - } - - /// Renderiza el **contenido interior** del `` de la página. - /// - /// Recorre `regions` en el **orden declarado** y, para cada región con contenido, delega en - /// [`render_region()`](Self::render_region) la generación del contenedor. Las regiones sin - /// contenido **no** producen salida. Se asume que cada identificador de región es **único** - /// dentro de la página. - /// - /// La etiqueta `` no se incluye aquí; únicamente renderiza su contenido. - #[inline] - fn render_body(&self, page: &mut Page, regions: &[RegionRef]) -> Markup { - html! { - @for region in regions { - (self.render_region(page, *region)) - } - } - } - - /// Renderiza el **contenido interior** del `` de la página. - /// - /// Incluye por defecto las etiquetas básicas (`charset`, `title`, `description`, `viewport`, - /// `X-UA-Compatible`), los metadatos (`name/content`) y propiedades (`property/content`), - /// además de los recursos CSS/JS de la página. - /// - /// La etiqueta `` no se incluye aquí; únicamente se renderiza su contenido. - #[inline] - fn render_head(&self, page: &mut Page) -> Markup { + fn render_page_head(&self, page: &mut Page) -> Markup { let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; html! { meta charset="utf-8"; @@ -151,155 +158,20 @@ pub trait ThemePage { (page.context().render_assets()) } } -} - -/// Interfaz común que debe implementar cualquier tema de PageTop. -/// -/// Un tema implementa [`Theme`] y los métodos necesarios de [`Extension`]. El único método -/// **obligatorio** de `Extension` para un tema es [`theme()`](Extension::theme). -/// -/// ```rust -/// # use pagetop::prelude::*; -/// pub struct MyTheme; -/// -/// impl Extension for MyTheme { -/// fn name(&self) -> L10n { -/// L10n::n("My theme") -/// } -/// -/// fn description(&self) -> L10n { -/// L10n::n("A personal theme") -/// } -/// -/// fn theme(&self) -> Option { -/// Some(&Self) -/// } -/// } -/// -/// impl Theme for MyTheme {} -/// ``` -pub trait Theme: Extension + ThemePage + Send + Sync { - /// **Obsoleto desde la versión 0.4.0**: usar [`page_regions()`](Self::page_regions) en su - /// lugar. - #[deprecated(since = "0.4.0", note = "Use `page_regions()` instead")] - fn regions(&self) -> Vec<(&'static str, L10n)> { - vec![("content", L10n::l("content"))] - } - - /// Declaración ordenada de las regiones disponibles en la página. - /// - /// Retorna una **lista estática** de referencias ([`RegionRef`](crate::core::theme::RegionRef)) - /// que representan las regiones que el tema admite dentro del ``. - /// - /// Cada referencia apunta a una instancia que implementa [`Region`](crate::core::theme::Region) - /// para definir cada región de forma segura y estable. Y si un tema necesita un conjunto - /// distinto de regiones, puede **sobrescribir** este método siguiendo estas recomendaciones: - /// - /// - Los identificadores devueltos por [`Region::key()`](crate::core::theme::Region::key) - /// deben ser **estables** (p. ej. `"sidebar-left"`, `"content"`). - /// - La región `"content"` es **obligatoria**, ya que se usa como destino por defecto para - /// insertar componentes y renderizarlos. - /// - El orden de la lista podría tener relevancia como **orden de renderizado** dentro del - /// `` segun la implementación de [`render_page_body()`](Self::render_page_body). - /// - Las etiquetas (`L10n`) de cada región se evaluarán con el idioma activo de la página. - /// - /// # Ejemplo - /// - /// ```rust,ignore - /// fn page_regions(&self) -> &'static [RegionRef] { - /// static REGIONS: LazyLock<[RegionRef; 4]> = LazyLock::new(|| { - /// [ - /// &DefaultRegions::Header, - /// &DefaultRegions::Content, - /// &DefaultRegions::Footer, - /// ] - /// }); - /// &*REGIONS - /// } - /// ``` - fn page_regions(&self) -> &'static [RegionRef] { - static REGIONS: LazyLock<[RegionRef; 3]> = LazyLock::new(|| { - [ - &DefaultRegions::Header, - &DefaultRegions::Content, - &DefaultRegions::Footer, - ] - }); - &*REGIONS - } - - /// Renderiza una región de la página. - /// - /// Si se sobrescribe este método, se puede volver al comportamiento base con: - /// `::render_region(self, page, region)`. - #[inline] - fn render_page_region(&self, page: &mut Page, region: RegionRef) -> Markup { - ::render_region(self, page, region) - } - - /// Acciones específicas del tema antes de renderizar el `` de la página. - /// - /// Útil para preparar clases, inyectar recursos o ajustar metadatos. - #[allow(unused_variables)] - fn before_render_page_body(&self, page: &mut Page) {} - - /// Renderiza el contenido del `` de la página. - /// - /// Si se sobrescribe este método, se puede volver al renderizado base con: - /// `::render_body(self, page, self.page_regions())`. - #[inline] - fn render_page_body(&self, page: &mut Page) -> Markup { - ::render_body(self, page, self.page_regions()) - } - - /// Acciones específicas del tema después de renderizar el `` de la página. - /// - /// Útil para *tracing*, métricas o ajustes finales del estado de la página. - #[allow(unused_variables)] - fn after_render_page_body(&self, page: &mut Page) {} - - /// Renderiza el contenido del `` de la página. - /// - /// Si se sobrescribe este método, se puede volver al renderizado base con: - /// `::render_head(self, page)`. - #[inline] - fn render_page_head(&self, page: &mut Page) -> Markup { - if page.param_or("include_basic_assets", false) { - let pkg_version = env!("CARGO_PKG_VERSION"); - - page.alter_assets(ContextOp::AddStyleSheet( - StyleSheet::from("/css/normalize.css") - .with_version("8.0.1") - .with_weight(-99), - )) - .alter_assets(ContextOp::AddStyleSheet( - StyleSheet::from("/css/root.css") - .with_version(pkg_version) - .with_weight(-99), - )) - .alter_assets(ContextOp::AddStyleSheet( - StyleSheet::from("/css/basic.css") - .with_version(pkg_version) - .with_weight(-99), - )); - } - ::render_head(self, page) - } /// Contenido predeterminado para la página de error "*403 - Forbidden*". /// - /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema. + /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la + /// página de error, manteniendo o no el mensaje de los textos localizados. fn error403(&self, page: &mut Page) -> Markup { html! { div { h1 { (L10n::l("error403_notice").using(page)) } } } } /// Contenido predeterminado para la página de error "*404 - Not Found*". /// - /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema. + /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la + /// página de error, manteniendo o no el mensaje de los textos localizados. fn error404(&self, page: &mut Page) -> Markup { html! { div { h1 { (L10n::l("error404_notice").using(page)) } } } } } - -/// Se implementa automáticamente `ThemePage` para cualquier tema. -impl ThemePage for T {} diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 17e15436..259417eb 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,6 +1,6 @@ +use crate::base::component::Region; use crate::core::component::{Child, ChildOp, Children}; use crate::core::theme::ThemeRef; -use crate::locale::L10n; use crate::{builder_fn, AutoDefault, UniqueId}; use parking_lot::RwLock; @@ -16,97 +16,36 @@ static THEME_REGIONS: LazyLock>> = static COMMON_REGIONS: LazyLock> = LazyLock::new(|| RwLock::new(ChildrenInRegions::default())); -/// Nombre de la región de contenido por defecto (`"content"`). -pub const REGION_CONTENT: &str = "content"; - -/// Define la interfaz mínima que describe una **región de renderizado** dentro de una página. -/// -/// Una *región* representa una zona del documento HTML (por ejemplo: `"header"`, `"content"` o -/// `"sidebar-left"`), en la que se pueden incluir y renderizar componentes dinámicamente. -/// -/// Este `trait` abstrae los metadatos básicos de cada región, esencialmente: -/// -/// - su **clave interna** (`key()`), que la identifica de forma única dentro de la página, y -/// - su **etiqueta localizada** (`label()`), que se usa como texto accesible (por ejemplo en -/// `aria-label` o en descripciones semánticas del contenedor). -/// -/// Las implementaciones típicas son *enumeraciones estáticas* declaradas por cada tema (ver como -/// ejemplo [`DefaultRegions`](crate::core::theme::DefaultRegions)), de modo que las claves y -/// etiquetas permanecen inmutables y fácilmente referenciables. -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// pub enum MyThemeRegions { -/// Header, -/// Content, -/// Footer, -/// } -/// -/// impl Region for MyThemeRegions { -/// fn key(&self) -> &str { -/// match self { -/// Self::Header => "header", -/// Self::Content => "content", -/// Self::Footer => "footer", -/// } -/// } -/// -/// fn label(&self) -> L10n { -/// L10n::l(join!("region__", self.key())) -/// } -/// } -/// ``` -pub trait Region: Send + Sync { - /// Devuelve la **clave interna** que identifica de forma única una región. - /// - /// La clave se utiliza para asociar los componentes de la región con su contenedor HTML - /// correspondiente. Por convención, se emplean nombres en minúsculas y con guiones (`"header"`, - /// `"main"`, `"sidebar-right"`, etc.), y la región `"content"` es **obligatoria** en todos los - /// temas. - fn key(&self) -> &str; - - /// Devuelve la **etiqueta localizada** (`L10n`) asociada a la región. - /// - /// Esta etiqueta se evalúa en el idioma activo de la página y se utiliza principalmente para - /// accesibilidad, como el valor de `aria-label` en el contenedor generado por - /// [`ThemePage::render_region()`](crate::core::theme::ThemePage::render_region). - fn label(&self) -> L10n; -} - -/// Referencia estática a una región. -pub type RegionRef = &'static dyn Region; - // Contenedor interno de componentes agrupados por región. #[derive(AutoDefault)] -pub struct ChildrenInRegions(HashMap<&'static str, Children>); +pub(crate) struct ChildrenInRegions(HashMap); impl ChildrenInRegions { - pub fn with(region_key: &'static str, child: Child) -> Self { - ChildrenInRegions::default().with_child_in(region_key, ChildOp::Add(child)) + pub fn with(region_name: impl AsRef, child: Child) -> Self { + Self::default().with_child_in(region_name, ChildOp::Add(child)) } #[builder_fn] - pub fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { - if let Some(region) = self.0.get_mut(region_key) { + pub fn with_child_in(mut self, region_name: impl AsRef, op: ChildOp) -> Self { + let name = region_name.as_ref(); + if let Some(region) = self.0.get_mut(name) { region.alter_child(op); } else { - self.0.insert(region_key, Children::new().with_child(op)); + self.0 + .insert(name.to_owned(), Children::new().with_child(op)); } self } - pub fn merge_all_components(&self, theme_ref: ThemeRef, region_key: &'static str) -> Children { + pub fn children_for(&self, theme_ref: ThemeRef, region_name: impl AsRef) -> Children { + let name = region_name.as_ref(); let common = COMMON_REGIONS.read(); - if let Some(r) = THEME_REGIONS.read().get(&theme_ref.type_id()) { - Children::merge(&[ - common.0.get(region_key), - self.0.get(region_key), - r.0.get(region_key), - ]) + let themed = THEME_REGIONS.read(); + + if let Some(r) = themed.get(&theme_ref.type_id()) { + Children::merge(&[common.0.get(name), self.0.get(name), r.0.get(name)]) } else { - Children::merge(&[common.0.get(region_key), self.0.get(region_key)]) + Children::merge(&[common.0.get(name), self.0.get(name)]) } } } @@ -120,10 +59,10 @@ impl ChildrenInRegions { /// estas regiones, como las páginas de contenido ([`Page`](crate::response::page::Page)). pub enum InRegion { /// Región de contenido por defecto. - Content, - /// Región identificada por la clave proporcionado. - Key(&'static str), - /// Región identificada por una clave para un tema concreto. + Default, + /// Región identificada por el nombre proporcionado. + Named(&'static str), + /// Región identificada por su nombre para un tema concreto. OfTheme(&'static str, ThemeRef), } @@ -135,39 +74,38 @@ impl InRegion { /// ```rust /// # use pagetop::prelude::*; /// // Banner global, en la región por defecto de cualquier página. - /// InRegion::Content.add(Child::with(Html::with(|_| + /// InRegion::Default.add(Child::with(Html::with(|_| /// html! { ("🎉 ¡Bienvenido!") } /// ))); /// /// // Texto en la región "sidebar". - /// InRegion::Key("sidebar").add(Child::with(Html::with(|_| + /// InRegion::Named("sidebar").add(Child::with(Html::with(|_| /// html! { ("Publicidad") } /// ))); /// ``` pub fn add(&self, child: Child) -> &Self { match self { - InRegion::Content => { - COMMON_REGIONS - .write() - .alter_child_in(REGION_CONTENT, ChildOp::Add(child)); - } - InRegion::Key(region_key) => { - COMMON_REGIONS - .write() - .alter_child_in(region_key, ChildOp::Add(child)); - } - InRegion::OfTheme(region_key, theme_ref) => { + InRegion::Default => Self::add_to_common(Region::DEFAULT, child), + InRegion::Named(region_name) => Self::add_to_common(region_name, child), + InRegion::OfTheme(region_name, theme_ref) => { let mut regions = THEME_REGIONS.write(); if let Some(r) = regions.get_mut(&theme_ref.type_id()) { - r.alter_child_in(region_key, ChildOp::Add(child)); + r.alter_child_in(region_name, ChildOp::Add(child)); } else { regions.insert( theme_ref.type_id(), - ChildrenInRegions::with(region_key, child), + ChildrenInRegions::with(region_name, child), ); } } } self } + + #[inline] + fn add_to_common(region_name: &str, child: Child) { + COMMON_REGIONS + .write() + .alter_child_in(region_name, ChildOp::Add(child)); + } } diff --git a/src/response/page.rs b/src/response/page.rs index 036c999c..7d7789d4 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -4,8 +4,10 @@ pub use error::ErrorPage; pub use actix_web::Result as ResultPage; use crate::base::action; -use crate::core::component::{Child, ChildOp, Component, Context, ContextOp, Contextual}; -use crate::core::theme::{ThemeRef, REGION_CONTENT}; +use crate::base::component::Region; +use crate::core::component::{Child, ChildOp, Component, ComponentRender}; +use crate::core::component::{Context, ContextOp, Contextual}; +use crate::core::theme::ThemeRef; use crate::html::{html, Markup, DOCTYPE}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{AttrClasses, ClassesOp}; @@ -109,14 +111,14 @@ impl Page { /// Añade un componente hijo a la región de contenido por defecto. pub fn add_child(mut self, component: impl Component) -> Self { self.context - .alter_child_in(REGION_CONTENT, ChildOp::Add(Child::with(component))); + .alter_child_in(Region::DEFAULT, ChildOp::Add(Child::with(component))); self } - /// Añade un componente hijo en una región (`region_key`) de la página. - pub fn add_child_in(mut self, region_key: &'static str, component: impl Component) -> Self { + /// Añade un componente hijo en la región `region_name` de la página. + pub fn add_child_in(mut self, region_name: &'static str, component: impl Component) -> Self { self.context - .alter_child_in(region_key, ChildOp::Add(Child::with(component))); + .alter_child_in(region_name, ChildOp::Add(Child::with(component))); self } @@ -191,7 +193,11 @@ impl Page { action::page::BeforeRenderBody::dispatch(self); // Renderiza el . - let body = self.context.theme().render_page_body(self); + let body = html! { + (Region::named(Region::PAGETOP).render(&mut self.context)) + (self.context.theme().render_page_body(self)) + (Region::named(Region::PAGEBOTTOM).render(&mut self.context)) + }; // Acciones específicas del tema después de renderizar el . self.context.theme().after_render_page_body(self); @@ -216,9 +222,7 @@ impl Page { (head) } body id=[self.body_id().get()] class=[self.body_classes().get()] { - (self.context.render_components_of("page-top")) (body) - (self.context.render_components_of("page-bottom")) } } }) @@ -247,14 +251,14 @@ impl Contextual for Page { } #[builder_fn] - fn with_theme(mut self, theme_name: &'static str) -> Self { - self.context.alter_theme(theme_name); + fn with_theme(mut self, theme: ThemeRef) -> Self { + self.context.alter_theme(theme); self } #[builder_fn] - fn with_layout(mut self, layout_name: &'static str) -> Self { - self.context.alter_layout(layout_name); + fn with_template(mut self, template_name: &'static str) -> Self { + self.context.alter_template(template_name); self } @@ -271,8 +275,8 @@ impl Contextual for Page { } #[builder_fn] - fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { - self.context.alter_child_in(region_key, op); + fn with_child_in(mut self, region_name: impl AsRef, op: ChildOp) -> Self { + self.context.alter_child_in(region_name, op); self } @@ -286,8 +290,8 @@ impl Contextual for Page { self.context.theme() } - fn layout(&self) -> &str { - self.context.layout() + fn template(&self) -> &str { + self.context.template() } fn param(&self, key: &'static str) -> Option<&T> { diff --git a/src/response/page/error.rs b/src/response/page/error.rs index 9945a948..7a590e6e 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -1,4 +1,4 @@ -use crate::base::component::Html; +use crate::base::component::{Html, Template}; use crate::core::component::Contextual; use crate::locale::L10n; use crate::response::ResponseError; @@ -33,7 +33,7 @@ impl Display for ErrorPage { let error403 = error_page.theme().error403(&mut error_page); if let Ok(page) = error_page .with_title(L10n::n("Error FORBIDDEN")) - .with_layout("error") + .with_template(Template::ERROR) .add_child(Html::with(move |_| error403.clone())) .render() { @@ -48,7 +48,7 @@ impl Display for ErrorPage { let error404 = error_page.theme().error404(&mut error_page); if let Ok(page) = error_page .with_title(L10n::n("Error RESOURCE NOT FOUND")) - .with_layout("error") + .with_template(Template::ERROR) .add_child(Html::with(move |_| error404.clone())) .render() {