use crate::core::component::{ContextOp, Contextual}; use crate::core::extension::Extension; use crate::core::theme::{Region, RegionRef, REGION_CONTENT}; 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`]. pub type ThemeRef = &'static dyn Theme; /// Conjunto de regiones predefinidas que los temas pueden exponer para el renderizado. /// /// `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. /// /// 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. /// /// 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. #[default] Content, /// Pie de página. /// /// Clave: `"footer"`. Suele contener enlaces legales, créditos o navegación secundaria. Footer, } impl Region for DefaultRegions { fn key(&self) -> &str { match self { Self::Header => "header", Self::Content => REGION_CONTENT, Self::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 /// 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. /// /// 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. /// /// Si la región **no produce contenido**, devuelve un `Markup` vacío. #[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 { let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; html! { meta charset="utf-8"; @if let Some(title) = page.title() { title { (global::SETTINGS.app.name) (" | ") (title) } } @else { title { (global::SETTINGS.app.name) } } @if let Some(description) = page.description() { meta name="description" content=(description); } meta name="viewport" content=(viewport); @for (name, content) in page.metadata() { meta name=(name) content=(content) {} } meta http-equiv="X-UA-Compatible" content="IE=edge"; @for (property, content) in page.properties() { meta property=(property) content=(content) {} } (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. 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. 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 {}