diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index ac29259..6df7042 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -26,7 +26,7 @@ pub type ExtensionRef = &'static dyn Extension; /// } /// ``` pub trait Extension: AnyInfo + Send + Sync { - /// Nombre legible para el usuario. + /// Nombre localizado de la extensión legible para el usuario. /// /// Predeterminado por el [`short_name()`](AnyInfo::short_name) del tipo asociado a la /// extensión. @@ -34,18 +34,15 @@ pub trait Extension: AnyInfo + Send + Sync { L10n::n(self.short_name()) } - /// Descripción corta para paneles, listados, etc. + /// Descripción corta localizada de la extensión para paneles, listados, etc. fn description(&self) -> L10n { L10n::default() } - /// Los temas son extensiones que implementan [`Extension`] y también - /// [`Theme`](crate::core::theme::Theme). + /// Devuelve una referencia a esta misma extensión cuando se trata de un tema. /// - /// Si la extensión no es un tema, este método devuelve `None` por defecto. - /// - /// En caso contrario, este método debe implementarse para devolver una referencia de sí mismo - /// como tema. Por ejemplo: + /// Para ello, debe implementar [`Extension`] y también [`Theme`](crate::core::theme::Theme). Si + /// la extensión no es un tema, este método devuelve `None` por defecto. /// /// ```rust /// use pagetop::prelude::*; @@ -81,7 +78,7 @@ pub trait Extension: AnyInfo + Send + Sync { actions_boxed![] } - /// Inicializa la extensión durante la lógica de arranque de la aplicación. + /// Inicializa la extensión durante la fase de arranque de la aplicación. /// /// Se llama una sola vez, después de que todas las dependencias se han inicializado y antes de /// aceptar cualquier petición HTTP. @@ -104,8 +101,8 @@ pub trait Extension: AnyInfo + Send + Sync { #[allow(unused_variables)] fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {} - /// Permite crear extensiones para deshabilitar y desinstalar los recursos de otras extensiones - /// utilizadas en versiones anteriores de la aplicación. + /// Permite crear extensiones para deshabilitar y desinstalar recursos de otras de versiones + /// anteriores de la aplicación. /// /// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones. fn drop_extensions(&self) -> Vec { diff --git a/src/core/theme.rs b/src/core/theme.rs index 2cd5bb1..aa526f1 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -19,9 +19,6 @@ pub use definition::{Theme, ThemeRef}; mod regions; pub(crate) use regions::ChildrenInRegions; -pub use regions::InRegion; +pub use regions::{InRegion, Region, REGION_CONTENT}; pub(crate) mod all; - -/// Nombre de la región por defecto: `content`. -pub const CONTENT_REGION_NAME: &str = "content"; diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index e9cfbba..8d1b632 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -1,10 +1,12 @@ use crate::core::extension::Extension; -use crate::core::theme::CONTENT_REGION_NAME; +use crate::core::theme::Region; use crate::global; use crate::html::{html, Markup}; use crate::locale::L10n; use crate::response::page::Page; +use std::sync::LazyLock; + /// Representa una referencia a un tema. /// /// Los temas son también extensiones. Por tanto se deben definir igual, es decir, como instancias @@ -38,21 +40,67 @@ pub type ThemeRef = &'static dyn Theme; /// impl Theme for MyTheme {} /// ``` pub trait Theme: Extension + Send + Sync { + /// **Obsoleto desde la versión 0.4.0**: usar [`declared_regions()`](Self::declared_regions) en + /// su lugar. + #[deprecated(since = "0.4.0", note = "Use `declared_regions()` instead")] fn regions(&self) -> Vec<(&'static str, L10n)> { - vec![(CONTENT_REGION_NAME, L10n::l("content"))] + vec![("content", L10n::l("content"))] } + /// Declaración ordenada de las regiones disponibles en la página. + /// + /// Devuelve una lista estática de pares `(Region, L10n)` que se usará para renderizar en el + /// orden indicado todas las regiones que componen una página. Los identificadores deben ser + /// **estables** como `"sidebar-left"` o `"content"`. La etiqueta `L10n` devuelve el nombre de la + /// región en el idioma activo de la página. + /// + /// Si el tema requiere un conjunto distinto de regiones, se puede sobrescribir este método para + /// devolver una lista diferente. Si no, se usará la lista predeterminada: + /// + /// - `"header"`: cabecera. + /// - `"content"`: contenido principal (**obligatoria**). + /// - `"footer"`: pie. + /// + /// Sólo la región `"content"` es obligatoria, usa [`Region::default()`] para declararla. + #[inline] + fn declared_regions(&self) -> &'static [(Region, L10n)] { + static REGIONS: LazyLock<[(Region, L10n); 3]> = LazyLock::new(|| { + [ + (Region::declare("header"), L10n::l("region_header")), + (Region::default(), L10n::l("region_content")), + (Region::declare("footer"), L10n::l("region_footer")), + ] + }); + ®IONS[..] + } + + /// 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. + /// + /// Por defecto, recorre [`declared_regions()`](Self::declared_regions) **en el orden que se han + /// declarado** y, para cada región con contenido, genera un contenedor con `role="region"` y + /// `aria-label` localizado. fn render_page_body(&self, page: &mut Page) -> Markup { html! { body id=[page.body_id().get()] class=[page.body_classes().get()] { - @for (region_name, _) in self.regions() { - @let output = page.render_region(region_name); + @for (region, region_label) in self.declared_regions() { + @let output = page.render_region(region.key()); @if !output.is_empty() { - div id=(region_name) class={ "region-container region-" (region_name) } { - (output) + @let region_name = region.name(); + div + id=(region_name) + class="region" + role="region" + aria-label=[region_label.using(page)] + { + div class={ "region__" (region_name) } { + (output) + } } } } @@ -60,9 +108,16 @@ pub trait Theme: Extension + Send + Sync { } } + /// 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. + /// + /// Por defecto, genera las etiquetas básicas (`charset`, `title`, `description`, `viewport`, + /// `X-UA-Compatible`), los metadatos y propiedades de la página y los recursos (CSS/JS). fn render_page_head(&self, page: &mut Page) -> Markup { let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; html! { @@ -94,11 +149,17 @@ pub trait Theme: Extension + Send + Sync { } } - fn error403(&self, _page: &mut Page) -> Markup { - html! { div { h1 { ("FORBIDDEN ACCESS") } } } + /// Página de error "*403 – Forbidden*" predeterminada. + /// + /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema. + fn error403(&self, page: &mut Page) -> Markup { + html! { div { h1 { (L10n::l("error403_notice").to_markup(page)) } } } } - fn error404(&self, _page: &mut Page) -> Markup { - html! { div { h1 { ("RESOURCE NOT FOUND") } } } + /// Página de error "*404 – Not Found*" predeterminada. + /// + /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema. + fn error404(&self, page: &mut Page) -> Markup { + html! { div { h1 { (L10n::l("error404_notice").to_markup(page)) } } } } } diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 22ab6f2..c8a0555 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,5 +1,5 @@ use crate::core::component::{Child, ChildOp, Children}; -use crate::core::theme::{ThemeRef, CONTENT_REGION_NAME}; +use crate::core::theme::ThemeRef; use crate::{builder_fn, AutoDefault, UniqueId}; use parking_lot::RwLock; @@ -7,15 +7,71 @@ use parking_lot::RwLock; use std::collections::HashMap; use std::sync::LazyLock; -// Regiones globales con componentes para un tema dado. +// Conjunto de regiones globales asociadas a un tema específico. static THEME_REGIONS: LazyLock>> = LazyLock::new(|| RwLock::new(HashMap::new())); -// Regiones globales con componentes para cualquier tema. +// Conjunto de regiones globales comunes a todos los temas. static COMMON_REGIONS: LazyLock> = LazyLock::new(|| RwLock::new(ChildrenInRegions::default())); -// Estructura interna para mantener los componentes de una región. +/// Nombre de la región de contenido por defecto (`"content"`). +pub const REGION_CONTENT: &str = "content"; + +/// Identificador de una región de 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}`). +/// +/// Se utiliza para declarar las regiones que componen una página en un tema (ver +/// [`declared_regions()`](crate::core::theme::Theme::declared_regions)). +pub struct Region { + key: &'static str, + name: String, +} + +impl Default for Region { + #[inline] + fn default() -> Self { + Self { + key: REGION_CONTENT, + name: String::from(REGION_CONTENT), + } + } +} + +impl Region { + /// Declara una región a partir de su clave estática. + /// + /// 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 (`-`). + /// + /// 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) -> Self { + Self { + key, + name: key.trim().to_ascii_lowercase().replace(' ', "-"), + } + } + + /// 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 atributos y búsquedas). + #[inline] + pub fn name(&self) -> &str { + &self.name + } +} + +// Contenedor interno de componentes agrupados por región. #[derive(AutoDefault)] pub struct ChildrenInRegions(HashMap<&'static str, Children>); @@ -48,25 +104,24 @@ impl ChildrenInRegions { } } -/// Permite añadir componentes a regiones globales o regiones de temas concretos. +/// Punto de acceso para añadir componentes a regiones globales o específicas de un tema. /// -/// Dada una región, según la variante seleccionada, se le podrán añadir ([`add()`](Self::add)) -/// componentes que se mantendrán durante la ejecución de la aplicación. +/// Según la variante, se pueden añadir componentes ([`add()`](Self::add)) que permanecerán +/// disponibles durante toda la ejecución. /// -/// Estas estructuras de componentes se renderizarán automáticamente al procesar los documentos HTML -/// que las usan, como las páginas de contenido ([`Page`](crate::response::page::Page)), por -/// ejemplo. +/// Estos componentes se renderizarán automáticamente al procesar los documentos HTML que incluyen +/// estas regiones, como las páginas de contenido ([`Page`](crate::response::page::Page)). pub enum InRegion { - /// Representa la región por defecto en la que se pueden añadir componentes. + /// Región de contenido por defecto. Content, - /// Representa la región con el nombre del argumento. + /// Región identificada por el nombre proporcionado. Named(&'static str), - /// Representa la región con el nombre y del tema especificado en los argumentos. + /// Región identificada por un nombre y asociada a un tema concreto. OfTheme(&'static str, ThemeRef), } impl InRegion { - /// Permite añadir un componente en la región de la variante seleccionada. + /// Añade un componente a la región indicada por la variante. /// /// # Ejemplo /// @@ -88,7 +143,7 @@ impl InRegion { InRegion::Content => { COMMON_REGIONS .write() - .alter_child_in_region(CONTENT_REGION_NAME, ChildOp::Add(child)); + .alter_child_in_region(REGION_CONTENT, ChildOp::Add(child)); } InRegion::Named(name) => { COMMON_REGIONS diff --git a/src/locale/en-US/theme.ftl b/src/locale/en-US/theme.ftl index 9c71e6b..f766766 100644 --- a/src/locale/en-US/theme.ftl +++ b/src/locale/en-US/theme.ftl @@ -1,2 +1,9 @@ -content = Content +# Regions. +region_header = Header +region_content = Content +region_footer = Footer + +error403_notice = FORBIDDEN ACCESS +error404_notice = RESOURCE NOT FOUND + pagetop_logo = PageTop Logo diff --git a/src/locale/es-ES/theme.ftl b/src/locale/es-ES/theme.ftl index f193c53..b8b9144 100644 --- a/src/locale/es-ES/theme.ftl +++ b/src/locale/es-ES/theme.ftl @@ -1,2 +1,9 @@ -content = Contenido +# Regions. +region_header = Cabecera +region_content = Contenido +region_footer = Pie de página + +error403_notice = ACCESO NO PERMITIDO +error404_notice = RECURSO NO ENCONTRADO + pagetop_logo = Logotipo de PageTop diff --git a/src/response/page.rs b/src/response/page.rs index 44cab72..f30e299 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -6,7 +6,7 @@ pub use actix_web::Result as ResultPage; use crate::base::action; use crate::builder_fn; use crate::core::component::{Child, ChildOp, Component}; -use crate::core::theme::{ChildrenInRegions, ThemeRef, CONTENT_REGION_NAME}; +use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT}; use crate::html::{html, AssetsOp, Context, Markup, DOCTYPE}; use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; @@ -123,7 +123,7 @@ impl Page { /// Añade un componente a la región de contenido por defecto. pub fn with_component(mut self, component: impl Component) -> Self { self.regions - .alter_child_in_region(CONTENT_REGION_NAME, ChildOp::Add(Child::with(component))); + .alter_child_in_region(REGION_CONTENT, ChildOp::Add(Child::with(component))); self } @@ -172,11 +172,6 @@ impl Page { self.context.request() } - /// Devuelve el identificador de idioma asociado. - pub fn langid(&self) -> &LanguageIdentifier { - self.context.langid() - } - /// Devuelve el tema que se usará para renderizar la página. pub fn theme(&self) -> ThemeRef { self.context.theme() @@ -250,3 +245,9 @@ impl Page { }) } } + +impl LangId for Page { + fn langid(&self) -> &'static LanguageIdentifier { + self.context.langid() + } +}