diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 341d394a..071d24b1 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -95,7 +95,7 @@ impl Extension for SuperMenu { })), )); - InRegion::Named("header").add(Child::with( + InRegion::Key("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 f4670aae..43fb65a5 100644 --- a/extensions/pagetop-aliner/README.md +++ b/extensions/pagetop-aliner/README.md @@ -63,11 +63,10 @@ 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 4ae4121e..cbc0f526 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -64,11 +64,10 @@ 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")) @@ -83,11 +82,15 @@ 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. /// -/// Define un tema mínimo útil para: +/// 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: /// -/// - 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 d6e1666a..84e11b57 100644 --- a/extensions/pagetop-bootsier/README.md +++ b/extensions/pagetop-bootsier/README.md @@ -63,11 +63,10 @@ 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 0bf94f47..76f9d1e2 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -64,11 +64,10 @@ 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")) @@ -102,6 +101,9 @@ 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/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 194cd378..732bbad7 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -54,56 +54,6 @@ pub fn html(input: TokenStream) -> TokenStream { /// [`Default`]. Aunque, a diferencia de un simple `#[derive(Default)]`, el atributo /// `#[derive(AutoDefault)]` permite usar anotaciones en los campos como `#[default = "..."]`, /// funcionando incluso en estructuras con campos que no implementan [`Default`] o en *enums*. -/// -/// # Ejemplos -/// -/// ```rust -/// # use pagetop_macros::AutoDefault; -/// # fn main() { -/// #[derive(AutoDefault)] -/// # #[derive(PartialEq)] -/// # #[allow(dead_code)] -/// enum Foo { -/// Bar, -/// #[default] -/// Baz { -/// #[default = 12] -/// a: i32, -/// b: i32, -/// #[default(Some(Default::default()))] -/// c: Option, -/// #[default(_code = "vec![1, 2, 3]")] -/// d: Vec, -/// #[default = "four"] -/// e: String, -/// }, -/// Qux(i32), -/// } -/// -/// assert!(Foo::default() == Foo::Baz { -/// a: 12, -/// b: 0, -/// c: Some(0), -/// d: vec![1, 2, 3], -/// e: "four".to_owned(), -/// }); -/// # } -/// ``` -/// -/// * `Baz` tiene el atributo `#[default]`. Esto significa que el valor por defecto de `Foo` es -/// `Foo::Baz`. Solo una variante puede tener el atributo `#[default]`, y dicho atributo no debe -/// tener ningún valor asociado. -/// * `a` tiene el atributo `#[default = 12]`. Esto significa que su valor por defecto es `12`. -/// * `b` no tiene ningún atributo `#[default = ...]`. Su valor por defecto será, por tanto, el -/// valor por defecto de `i32`, es decir, `0`. -/// * `c` es un `Option`, y su valor por defecto es `Some(Default::default())`. Rust no puede -/// (actualmente) analizar `#[default = Some(Default::default())]`, pero podemos escribir -/// `#[default(Some(Default::default))]`. -/// * `d` contiene el token `!`, que (actualmente) no puede ser analizado ni siquiera usando -/// `#[default(...)]`, así que debemos codificarlo como una cadena y marcarlo con `_code =`. -/// * `e` es un `String`, por lo que el literal de cadena `"four"` se convierte automáticamente en -/// él. Esta conversión automática **solo** ocurre con literales de cadena (o de bytes), y solo si -/// no se usa `_code`. #[proc_macro_derive(AutoDefault, attributes(default))] pub fn derive_auto_default(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -115,49 +65,46 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream { /// Macro (*attribute*) que asocia un método *builder* `with_` con un método `alter_`. /// -/// La macro añade automáticamente un método `alter_` que permite modificar la instancia actual -/// usando `&mut self`; y redefine el método *builder* `with_`, que consume `mut self`, para delegar -/// la lógica al nuevo método `alter_`, reutilizando así la misma implementación. +/// La macro añade automáticamente un método `alter_` para modificar la instancia actual usando +/// `&mut self`, y redefine el método *builder* `with_`, que consume la instancia (`mut self`), para +/// delegar la lógica de la modificación al nuevo método `alter_`, reutilizando así la misma +/// implementación. /// /// Esta macro emitirá un error en tiempo de compilación si la función anotada no cumple con la /// firma esperada para el método *builder*: `pub fn with_...(mut self, ...) -> Self`. /// -/// # Ejemplo +/// # Ejemplos /// /// Si defines un método `with_` como este: /// -/// ```rust -/// # use pagetop_macros::builder_fn; -/// # struct Example {value: Option}; -/// # impl Example { +/// ```rust,ignore /// #[builder_fn] /// pub fn with_example(mut self, value: impl Into) -> Self { /// self.value = Some(value.into()); /// self /// } -/// # } /// ``` /// -/// la macro rescribirá el método `with_` y generará un nuevo método `alter_`: -/// -/// ```rust -/// # struct Example {value: Option}; -/// # impl Example { -/// #[inline] -/// pub fn with_example(mut self, value: impl Into) -> Self { -/// self.alter_example(value); -/// self -/// } +/// la macro generará automáticamente el siguiente método `alter_`: /// +/// ```rust,ignore /// pub fn alter_example(&mut self, value: impl Into) -> &mut Self { /// self.value = Some(value.into()); /// self /// } -/// # } /// ``` /// -/// De esta forma, cada método *builder* `with_...()` generará automáticamente su correspondiente -/// método `alter_...()` para dejar modificar instancias existentes. +/// y reescribirá el método `with_` para delegar la modificación al método `alter_`: +/// +/// ```rust,ignore +/// pub fn with_example(mut self, value: impl Into) -> Self { +/// self.alter_example(value); +/// self +/// } +/// ``` +/// +/// Así, cada método *builder* `with_...()` generará automáticamente su correspondiente método +/// `alter_...()`, que permitirá más adelante modificar instancias existentes. #[proc_macro_attribute] pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type}; diff --git a/src/base/component.rs b/src/base/component.rs index fa9ed2ad..bdab35c6 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -1,46 +1,48 @@ //! Componentes nativos proporcionados por PageTop. -//! -//! 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. + +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", + } + } +} + +// ************************************************************************************************* mod html; pub use html::Html; -mod region; -pub use region::Region; - -mod template; -pub use template::Template; - mod block; pub use block::Block; @@ -49,3 +51,6 @@ pub use intro::{Intro, IntroOpening}; mod poweredby; pub use poweredby::PoweredBy; + +mod icon; +pub use icon::{Icon, IconKind}; diff --git a/extensions/pagetop-bootsier/src/theme/icon.rs b/src/base/component/icon.rs similarity index 100% rename from extensions/pagetop-bootsier/src/theme/icon.rs rename to src/base/component/icon.rs diff --git a/src/base/component/poweredby.rs b/src/base/component/poweredby.rs index 797253dc..51ab79d8 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 informa del 'Powered by' (*Funciona con*) típica del pie de página. +/// Componente que renderiza la sección '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 deleted file mode 100644 index 5dfa25ce..00000000 --- a/src/base/component/region.rs +++ /dev/null @@ -1,150 +0,0 @@ -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 deleted file mode 100644 index 6c70d00e..00000000 --- a/src/base/component/template.rs +++ /dev/null @@ -1,84 +0,0 @@ -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 4a13a4e4..1e5b1a85 100644 --- a/src/base/theme.rs +++ b/src/base/theme.rs @@ -1,4 +1,4 @@ -//! Tema básico soportados por PageTop. +//! Temas básicos soportados por PageTop. mod basic; -pub use basic::Basic; +pub use basic::{Basic, BasicRegions}; diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index eb2274f6..2d713e37 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 [`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 2a3dfd2d..fbbf8427 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(crate) fn add_action(action: ActionBox) { +pub 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 b3670433..3b8f2abf 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(AutoDefault, Clone)] +#[derive(Clone, Default)] pub struct Children(Vec); impl Children { diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 5cc5d2e8..8c4e47e1 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -1,6 +1,5 @@ -use crate::base::component::Template; use crate::core::component::ChildOp; -use crate::core::theme::all::DEFAULT_THEME; +use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME}; use crate::core::theme::{ChildrenInRegions, ThemeRef}; use crate::core::TypeInfo; use crate::html::{html, Markup}; @@ -14,16 +13,19 @@ 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. @@ -48,27 +50,27 @@ pub enum ContextError { /// Interfaz para gestionar el **contexto de renderizado** de un documento HTML. /// -/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para: +/// `Contextual` extiende [`LangId`] y define los métodos para: /// +/// - Establecer el **idioma** del documento. /// - Almacenar la **solicitud HTTP** de origen. -/// - Seleccionar el **tema** y la **plantilla** de renderizado. +/// - Seleccionar **tema** y **composición** (*layout*) 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 manejan el contexto de renderizado, como +/// Lo implementan, típicamente, estructuras que representan 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_template(Template::DEFAULT) +/// .with_theme("aliner") +/// .with_layout("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"))) @@ -88,11 +90,11 @@ pub trait Contextual: LangId { /// Especifica el tema para renderizar el documento. #[builder_fn] - fn with_theme(self, theme: ThemeRef) -> Self; + fn with_theme(self, theme_name: &'static str) -> Self; - /// Especifica la plantilla para renderizar el documento. + /// Especifica la composición para renderizar el documento. #[builder_fn] - fn with_template(self, template_name: &'static str) -> Self; + fn with_layout(self, layout_name: &'static str) -> Self; /// Añade o modifica un parámetro dinámico del contexto. #[builder_fn] @@ -102,9 +104,9 @@ pub trait Contextual: LangId { #[builder_fn] fn with_assets(self, op: ContextOp) -> Self; - /// Opera con [`ChildOp`] en una región (`region_name`) del documento. + /// Opera con [`ChildOp`] en una región (`region_key`) de la página. #[builder_fn] - fn with_child_in(self, region_name: impl AsRef, op: ChildOp) -> Self; + fn with_child_in(self, region_key: &'static str, op: ChildOp) -> Self; // **< Contextual GETTERS >********************************************************************* @@ -114,8 +116,8 @@ pub trait Contextual: LangId { /// Devuelve el tema que se usará para renderizar el documento. fn theme(&self) -> ThemeRef; - /// Devuelve el nombre de la plantilla usada para renderizar el documento. - fn template(&self) -> &str; + /// Devuelve la composición para renderizar el documento. Por defecto es `"default"`. + fn layout(&self) -> &str; /// Recupera un parámetro como [`Option`]. fn param(&self, key: &'static str) -> Option<&T>; @@ -166,13 +168,12 @@ 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")) -/// // Establece el tema para renderizar. -/// .with_theme(&Aliner) +/// // Selecciona un tema (por su nombre corto). +/// .with_theme("aliner") /// // Asigna un favicon. /// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) /// // Añade una hoja de estilo externa. @@ -207,8 +208,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 usado para renderizar. - template : &'static str, // Nombre de la plantilla usada para renderizar. + theme : ThemeRef, // Referencia al tema para renderizar. + layout : &'static str, // Composición del documento para renderizar. favicon : Option, // Favicon, si se ha definido. stylesheets: Assets, // Hojas de estilo CSS. javascripts: Assets, // Scripts JavaScript. @@ -226,8 +227,8 @@ impl Default for Context { impl Context { /// Crea un nuevo contexto asociado a una solicitud HTTP. /// - /// El contexto inicializa el idioma, el tema y la plantilla por defecto, sin favicon ni otros - /// recursos cargados. + /// El contexto inicializa el idioma, tema y composición por defecto, sin favicon ni recursos + /// cargados. #[rustfmt::skip] pub fn new(request: Option) -> Self { // Se intenta DEFAULT_LANGID. @@ -248,7 +249,7 @@ impl Context { request, langid, theme : *DEFAULT_THEME, - template : Template::DEFAULT, + layout : "default", favicon : None, stylesheets: Assets::::new(), javascripts: Assets::::new(), @@ -286,10 +287,10 @@ impl Context { markup } - /// Renderiza los componentes de la región `region_name`. - pub fn render_region(&mut self, region_name: impl AsRef) -> Markup { + /// Renderiza los componentes de una región (`region_key`). + pub fn render_components_of(&mut self, region_key: &'static str) -> Markup { self.regions - .children_for(self.theme, region_name) + .merge_all_components(self.theme, region_key) .render(self) } @@ -363,7 +364,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 sólo interesa borrar la entrada. + /// Devuelve `false` en caso contrario. Usar cuando solo interesa borrar la entrada. /// /// # Ejemplos /// @@ -410,15 +411,19 @@ 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: ThemeRef) -> Self { - self.theme = theme; + fn with_theme(mut self, theme_name: &'static str) -> Self { + self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); self } #[builder_fn] - fn with_template(mut self, template_name: &'static str) -> Self { - self.template = template_name; + fn with_layout(mut self, layout_name: &'static str) -> Self { + self.layout = layout_name; self } @@ -462,7 +467,7 @@ impl Contextual for Context { ContextOp::RemoveStyleSheet(path) => { self.stylesheets.remove(path); } - // Scripts JavaScript. + // JavaScripts. ContextOp::AddJavaScript(js) => { self.javascripts.add(js); } @@ -474,8 +479,8 @@ impl Contextual for Context { } #[builder_fn] - fn with_child_in(mut self, region_name: impl AsRef, op: ChildOp) -> Self { - self.regions.alter_child_in(region_name, op); + fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { + self.regions.alter_child_in(region_key, op); self } @@ -489,8 +494,8 @@ impl Contextual for Context { self.theme } - fn template(&self) -> &str { - self.template + fn layout(&self) -> &str { + self.layout } /// Recupera un parámetro como [`Option`], simplificando el acceso. diff --git a/src/core/theme.rs b/src/core/theme.rs index 8774276e..28638ba6 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -1,24 +1,25 @@ //! API para añadir y gestionar nuevos temas. //! -//! 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.). +//! 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. //! -//! 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. +//! 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.). //! //! 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, ThemeRef}; +pub use definition::{Theme, ThemePage, ThemeRef, DefaultRegions}; mod regions; -pub(crate) use regions::ChildrenInRegions; -pub use regions::InRegion; +pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT}; +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 de11d1ba..7d21c146 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -1,136 +1,129 @@ -use crate::base::component::Template; -use crate::core::component::{ComponentRender, ContextOp, Contextual}; +use crate::core::component::{ContextOp, Contextual}; use crate::core::extension::Extension; -use crate::global; +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`]. Estas instancias se exponen usando -/// [`Extension::theme()`](crate::core::extension::Extension::theme). +/// implementen [`Theme`] y, a su vez, [`Extension`]. pub type ThemeRef = &'static dyn Theme; -/// Interfaz común que debe implementar cualquier tema de PageTop. +/// Conjunto de regiones predefinidas que los temas pueden exponer para el renderizado. /// -/// 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. +/// `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. /// -/// 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. +/// 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. /// - /// 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: - /// - /// - 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) {} + /// Clave: `"header"`. Suele contener *branding*, navegación principal o avisos globales. + Header, - /// Renderiza el contenido del `` de la página. + /// Contenido principal de la página (**obligatoria**). /// - /// 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()) - } + /// Clave: `"content"`. Es el destino por defecto para insertar componentes a nivel de página. + #[default] + Content, - /// Acciones específicas del tema después de renderizar el `` de la página. + /// Pie de 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"); + /// Clave: `"footer"`. Suele contener enlaces legales, créditos o navegación secundaria. + Footer, +} - 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), - )); +impl Region for DefaultRegions { + fn key(&self) -> &str { + match self { + Self::Header => "header", + Self::Content => REGION_CONTENT, + Self::Footer => "footer", } } - /// Renderiza el contenido del `` de la página. + 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. /// - /// 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. + /// 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. /// - /// 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.). + /// Si la región **no produce contenido**, devuelve un `Markup` vacío. #[inline] - fn render_page_head(&self, page: &mut Page) -> Markup { + 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"; @@ -158,20 +151,155 @@ pub trait Theme: Extension + Send + Sync { (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*". /// - /// 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. + /// 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*". /// - /// 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. + /// 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 {} diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 259417eb..17e15436 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,36 +16,97 @@ 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(crate) struct ChildrenInRegions(HashMap); +pub struct ChildrenInRegions(HashMap<&'static str, Children>); impl ChildrenInRegions { - pub fn with(region_name: impl AsRef, child: Child) -> Self { - Self::default().with_child_in(region_name, ChildOp::Add(child)) + pub fn with(region_key: &'static str, child: Child) -> Self { + ChildrenInRegions::default().with_child_in(region_key, ChildOp::Add(child)) } #[builder_fn] - 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) { + pub fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { + if let Some(region) = self.0.get_mut(region_key) { region.alter_child(op); } else { - self.0 - .insert(name.to_owned(), Children::new().with_child(op)); + self.0.insert(region_key, Children::new().with_child(op)); } self } - pub fn children_for(&self, theme_ref: ThemeRef, region_name: impl AsRef) -> Children { - let name = region_name.as_ref(); + pub fn merge_all_components(&self, theme_ref: ThemeRef, region_key: &'static str) -> Children { let common = COMMON_REGIONS.read(); - 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)]) + 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), + ]) } else { - Children::merge(&[common.0.get(name), self.0.get(name)]) + Children::merge(&[common.0.get(region_key), self.0.get(region_key)]) } } } @@ -59,10 +120,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. - Default, - /// Región identificada por el nombre proporcionado. - Named(&'static str), - /// Región identificada por su nombre para un tema concreto. + Content, + /// Región identificada por la clave proporcionado. + Key(&'static str), + /// Región identificada por una clave para un tema concreto. OfTheme(&'static str, ThemeRef), } @@ -74,38 +135,39 @@ impl InRegion { /// ```rust /// # use pagetop::prelude::*; /// // Banner global, en la región por defecto de cualquier página. - /// InRegion::Default.add(Child::with(Html::with(|_| + /// InRegion::Content.add(Child::with(Html::with(|_| /// html! { ("🎉 ¡Bienvenido!") } /// ))); /// /// // Texto en la región "sidebar". - /// InRegion::Named("sidebar").add(Child::with(Html::with(|_| + /// InRegion::Key("sidebar").add(Child::with(Html::with(|_| /// html! { ("Publicidad") } /// ))); /// ``` pub fn add(&self, child: Child) -> &Self { match self { - 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) => { + 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) => { let mut regions = THEME_REGIONS.write(); if let Some(r) = regions.get_mut(&theme_ref.type_id()) { - r.alter_child_in(region_name, ChildOp::Add(child)); + r.alter_child_in(region_key, ChildOp::Add(child)); } else { regions.insert( theme_ref.type_id(), - ChildrenInRegions::with(region_name, child), + ChildrenInRegions::with(region_key, 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 7d7789d4..036c999c 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -4,10 +4,8 @@ pub use error::ErrorPage; pub use actix_web::Result as ResultPage; use crate::base::action; -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::core::component::{Child, ChildOp, Component, Context, ContextOp, Contextual}; +use crate::core::theme::{ThemeRef, REGION_CONTENT}; use crate::html::{html, Markup, DOCTYPE}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{AttrClasses, ClassesOp}; @@ -111,14 +109,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::DEFAULT, ChildOp::Add(Child::with(component))); + .alter_child_in(REGION_CONTENT, ChildOp::Add(Child::with(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 { + /// 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 { self.context - .alter_child_in(region_name, ChildOp::Add(Child::with(component))); + .alter_child_in(region_key, ChildOp::Add(Child::with(component))); self } @@ -193,11 +191,7 @@ impl Page { action::page::BeforeRenderBody::dispatch(self); // Renderiza el . - 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)) - }; + let body = self.context.theme().render_page_body(self); // Acciones específicas del tema después de renderizar el . self.context.theme().after_render_page_body(self); @@ -222,7 +216,9 @@ 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")) } } }) @@ -251,14 +247,14 @@ impl Contextual for Page { } #[builder_fn] - fn with_theme(mut self, theme: ThemeRef) -> Self { - self.context.alter_theme(theme); + fn with_theme(mut self, theme_name: &'static str) -> Self { + self.context.alter_theme(theme_name); self } #[builder_fn] - fn with_template(mut self, template_name: &'static str) -> Self { - self.context.alter_template(template_name); + fn with_layout(mut self, layout_name: &'static str) -> Self { + self.context.alter_layout(layout_name); self } @@ -275,8 +271,8 @@ impl Contextual for Page { } #[builder_fn] - fn with_child_in(mut self, region_name: impl AsRef, op: ChildOp) -> Self { - self.context.alter_child_in(region_name, op); + fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { + self.context.alter_child_in(region_key, op); self } @@ -290,8 +286,8 @@ impl Contextual for Page { self.context.theme() } - fn template(&self) -> &str { - self.context.template() + fn layout(&self) -> &str { + self.context.layout() } fn param(&self, key: &'static str) -> Option<&T> { diff --git a/src/response/page/error.rs b/src/response/page/error.rs index 7a590e6e..9945a948 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -1,4 +1,4 @@ -use crate::base::component::{Html, Template}; +use crate::base::component::Html; 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_template(Template::ERROR) + .with_layout("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_template(Template::ERROR) + .with_layout("error") .add_child(Html::with(move |_| error404.clone())) .render() {