♻️ Refactoriza la gestión de regiones y plantillas
This commit is contained in:
parent
bfdc0da407
commit
f2733bb250
15 changed files with 494 additions and 655 deletions
|
|
@ -1,30 +1,26 @@
|
|||
use crate::base::component::Template;
|
||||
use crate::core::component::{ComponentRender, ContextOp, Contextual};
|
||||
use crate::core::component::Contextual;
|
||||
use crate::core::extension::Extension;
|
||||
use crate::global;
|
||||
use crate::html::{html, Markup, StyleSheet};
|
||||
use crate::html::{html, Markup};
|
||||
use crate::locale::L10n;
|
||||
use crate::prelude::{DefaultTemplate, TemplateRef};
|
||||
use crate::response::page::Page;
|
||||
|
||||
/// 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).
|
||||
pub type ThemeRef = &'static dyn Theme;
|
||||
|
||||
/// Interfaz común que debe implementar cualquier tema de PageTop.
|
||||
///
|
||||
/// Un tema es una [`Extension`](crate::core::extension::Extension) que define el aspecto general de
|
||||
/// las páginas: cómo se renderiza el `<head>`, cómo se presenta el `<body>` mediante plantillas
|
||||
/// ([`Template`]) y qué contenido mostrar en las páginas de error.
|
||||
/// las páginas: cómo se renderiza el `<head>`, cómo se presenta el `<body>` usando plantillas
|
||||
/// ([`Template`](crate::core::theme::Template)) que maquetan regiones
|
||||
/// ([`Region`](crate::core::theme::Region)) y qué contenido mostrar en las páginas de error. El
|
||||
/// contenido de cada región depende del [`Context`](crate::core::component::Context) y de su nombre
|
||||
/// lógico.
|
||||
///
|
||||
/// 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:
|
||||
/// que debe devolver una referencia al propio tema:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -47,32 +43,55 @@ pub type ThemeRef = &'static dyn Theme;
|
|||
/// impl Theme for MyTheme {}
|
||||
/// ```
|
||||
pub trait Theme: Extension + Send + Sync {
|
||||
/// Devuelve la plantilla ([`Template`](crate::core::theme::Template)) que el propio tema
|
||||
/// propone como predeterminada.
|
||||
///
|
||||
/// Se utiliza al inicializar un [`Context`](crate::core::component::Context) o una página
|
||||
/// ([`Page`](crate::response::page::Page)) por si no se elige ninguna otra plantilla con
|
||||
/// [`Contextual::with_template()`](crate::core::component::Contextual::with_template).
|
||||
///
|
||||
/// La implementación por defecto devuelve la plantilla estándar ([`DefaultTemplate::Standard`])
|
||||
/// con una estructura básica para la página. Los temas pueden sobrescribir este método para
|
||||
/// seleccionar otra plantilla predeterminada o una plantilla propia.
|
||||
#[inline]
|
||||
fn default_template(&self) -> TemplateRef {
|
||||
&DefaultTemplate::Standard
|
||||
}
|
||||
|
||||
/// Acciones específicas del tema antes de renderizar el `<body>` 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:
|
||||
/// 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.
|
||||
/// - Añadir metadatos o propiedades a la cabecera de la página.
|
||||
/// - Preparar atributos compartidos.
|
||||
/// - Registrar *assets* condicionales en el contexto.
|
||||
///
|
||||
/// La implementación por defecto no realiza ninguna acción.
|
||||
#[allow(unused_variables)]
|
||||
fn before_render_page_body(&self, page: &mut Page) {}
|
||||
|
||||
/// Renderiza el contenido del `<body>` de la página.
|
||||
///
|
||||
/// 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.
|
||||
/// La implementación predeterminada delega en la plantilla asociada a la página, obtenida desde
|
||||
/// su [`Context`](crate::core::component::Context), y llama a
|
||||
/// [`Template::render()`](crate::core::theme::Template::render) para componer el `<body>` a
|
||||
/// partir de las regiones.
|
||||
///
|
||||
/// Con la configuración por defecto, la plantilla estándar utiliza las regiones
|
||||
/// [`DefaultRegion::Header`](crate::core::theme::DefaultRegion::Header),
|
||||
/// [`DefaultRegion::Content`](crate::core::theme::DefaultRegion::Content) y
|
||||
/// [`DefaultRegion::Footer`](crate::core::theme::DefaultRegion::Footer) en ese orden.
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para:
|
||||
///
|
||||
/// - Forzar una plantilla concreta en determinadas páginas.
|
||||
/// - Envolver el contenido en marcadores adicionales.
|
||||
/// - Consultar la plantilla de la página y variar la composición según su nombre.
|
||||
/// - Envolver el contenido en contenedores 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())
|
||||
page.template().render(page.context())
|
||||
}
|
||||
|
||||
/// Acciones específicas del tema después de renderizar el `<body>` de la página.
|
||||
|
|
@ -83,6 +102,8 @@ pub trait Theme: Extension + Send + Sync {
|
|||
/// - Realizar *tracing* o recopilar métricas.
|
||||
/// - Aplicar ajustes finales al estado de la página antes de producir el `<head>` o la
|
||||
/// respuesta final.
|
||||
///
|
||||
/// La implementación por defecto no realiza ninguna acción.
|
||||
#[allow(unused_variables)]
|
||||
fn after_render_page_body(&self, page: &mut Page) {}
|
||||
|
||||
|
|
@ -101,34 +122,11 @@ pub trait Theme: Extension + Send + Sync {
|
|||
/// - La etiqueta `viewport` básica para diseño adaptable.
|
||||
/// - Los metadatos (`name`/`content`) y propiedades (`property`/`content`) declarados en la
|
||||
/// página.
|
||||
/// - Los *assets* registrados en el contexto de la página. Si el parámetro
|
||||
/// `include_basic_assets` está activado, añade de serie las siguientes hojas de estilo
|
||||
/// básicas: `normalize.css`, `root.css`, `basic.css`, útiles para temas sencillos o de uso
|
||||
/// general.
|
||||
/// - Los *assets* registrados en el contexto de la página.
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo,
|
||||
/// *favicons* personalizados, manifest, etiquetas de analítica, etc.).
|
||||
#[inline]
|
||||
fn render_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),
|
||||
));
|
||||
}
|
||||
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
|
||||
html! {
|
||||
meta charset="utf-8";
|
||||
|
|
@ -173,3 +171,6 @@ pub trait Theme: Extension + Send + Sync {
|
|||
html! { div { h1 { (L10n::l("error404_notice").using(page)) } } }
|
||||
}
|
||||
}
|
||||
|
||||
/// Referencia estática a un tema.
|
||||
pub type ThemeRef = &'static dyn Theme;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use crate::base::component::Region;
|
||||
use crate::core::component::{Child, ChildOp, Children};
|
||||
use crate::core::theme::ThemeRef;
|
||||
use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef};
|
||||
use crate::{builder_fn, AutoDefault, UniqueId};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
|
@ -21,24 +20,23 @@ static COMMON_REGIONS: LazyLock<RwLock<ChildrenInRegions>> =
|
|||
pub(crate) struct ChildrenInRegions(HashMap<String, Children>);
|
||||
|
||||
impl ChildrenInRegions {
|
||||
pub fn with(region_name: impl AsRef<str>, child: Child) -> Self {
|
||||
Self::default().with_child_in(region_name, ChildOp::Add(child))
|
||||
pub fn with(region_ref: RegionRef, child: Child) -> Self {
|
||||
Self::default().with_child_in(region_ref, ChildOp::Add(child))
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_child_in(mut self, region_name: impl AsRef<str>, 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_ref: RegionRef, op: ChildOp) -> Self {
|
||||
if let Some(region) = self.0.get_mut(region_ref.name()) {
|
||||
region.alter_child(op);
|
||||
} else {
|
||||
self.0
|
||||
.insert(name.to_owned(), Children::new().with_child(op));
|
||||
.insert(region_ref.name().to_owned(), Children::new().with_child(op));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn children_for(&self, theme_ref: ThemeRef, region_name: impl AsRef<str>) -> Children {
|
||||
let name = region_name.as_ref();
|
||||
pub fn children_for(&self, theme_ref: ThemeRef, region_ref: RegionRef) -> Children {
|
||||
let name = region_ref.name();
|
||||
let common = COMMON_REGIONS.read();
|
||||
let themed = THEME_REGIONS.read();
|
||||
|
||||
|
|
@ -50,20 +48,36 @@ impl ChildrenInRegions {
|
|||
}
|
||||
}
|
||||
|
||||
/// Permite añadir componentes a regiones globales o específicas de un tema.
|
||||
/// Añade componentes a regiones globales o específicas de un tema.
|
||||
///
|
||||
/// Según la variante, se pueden añadir componentes ([`add()`](Self::add)) que permanecerán
|
||||
/// disponibles durante toda la ejecución.
|
||||
///
|
||||
/// 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)).
|
||||
/// Cada variante indica la región en la que se añade el componente usando [`Self::add()`]. Los
|
||||
/// componentes añadidos se mantienen durante toda la ejecución y se inyectan automáticamente al
|
||||
/// renderizar los documentos HTML que utilizan esas 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.
|
||||
OfTheme(&'static str, ThemeRef),
|
||||
/// Región principal de **contenido** por defecto.
|
||||
///
|
||||
/// Añade el componente a la región lógica de contenido principal de la aplicación. Por
|
||||
/// convención, esta región corresponde a [`DefaultRegion::Content`], cuyo nombre es
|
||||
/// `"content"`. Cualquier tema que renderice esa misma región de contenido, ya sea usando
|
||||
/// directamente [`DefaultRegion::Content`] o cualquier otra implementación de
|
||||
/// [`Region`](crate::core::theme::Region) que devuelva ese mismo nombre, mostrará los
|
||||
/// componentes registrados aquí, aunque lo harán según su propio método de renderizado
|
||||
/// ([`Region::render()`](crate::core::theme::Region::render)).
|
||||
Content,
|
||||
/// Región global compartida por todos los temas.
|
||||
///
|
||||
/// Los componentes añadidos aquí se asocian al nombre de la región indicado por [`RegionRef`],
|
||||
/// es decir, al valor devuelto por [`Region::name()`](crate::core::theme::Region::name) para
|
||||
/// esa región. Se mostrarán en cualquier tema cuya plantilla renderice una región que devuelva
|
||||
/// ese mismo nombre.
|
||||
Global(RegionRef),
|
||||
/// Región asociada a un tema concreto.
|
||||
///
|
||||
/// Los componentes sólo se renderizarán cuando el documento se procese con el tema indicado y
|
||||
/// se utilice la región referenciada. Resulta útil para añadir contenido específico en un tema
|
||||
/// sin afectar a otros.
|
||||
ForTheme(ThemeRef, RegionRef),
|
||||
}
|
||||
|
||||
impl InRegion {
|
||||
|
|
@ -73,28 +87,33 @@ 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(|_|
|
||||
/// html! { ("🎉 ¡Bienvenido!") }
|
||||
/// )));
|
||||
/// // Banner global en la región por defecto.
|
||||
/// InRegion::Content.add(Child::with(Html::with(|_| {
|
||||
/// html! { "🎉 ¡Bienvenido!" }
|
||||
/// })));
|
||||
///
|
||||
/// // Texto en la región "sidebar".
|
||||
/// InRegion::Named("sidebar").add(Child::with(Html::with(|_|
|
||||
/// html! { ("Publicidad") }
|
||||
/// )));
|
||||
/// // Texto en la cabecera.
|
||||
/// InRegion::Global(&DefaultRegion::Header).add(Child::with(Html::with(|_| {
|
||||
/// html! { "Publicidad" }
|
||||
/// })));
|
||||
///
|
||||
/// // Contenido sólo para la región del pie de página en un tema concreto.
|
||||
/// InRegion::ForTheme(&theme::Basic, &DefaultRegion::Footer).add(Child::with(Html::with(|_| {
|
||||
/// html! { "Aviso legal" }
|
||||
/// })));
|
||||
/// ```
|
||||
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 => Self::add_to_common(&DefaultRegion::Content, child),
|
||||
InRegion::Global(region_ref) => Self::add_to_common(*region_ref, child),
|
||||
InRegion::ForTheme(theme_ref, region_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_ref, ChildOp::Add(child));
|
||||
} else {
|
||||
regions.insert(
|
||||
theme_ref.type_id(),
|
||||
ChildrenInRegions::with(region_name, child),
|
||||
ChildrenInRegions::with(*region_ref, child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -103,9 +122,9 @@ impl InRegion {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
fn add_to_common(region_name: &str, child: Child) {
|
||||
fn add_to_common(region_ref: RegionRef, child: Child) {
|
||||
COMMON_REGIONS
|
||||
.write()
|
||||
.alter_child_in(region_name, ChildOp::Add(child));
|
||||
.alter_child_in(region_ref, ChildOp::Add(child));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue