♻️ Refactoriza la gestión de regiones y plantillas

This commit is contained in:
Manuel Cillero 2025-11-30 00:16:54 +01:00
parent 0746bbbee7
commit 9f79e4179a
15 changed files with 494 additions and 655 deletions

View file

@ -1,13 +1,27 @@
//! Responde a una petición web generando una página HTML completa.
//!
//! Este módulo define [`Page`], que representa una página HTML lista para renderizar. Cada página
//! se construye a partir de un [`Context`] propio, donde se registran el tema activo, la plantilla
//! ([`Template`](crate::core::theme::Template)) que define la disposición de las regiones
//! ([`Region`]), los componentes asociados y los recursos adicionales (hojas de estilo, scripts,
//! *favicon*, etc.).
//!
//! El renderizado ([`Page::render()`]) delega en el tema ([`Theme`](crate::core::theme::Theme)) la
//! composición del `<head>` y del `<body>`, y se ejecutan las acciones registradas por las
//! extensiones antes y después de generar los contenidos.
//!
//! También introduce regiones internas reservadas ([`ReservedRegion`]) que actúan como puntos de
//! anclaje globales al inicio y al final del documento.
mod error;
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::{Child, ChildOp, Component};
use crate::core::component::{Context, ContextOp, Contextual};
use crate::core::theme::ThemeRef;
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
use crate::html::{html, Markup, DOCTYPE};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::html::{AttrClasses, ClassesOp};
@ -16,6 +30,57 @@ use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
use crate::service::HttpRequest;
use crate::{builder_fn, AutoDefault};
// **< ReservedRegion >*****************************************************************************
/// Regiones internas reservadas como puntos de anclaje globales.
///
/// Representan contenedores especiales situados al inicio y al final de un documento. Están
/// pensadas para proporcionar regiones donde inyectar contenido global o técnico. No suelen usarse
/// como regiones visibles en los temas.
pub enum ReservedRegion {
/// Región interna situada al **inicio del documento**.
///
/// Su función es proporcionar un contenedor donde las extensiones puedan inyectar contenido
/// global antes del 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.
PageTop,
/// Región interna situada al **final del documento**.
///
/// Pensada para proporcionar un contenedor donde las extensiones puedan inyectar contenido
/// global después del 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.
PageBottom,
}
impl Region for ReservedRegion {
#[inline]
fn name(&self) -> &'static str {
match self {
Self::PageTop => "page-top",
Self::PageBottom => "page-bottom",
}
}
#[inline]
fn label(&self) -> L10n {
L10n::default()
}
}
// **< Page >***************************************************************************************
/// Representa una página HTML completa lista para renderizar.
///
/// Una instancia de `Page` se compone dinámicamente permitiendo establecer título, descripción,
@ -77,7 +142,7 @@ impl Page {
/// Añade una entrada `<meta property="..." content="...">` al `<head>`.
#[builder_fn]
pub fn with_property(mut self, property: &'static str, content: &'static str) -> Self {
self.metadata.push((property, content));
self.properties.push((property, content));
self
}
@ -97,15 +162,17 @@ 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)));
self.context.alter_child_in(
&DefaultRegion::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 {
pub fn add_child_in(mut self, region_ref: RegionRef, component: impl Component) -> Self {
self.context
.alter_child_in(region_name, ChildOp::Add(Child::with(component)));
.alter_child_in(region_ref, ChildOp::Add(Child::with(component)));
self
}
@ -154,8 +221,30 @@ impl Page {
/// Renderiza la página completa en formato HTML.
///
/// Ejecuta las acciones correspondientes antes y después de renderizar el `<body>`,
/// así como del `<head>`, e inserta los atributos `lang` y `dir` en la etiqueta `<html>`.
/// El proceso de renderizado de la página sigue esta secuencia:
///
/// 1. Ejecuta
/// [`Theme::before_render_page_body()`](crate::core::theme::Theme::before_render_page_body)
/// para que el tema pueda ejecutar acciones específicas antes de renderizar el `<body>`.
/// 2. Despacha [`action::page::BeforeRenderBody`] para que otras extensiones puedan realizar
/// ajustes previos sobre la página.
/// 3. **Construye el contenido del `<body>`**:
/// - Renderiza la región reservada superior ([`ReservedRegion::PageTop`]).
/// - Llama a [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para
/// renderizar las regiones del cuerpo principal de la página.
/// - Renderiza la región reservada inferior ([`ReservedRegion::PageBottom`]).
/// 4. Ejecuta
/// [`Theme::after_render_page_body()`](crate::core::theme::Theme::after_render_page_body)
/// para que el tema pueda aplicar ajustes finales.
/// 5. Despacha [`action::page::AfterRenderBody`] para permitir que otras extensiones realicen
/// sus últimos ajustes tras generar el `<body>`.
/// 6. Renderiza el `<head>` llamando a
/// [`Theme::render_page_head()`](crate::core::theme::Theme::render_page_head).
/// 7. Obtiene el idioma y la dirección del texto a partir de
/// [`Context::langid()`](crate::core::component::Context::langid) e inserta los atributos
/// `lang` y `dir` en la etiqueta `<html>`.
/// 8. Compone el documento HTML completo (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) y
/// devuelve un [`ResultPage`] con el [`Markup`] final.
pub fn render(&mut self) -> ResultPage<Markup, ErrorPage> {
// Acciones específicas del tema antes de renderizar el <body>.
self.context.theme().before_render_page_body(self);
@ -165,9 +254,9 @@ impl Page {
// Renderiza el <body>.
let body = html! {
(Region::named(Region::PAGETOP).render(&mut self.context))
(ReservedRegion::PageTop.render(&mut self.context))
(self.context.theme().render_page_body(self))
(Region::named(Region::PAGEBOTTOM).render(&mut self.context))
(ReservedRegion::PageBottom.render(&mut self.context))
};
// Acciones específicas del tema después de renderizar el <body>.
@ -228,8 +317,8 @@ impl Contextual for Page {
}
#[builder_fn]
fn with_template(mut self, template_name: &'static str) -> Self {
self.context.alter_template(template_name);
fn with_template(mut self, template: TemplateRef) -> Self {
self.context.alter_template(template);
self
}
@ -246,8 +335,8 @@ impl Contextual for Page {
}
#[builder_fn]
fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
self.context.alter_child_in(region_name, op);
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
self.context.alter_child_in(region_ref, op);
self
}
@ -261,7 +350,7 @@ impl Contextual for Page {
self.context.theme()
}
fn template(&self) -> &str {
fn template(&self) -> TemplateRef {
self.context.template()
}

View file

@ -1,5 +1,6 @@
use crate::base::component::{Html, Template};
use crate::base::component::Html;
use crate::core::component::Contextual;
use crate::core::theme::DefaultTemplate;
use crate::locale::L10n;
use crate::response::ResponseError;
use crate::service::http::{header::ContentType, StatusCode};
@ -7,8 +8,21 @@ use crate::service::{HttpRequest, HttpResponse};
use super::Page;
use std::fmt::{self, Display};
use std::fmt;
/// Página de error asociada a un código de estado HTTP.
///
/// Este enumerado agrupa los distintos tipos de error que pueden devolverse como página HTML
/// completa. Cada variante encapsula la solicitud original ([`HttpRequest`]) y se corresponde con
/// un código de estado concreto.
///
/// Para algunos errores (como [`ErrorPage::AccessDenied`] y [`ErrorPage::NotFound`]) se construye
/// una [`Page`] usando la plantilla de error del tema activo ([`DefaultTemplate::Error`]), lo que
/// permite personalizar el contenido del mensaje. En el resto de casos se devuelve un cuerpo HTML
/// mínimo basado en una descripción genérica del error.
///
/// `ErrorPage` implementa [`ResponseError`], por lo que puede utilizarse directamente como tipo de
/// error en los controladores HTTP.
#[derive(Debug)]
pub enum ErrorPage {
NotModified(HttpRequest),
@ -20,7 +34,7 @@ pub enum ErrorPage {
Timeout(HttpRequest),
}
impl Display for ErrorPage {
impl fmt::Display for ErrorPage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
// Error 304.
@ -33,7 +47,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_template(&DefaultTemplate::Error)
.add_child(Html::with(move |_| error403.clone()))
.render()
{
@ -48,7 +62,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_template(&DefaultTemplate::Error)
.add_child(Html::with(move |_| error404.clone()))
.render()
{