From 8ceb6fbd9d0a744e4d68f9f5dbba5b161cb3bb8f Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Tue, 7 Oct 2025 05:56:32 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Mejora=20el=20uso=20de=20regione?= =?UTF-8?q?s=20y=20a=C3=B1ade=20`BasicRegion`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base/theme.rs | 2 +- src/base/theme/basic.rs | 9 ++- src/core/theme.rs | 4 +- src/core/theme/definition.rs | 121 ++++++++++++++++++++++++----------- src/core/theme/regions.rs | 110 ++++++++++++++++--------------- 5 files changed, 148 insertions(+), 98 deletions(-) diff --git a/src/base/theme.rs b/src/base/theme.rs index 40129bf..a4b2df5 100644 --- a/src/base/theme.rs +++ b/src/base/theme.rs @@ -1,4 +1,4 @@ //! Temas básicos soportados por PageTop. mod basic; -pub use basic::Basic; +pub use basic::{Basic, BasicRegion}; diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 97d28b8..b6a982f 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -1,6 +1,9 @@ /// Es el tema básico que incluye PageTop por defecto. use crate::prelude::*; +/// El tema básico usa las mismas regiones predefinidas por [`ThemeRegion`]. +pub type BasicRegion = ThemeRegion; + /// Tema básico por defecto. /// /// Ofrece las siguientes composiciones (*layouts*): @@ -90,9 +93,9 @@ fn render_intro(page: &mut Page) -> Markup { let intro = page.description().unwrap_or_default(); let theme = page.context().theme(); - let h = theme.render_page_region(page, "header"); - let c = theme.render_page_region(page, "content"); - let f = theme.render_page_region(page, "footer"); + let h = theme.render_page_region(page, &BasicRegion::Header); + let c = theme.render_page_region(page, &BasicRegion::Content); + let f = theme.render_page_region(page, &BasicRegion::Footer); let intro_button_txt: L10n = page.param_or_default("intro_button_txt"); let intro_button_lnk: Option<&String> = page.param("intro_button_lnk"); diff --git a/src/core/theme.rs b/src/core/theme.rs index 61d820b..64f40f3 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -15,10 +15,10 @@ //! [`Theme`]. mod definition; -pub use definition::{Theme, ThemePage, ThemeRef}; +pub use definition::{Theme, ThemePage, ThemeRef, ThemeRegion}; mod regions; pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT}; -pub use regions::{InRegion, Region}; +pub use regions::{InRegion, Region, RegionRef}; pub(crate) mod all; diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 5756fb2..7ef95c4 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -1,9 +1,9 @@ use crate::core::extension::Extension; -use crate::core::theme::Region; -use crate::global; +use crate::core::theme::{Region, RegionRef, REGION_CONTENT}; use crate::html::{html, Markup}; use crate::locale::L10n; use crate::response::page::Page; +use crate::{global, join}; use std::sync::LazyLock; @@ -13,6 +13,46 @@ use std::sync::LazyLock; /// implementen [`Theme`] y, a su vez, [`Extension`]. pub type ThemeRef = &'static dyn Theme; +/// Conjunto de regiones que los temas pueden exponer para el renderizado. +/// +/// `ThemeRegion` define un conjunto de regiones predefinidas para estructurar un documento HTML. +/// Proporciona **identificadores estables** (vía [`Region::key()`]) y **etiquetas localizables** +/// (vía [`Region::label()`]) a las regiones donde se añadirán los componentes. +/// +/// Se usa por defecto en [`Theme::page_regions()`](crate::core::theme::Theme::page_regions) y sus +/// variantes representan el conjunto mínimo recomendado para cualquier tema. Sin embargo, cada tema +/// podría exponer su propio conjunto de regiones. +pub enum ThemeRegion { + /// Cabecera de la página. + /// + /// Clave: `"header"`. Suele contener *branding*, navegación principal o avisos globales. + Header, + + /// Contenido principal de la página (**obligatoria**). + /// + /// Clave: `"content"`. Es el destino por defecto para insertar componentes a nivel de página. + Content, + + /// Pie de página. + /// + /// Clave: `"footer"`. Suele contener enlaces legales, créditos o navegación secundaria. + Footer, +} + +impl Region for ThemeRegion { + fn key(&self) -> &str { + match self { + ThemeRegion::Header => "header", + ThemeRegion::Content => REGION_CONTENT, + ThemeRegion::Footer => "footer", + } + } + + 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 @@ -37,14 +77,14 @@ pub trait ThemePage { /// /// Si la región **no produce contenido**, devuelve un `Markup` vacío. #[inline] - fn render_region(&self, page: &mut Page, region: &Region) -> Markup { + fn render_region(&self, page: &mut Page, region: RegionRef) -> Markup { html! { - @let output = page.context().render_components_of(region.key()); + @let key = region.key(); + @let output = page.context().render_components_of(key); @if !output.is_empty() { - @let region_name = region.name(); div - id=(region_name) - class={ "region region--" (region_name) } + id=(key) + class={ "region region--" (key) } role="region" aria-label=[region.label().lookup(page)] { @@ -63,10 +103,10 @@ pub trait ThemePage { /// /// La etiqueta `` no se incluye aquí; únicamente renderiza su contenido. #[inline] - fn render_body(&self, page: &mut Page, regions: &[Region]) -> Markup { + fn render_body(&self, page: &mut Page, regions: &[RegionRef]) -> Markup { html! { @for region in regions { - (self.render_region(page, region)) + (self.render_region(page, *region)) } } } @@ -145,44 +185,53 @@ pub trait Theme: Extension + ThemePage + Send + Sync { /// Declaración ordenada de las regiones disponibles en la página. /// - /// Devuelve una **lista estática** de regiones ([`Region`](crate::core::theme::Region)) con la - /// información necesaria para renderizar el contenedor de cada región. + /// Retorna una **lista estática** de referencias ([`RegionRef`](crate::core::theme::RegionRef)) + /// que representan las regiones que el tema admite dentro del ``. /// - /// Si un tema necesita un conjunto distinto de regiones, se puede **sobrescribir** este método - /// con los siguientes requisitos y recomendaciones: + /// 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 deben ser **estables** (p. ej. `"sidebar-left"`, `"content"`). - /// - La región `"content"` es **obligatoria** porque se usa por defecto para añadir componentes - /// para renderizar. Se puede utilizar [`Region::default()`] para declararla. - /// - La etiqueta `L10n` se evaluará con el idioma activo de la página. + /// - 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. /// - /// Por defecto devuelve: + /// # Ejemplo /// - /// - `"header"`: cabecera. - /// - `"content"`: contenido principal (**obligatoria**). - /// - `"footer"`: pie. - fn page_regions(&self) -> &'static [Region] { - static REGIONS: LazyLock<[Region; 3]> = LazyLock::new(|| { + /// ```rust,ignore + /// fn page_regions(&self) -> &'static [RegionRef] { + /// static REGIONS: LazyLock<[RegionRef; 4]> = LazyLock::new(|| { + /// [ + /// &ThemeRegion::Header, + /// &ThemeRegion::Content, + /// &ThemeRegion::Footer, + /// ] + /// }); + /// &*REGIONS + /// } + /// ``` + fn page_regions(&self) -> &'static [RegionRef] { + static REGIONS: LazyLock<[RegionRef; 3]> = LazyLock::new(|| { [ - Region::declare("header", L10n::l("region_header")), - Region::default(), - Region::declare("footer", L10n::l("region_footer")), + &ThemeRegion::Header, + &ThemeRegion::Content, + &ThemeRegion::Footer, ] }); &*REGIONS } - /// Renderiza una región de la página **por clave**. + /// Renderiza una región de la página. /// - /// Busca en [`page_regions()`](Self::page_regions) la región asociada a una clave y, si existe, - /// delega en [`ThemePage::render_region()`] su renderizado. Si no se encuentra la clave o la - /// región no produce contenido, devuelve un `Markup` vacío. - fn render_page_region(&self, page: &mut Page, key: &str) -> Markup { - html! { - @if let Some(region) = self.page_regions().iter().find(|r| r.key() == key) { - (self.render_region(page, region)) - } - } + /// 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. diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 00ff60c..ecb5eb5 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -19,68 +19,66 @@ static COMMON_REGIONS: LazyLock> = /// Nombre de la región de contenido por defecto (`"content"`). pub const REGION_CONTENT: &str = "content"; -/// Identificador de una región de página. +/// Define la interfaz mínima que describe una **región de renderizado** dentro de una página. /// -/// Incluye una **clave estática** ([`key()`](Self::key)) que identifica la región en el tema, y un -/// **nombre normalizado** ([`name()`](Self::name)) en minúsculas para su uso en atributos HTML -/// (p.ej., clases `region__{name}`). +/// 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. /// -/// Se utiliza para declarar las regiones que componen una página en un tema (ver -/// [`page_regions()`](crate::core::theme::Theme::page_regions)). -pub struct Region { - key: &'static str, - name: String, - label: L10n, -} - -impl Default for Region { - #[inline] - fn default() -> Self { - Self { - key: REGION_CONTENT, - name: REGION_CONTENT.to_string(), - label: L10n::l("region_content"), - } - } -} - -impl Region { - /// Declara una región a partir de su clave estática. +/// 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 [`ThemeRegion`](crate::core::theme::ThemeRegion)), de modo que las claves y etiquetas +/// permanecen inmutables y fácilmente referenciables. +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// pub enum MyThemeRegion { +/// Header, +/// Content, +/// Footer, +/// } +/// +/// impl Region for MyThemeRegion { +/// fn key(&self) -> &str { +/// match self { +/// MyThemeRegion::Header => "header", +/// MyThemeRegion::Content => "content", +/// MyThemeRegion::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. /// - /// Genera además un nombre normalizado de la clave, eliminando espacios iniciales y finales, - /// convirtiendo a minúsculas y sustituyendo los espacios intermedios por guiones (`-`). + /// 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 clave se usará para añadir componentes a la región; por ello se recomiendan nombres - /// sencillos, limitando los caracteres a `[a-z0-9-]` (p.ej., `"sidebar"` o `"main-menu"`), cuyo - /// nombre normalizado coincidirá con la clave. - #[inline] - pub fn declare(key: &'static str, label: L10n) -> Self { - Self { - key, - name: key.trim().to_ascii_lowercase().replace(' ', "-"), - label, - } - } - - /// Devuelve la clave estática asignada a la región. - #[inline] - pub fn key(&self) -> &'static str { - self.key - } - - /// Devuelve el nombre normalizado de la región (para identificadores y atributos HTML). - #[inline] - pub fn name(&self) -> &str { - &self.name - } - - /// Devuelve la etiqueta localizada asociada a la región. - #[inline] - pub fn label(&self) -> &L10n { - &self.label - } + /// 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>);