From a1bb6cd12db4bb6aced9fb259503490f615e9c3c Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 27 Jul 2025 21:24:49 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20A=C3=B1ade=20soporte=20para=20respo?= =?UTF-8?q?nder=20p=C3=A1ginas=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Amplia la estructura "Page" para trabajar el renderizado con regiones de componentes para componer la página. - Añade acciones "BeforeRenderBody" y "AfterRenderBody" para alterar el contenido de la página antes y después del renderizado. - Actualiza "Context" para admitir parámetros dinámicos y mejorar la gestión de temas. - Implementa el manejo de errores HTTP respondiendo páginas. - Mejora la documentación y reorganiza el código en varios módulos. --- src/base/action.rs | 2 + .../component/after_render_component.rs | 2 +- .../component/before_render_component.rs | 2 +- src/base/action/component/is_renderable.rs | 2 +- src/base/action/page.rs | 17 ++ src/base/action/page/after_render_body.rs | 46 ++++ src/base/action/page/before_render_body.rs | 46 ++++ src/core/component/children.rs | 96 +++---- src/core/theme.rs | 3 + src/core/theme/all.rs | 4 +- src/core/theme/definition.rs | 66 ++++- src/core/theme/regions.rs | 47 ++-- src/html/context.rs | 138 +++++----- src/prelude.rs | 2 +- src/response.rs | 2 + src/response/page.rs | 249 ++++++++++++++++++ src/response/page/error.rs | 88 +++++++ 17 files changed, 669 insertions(+), 143 deletions(-) create mode 100644 src/base/action/page.rs create mode 100644 src/base/action/page/after_render_body.rs create mode 100644 src/base/action/page/before_render_body.rs create mode 100644 src/response/page.rs create mode 100644 src/response/page/error.rs diff --git a/src/base/action.rs b/src/base/action.rs index 119a7ea..be35e92 100644 --- a/src/base/action.rs +++ b/src/base/action.rs @@ -13,3 +13,5 @@ pub type FnActionWithComponent = fn(component: &mut C, cx: &mut Context); pub mod component; pub mod theme; + +pub mod page; diff --git a/src/base/action/component/after_render_component.rs b/src/base/action/component/after_render_component.rs index 3c8ead9..48873f7 100644 --- a/src/base/action/component/after_render_component.rs +++ b/src/base/action/component/after_render_component.rs @@ -22,7 +22,7 @@ impl ActionDispatcher for AfterRender { self.referer_id.get() } - /// Devuelve el peso para definir el orden de aplicación. + /// Devuelve el peso para definir el orden de ejecución. fn weight(&self) -> Weight { self.weight } diff --git a/src/base/action/component/before_render_component.rs b/src/base/action/component/before_render_component.rs index 0ebe409..483b916 100644 --- a/src/base/action/component/before_render_component.rs +++ b/src/base/action/component/before_render_component.rs @@ -22,7 +22,7 @@ impl ActionDispatcher for BeforeRender { self.referer_id.get() } - /// Devuelve el peso para definir el orden de aplicación. + /// Devuelve el peso para definir el orden de ejecución. fn weight(&self) -> Weight { self.weight } diff --git a/src/base/action/component/is_renderable.rs b/src/base/action/component/is_renderable.rs index 7ba7d53..3f0f163 100644 --- a/src/base/action/component/is_renderable.rs +++ b/src/base/action/component/is_renderable.rs @@ -27,7 +27,7 @@ impl ActionDispatcher for IsRenderable { self.referer_id.get() } - /// Devuelve el peso para definir el orden de aplicación. + /// Devuelve el peso para definir el orden de ejecución. fn weight(&self) -> Weight { self.weight } diff --git a/src/base/action/page.rs b/src/base/action/page.rs new file mode 100644 index 0000000..b6dbe9a --- /dev/null +++ b/src/base/action/page.rs @@ -0,0 +1,17 @@ +//! Acciones para alterar el contenido de las páginas a renderizar. + +use crate::response::page::Page; + +/// Tipo de función para manipular una página durante su construcción o renderizado. +/// +/// Se emplea en acciones orientadas a modificar o inspeccionar una instancia de [`Page`] +/// directamente, sin acceder a los componentes individuales ni al contexto de renderizado. +/// +/// Recibe una referencia mutable (`&mut`) a la página en cuestión. +pub type FnActionWithPage = fn(page: &mut Page); + +mod before_render_body; +pub use before_render_body::*; + +mod after_render_body; +pub use after_render_body::*; diff --git a/src/base/action/page/after_render_body.rs b/src/base/action/page/after_render_body.rs new file mode 100644 index 0000000..081b9aa --- /dev/null +++ b/src/base/action/page/after_render_body.rs @@ -0,0 +1,46 @@ +use crate::prelude::*; + +use crate::base::action::page::FnActionWithPage; + +/// Ejecuta [`FnActionWithPage`](crate::base::action::page::FnActionWithPage) después de renderizar +/// el cuerpo de la página. +/// +/// Este tipo de acción se despacha después de renderizar el contenido principal de la página +/// (``), permitiendo ajustes finales sobre la instancia [`Page`]. +/// +/// Las acciones se ejecutan en orden según el [`Weight`] asignado. +pub struct AfterRenderBody { + f: FnActionWithPage, + weight: Weight, +} + +impl ActionDispatcher for AfterRenderBody { + /// Devuelve el peso para definir el orden de ejecución. + fn weight(&self) -> Weight { + self.weight + } +} + +impl AfterRenderBody { + /// Permite [registrar](ExtensionTrait::actions) una nueva acción + /// [`FnActionWithPage`](crate::base::action::page::FnActionWithPage). + pub fn new(f: FnActionWithPage) -> Self { + AfterRenderBody { f, weight: 0 } + } + + /// Opcional. Acciones con pesos más bajos se aplican antes. Se pueden usar valores negativos. + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + // Despacha las acciones. + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(page: &mut Page) { + dispatch_actions( + &ActionKey::new(UniqueId::of::(), None, None, None), + |action: &Self| (action.f)(page), + ); + } +} diff --git a/src/base/action/page/before_render_body.rs b/src/base/action/page/before_render_body.rs new file mode 100644 index 0000000..a6e5c56 --- /dev/null +++ b/src/base/action/page/before_render_body.rs @@ -0,0 +1,46 @@ +use crate::prelude::*; + +use crate::base::action::page::FnActionWithPage; + +/// Ejecuta [`FnActionWithPage`](crate::base::action::page::FnActionWithPage) antes de renderizar +/// el cuerpo de la página. +/// +/// Este tipo de acción se despacha antes de renderizar el contenido principal de la página +/// (``), permitiendo ajustes sobre la instancia [`Page`]. +/// +/// Las acciones se ejecutan en orden según el [`Weight`] asignado. +pub struct BeforeRenderBody { + f: FnActionWithPage, + weight: Weight, +} + +impl ActionDispatcher for BeforeRenderBody { + /// Devuelve el peso para definir el orden de ejecución. + fn weight(&self) -> Weight { + self.weight + } +} + +impl BeforeRenderBody { + /// Permite [registrar](ExtensionTrait::actions) una nueva acción + /// [`FnActionWithPage`](crate::base::action::page::FnActionWithPage). + pub fn new(f: FnActionWithPage) -> Self { + BeforeRenderBody { f, weight: 0 } + } + + /// Opcional. Acciones con pesos más bajos se aplican antes. Se pueden usar valores negativos. + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + // Despacha las acciones. + #[inline(always)] + #[allow(clippy::inline_always)] + pub(crate) fn dispatch(page: &mut Page) { + dispatch_actions( + &ActionKey::new(UniqueId::of::(), None, None, None), + |action: &Self| (action.f)(page), + ); + } +} diff --git a/src/core/component/children.rs b/src/core/component/children.rs index cc3a864..c5b65a2 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -77,8 +77,8 @@ impl Typed { // Typed HELPERS ******************************************************************************* - /// Convierte el componente tipado en un [`Child`]. - fn to_child(&self) -> Child { + // Convierte el componente tipado en un [`Child`]. + fn into_child(self) -> Child { Child(self.0.clone()) } } @@ -155,12 +155,12 @@ impl Children { #[builder_fn] pub fn with_typed(mut self, op: TypedOp) -> Self { match op { - TypedOp::Add(typed) => self.add(typed.to_child()), - TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.to_child()), - TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.to_child()), - TypedOp::Prepend(typed) => self.prepend(typed.to_child()), + TypedOp::Add(typed) => self.add(typed.into_child()), + TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.into_child()), + TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.into_child()), + TypedOp::Prepend(typed) => self.prepend(typed.into_child()), TypedOp::RemoveById(id) => self.remove_by_id(id), - TypedOp::ReplaceById(id, typed) => self.replace_by_id(id, typed.to_child()), + TypedOp::ReplaceById(id, typed) => self.replace_by_id(id, typed.into_child()), TypedOp::Reset => self.reset(), } } @@ -174,6 +174,48 @@ impl Children { self } + // Children GETTERS **************************************************************************** + + /// Devuelve el número de componentes hijo de la lista. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Indica si la lista está vacía. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Devuelve el primer componente hijo con el identificador indicado, si existe. + pub fn get_by_id(&self, id: impl AsRef) -> Option<&Child> { + let id = Some(id.as_ref()); + self.0.iter().find(|c| c.id().as_deref() == id) + } + + /// Devuelve un iterador sobre los componentes hijo con el identificador indicado. + pub fn iter_by_id<'a>(&'a self, id: &'a str) -> impl Iterator + 'a { + self.0.iter().filter(move |c| c.id().as_deref() == Some(id)) + } + + /// Devuelve un iterador sobre los componentes hijo con el identificador de tipo ([`UniqueId`]) + /// indicado. + pub fn iter_by_type_id(&self, type_id: UniqueId) -> impl Iterator { + self.0.iter().filter(move |&c| c.type_id() == type_id) + } + + // Children RENDER ***************************************************************************** + + /// Renderiza todos los componentes hijo, en orden. + pub fn render(&self, cx: &mut Context) -> Markup { + html! { + @for c in &self.0 { + (c.render(cx)) + } + } + } + + // Children HELPERS **************************************************************************** + // Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra. #[inline] fn insert_after_id(&mut self, id: impl AsRef, child: Child) -> &mut Self { @@ -232,46 +274,6 @@ impl Children { self.0.clear(); self } - - // Children GETTERS **************************************************************************** - - /// Devuelve el número de componentes hijo de la lista. - pub fn len(&self) -> usize { - self.0.len() - } - - /// Indica si la lista está vacía. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Devuelve el primer componente hijo con el identificador indicado, si existe. - pub fn get_by_id(&self, id: impl AsRef) -> Option<&Child> { - let id = Some(id.as_ref()); - self.0.iter().find(|c| c.id().as_deref() == id) - } - - /// Devuelve un iterador sobre los componentes hijo con el identificador indicado. - pub fn iter_by_id<'a>(&'a self, id: &'a str) -> impl Iterator + 'a { - self.0.iter().filter(move |c| c.id().as_deref() == Some(id)) - } - - /// Devuelve un iterador sobre los componentes hijo con el identificador tipo ([`UniqueId`]) - /// indicado. - pub fn iter_by_type_id(&self, type_id: UniqueId) -> impl Iterator { - self.0.iter().filter(move |&c| c.type_id() == type_id) - } - - // Children RENDER ***************************************************************************** - - /// Renderiza todos los componentes hijo, en orden. - pub fn render(&self, cx: &mut Context) -> Markup { - html! { - @for c in &self.0 { - (c.render(cx)) - } - } - } } impl IntoIterator for Children { diff --git a/src/core/theme.rs b/src/core/theme.rs index d67a445..35d9887 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -22,3 +22,6 @@ pub(crate) use regions::ChildrenInRegions; pub use regions::InRegion; pub(crate) mod all; + +/// Nombre de la región por defecto: `content`. +pub const CONTENT_REGION_NAME: &str = "content"; diff --git a/src/core/theme/all.rs b/src/core/theme/all.rs index 1b98268..787e0c9 100644 --- a/src/core/theme/all.rs +++ b/src/core/theme/all.rs @@ -20,8 +20,8 @@ pub static DEFAULT_THEME: LazyLock = // TEMA POR NOMBRE ********************************************************************************* /// Devuelve el tema identificado por su [`short_name`](AnyInfo::short_name). -pub fn theme_by_short_name(short_name: impl AsRef) -> Option { - let short_name = short_name.as_ref().to_lowercase(); +pub fn theme_by_short_name(short_name: &'static str) -> Option { + let short_name = short_name.to_lowercase(); match THEMES .read() .iter() diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 6f1e54a..075ce20 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -1,5 +1,9 @@ use crate::core::extension::ExtensionTrait; +use crate::core::theme::CONTENT_REGION_NAME; +use crate::global; +use crate::html::{html, Markup}; use crate::locale::L10n; +use crate::response::page::Page; /// Representa una referencia a un tema. /// @@ -30,6 +34,66 @@ pub type ThemeRef = &'static dyn ThemeTrait; /// ``` pub trait ThemeTrait: ExtensionTrait + Send + Sync { fn regions(&self) -> Vec<(&'static str, L10n)> { - vec![("content", L10n::l("content"))] + vec![(CONTENT_REGION_NAME, L10n::l("content"))] + } + + #[allow(unused_variables)] + fn before_render_page_body(&self, page: &mut Page) {} + + fn render_page_body(&self, page: &mut Page) -> Markup { + html! { + body id=[page.body_id().get()] class=[page.body_classes().get()] { + @for (region_name, _) in self.regions() { + @let output = page.render_region(region_name); + @if !output.is_empty() { + div id=(region_name) class={ "region-container region-" (region_name) } { + (output) + } + } + } + } + } + } + + #[allow(unused_variables)] + fn after_render_page_body(&self, page: &mut Page) {} + + fn render_page_head(&self, page: &mut Page) -> Markup { + let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; + html! { + head { + 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.render_assets()) + } + } + } + + fn error403(&self, _page: &mut Page) -> Markup { + html! { div { h1 { ("FORBIDDEN ACCESS") } } } + } + + fn error404(&self, _page: &mut Page) -> Markup { + html! { div { h1 { ("RESOURCE NOT FOUND") } } } } } diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 5beee25..5e0186a 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,5 +1,5 @@ use crate::core::component::{Child, ChildOp, Children}; -use crate::core::theme::ThemeRef; +use crate::core::theme::{ThemeRef, CONTENT_REGION_NAME}; use crate::{builder_fn, AutoDefault, UniqueId}; use parking_lot::RwLock; @@ -7,45 +7,43 @@ use parking_lot::RwLock; use std::collections::HashMap; use std::sync::LazyLock; +// Regiones globales con componentes para un tema dado. static THEME_REGIONS: LazyLock>> = LazyLock::new(|| RwLock::new(HashMap::new())); +// Regiones globales con componentes para cualquier tema. static COMMON_REGIONS: LazyLock> = LazyLock::new(|| RwLock::new(ChildrenInRegions::default())); -// Estructura interna para mantener los componentes de cada región dada. +// Estructura interna para mantener los componentes de una región. #[derive(AutoDefault)] pub struct ChildrenInRegions(HashMap<&'static str, Children>); impl ChildrenInRegions { - pub fn new() -> Self { - ChildrenInRegions::default() - } - - pub fn with(region_id: &'static str, child: Child) -> Self { - ChildrenInRegions::default().with_in_region(region_id, ChildOp::Add(child)) + pub fn with(region_name: &'static str, child: Child) -> Self { + ChildrenInRegions::default().with_child_in_region(region_name, ChildOp::Add(child)) } #[builder_fn] - pub fn with_in_region(mut self, region_id: &'static str, op: ChildOp) -> Self { - if let Some(region) = self.0.get_mut(region_id) { + pub fn with_child_in_region(mut self, region_name: &'static str, op: ChildOp) -> Self { + if let Some(region) = self.0.get_mut(region_name) { region.alter_child(op); } else { - self.0.insert(region_id, Children::new().with_child(op)); + self.0.insert(region_name, Children::new().with_child(op)); } self } - pub fn all_in_region(&self, theme: ThemeRef, region_id: &str) -> Children { + pub fn merge_all_components(&self, theme_ref: ThemeRef, region_name: &'static str) -> Children { let common = COMMON_REGIONS.read(); - if let Some(r) = THEME_REGIONS.read().get(&theme.type_id()) { + if let Some(r) = THEME_REGIONS.read().get(&theme_ref.type_id()) { Children::merge(&[ - common.0.get(region_id), - self.0.get(region_id), - r.0.get(region_id), + common.0.get(region_name), + self.0.get(region_name), + r.0.get(region_name), ]) } else { - Children::merge(&[common.0.get(region_id), self.0.get(region_id)]) + Children::merge(&[common.0.get(region_name), self.0.get(region_name)]) } } } @@ -90,19 +88,22 @@ impl InRegion { InRegion::Content => { COMMON_REGIONS .write() - .alter_in_region("region-content", ChildOp::Add(child)); + .alter_child_in_region(CONTENT_REGION_NAME, ChildOp::Add(child)); } InRegion::Named(name) => { COMMON_REGIONS .write() - .alter_in_region(name, ChildOp::Add(child)); + .alter_child_in_region(name, ChildOp::Add(child)); } - InRegion::OfTheme(region, theme) => { + InRegion::OfTheme(region_name, theme_ref) => { let mut regions = THEME_REGIONS.write(); - if let Some(r) = regions.get_mut(&theme.type_id()) { - r.alter_in_region(region, ChildOp::Add(child)); + if let Some(r) = regions.get_mut(&theme_ref.type_id()) { + r.alter_child_in_region(region_name, ChildOp::Add(child)); } else { - regions.insert(theme.type_id(), ChildrenInRegions::with(region, child)); + regions.insert( + theme_ref.type_id(), + ChildrenInRegions::with(region_name, child), + ); } } } diff --git a/src/html/context.rs b/src/html/context.rs index 0035b26..a1df987 100644 --- a/src/html/context.rs +++ b/src/html/context.rs @@ -61,37 +61,42 @@ impl Error for ErrorParam {} /// hojas de estilo ([`StyleSheet`]) y *scripts* ([`JavaScript`]), así como parámetros de contexto /// definidos en tiempo de ejecución. /// -/// # Ejemplo +/// # Ejemplos +/// +/// Crea un nuevo contexto asociado a una solicitud HTTP: /// /// ```rust /// use pagetop::prelude::*; /// -/// fn configure_context(cx: &mut Context) { -/// // Establece el idioma del documento a español. -/// cx.alter_langid(LangMatch::langid_or_default("es-ES")) -/// // Selecciona un tema (por su nombre corto). -/// .alter_theme("aliner") -/// // Añade un parámetro dinámico al contexto. -/// .alter_param("usuario_id", 42) -/// // Asigna un favicon. -/// .alter_assets(AssetsOp::SetFavicon(Some( -/// Favicon::new().with_icon("/icons/favicon.ico") -/// ))) -/// // Añade una hoja de estilo externa. -/// .alter_assets(AssetsOp::AddStyleSheet( -/// StyleSheet::from("/css/style.css") -/// )) -/// // Añade un script JavaScript. -/// .alter_assets(AssetsOp::AddJavaScript( -/// JavaScript::defer("/js/main.js") -/// )); +/// fn new_context(request: HttpRequest) -> Context { +/// Context::new(request) +/// // Establece el idioma del documento a español. +/// .with_langid(LangMatch::langid_or_default("es-ES")) +/// // Selecciona un tema (por su nombre corto). +/// .with_theme("aliner") +/// // Asigna un favicon. +/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) +/// // Añade una hoja de estilo externa. +/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/style.css"))) +/// // Añade un script JavaScript. +/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js"))) +/// // Añade un parámetro dinámico al contexto. +/// .with_param("usuario_id", 42) +/// } +/// ``` /// +/// Y hace operaciones con un contexto dado: +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// fn use_context(cx: &mut Context) { /// // Recupera el tema seleccionado. /// let active_theme = cx.theme(); /// assert_eq!(active_theme.short_name(), "aliner"); /// /// // Recupera el parámetro a su tipo original. -/// let id: i32 = cx.param("usuario_id").unwrap(); +/// let id: i32 = cx.get_param("usuario_id").unwrap(); /// assert_eq!(id, 42); /// /// // Genera un identificador para un componente de tipo `Menu`. @@ -102,32 +107,33 @@ impl Error for ErrorParam {} /// ``` #[rustfmt::skip] pub struct Context { - request : HttpRequest, // Solicitud HTTP de origen. - langid : &'static LanguageIdentifier, // Identificador del idioma. - theme : ThemeRef, // Referencia al tema para renderizar. - layout : &'static str, // Composición del documento para renderizar. - params : HashMap, // Parámetros definidos en tiempo de ejecución. - favicon : Option, // Favicon, si se ha definido. - stylesheets: Assets, // Hojas de estilo CSS. - javascripts: Assets, // Scripts JavaScript. - id_counter : usize, // Contador para generar identificadores únicos. + request : HttpRequest, // Solicitud HTTP de origen. + langid : &'static LanguageIdentifier, // Identificador de idioma. + 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. + params : HashMap<&'static str, String>, // Parámetros definidos en tiempo de ejecución. + id_counter : usize, // Contador para generar identificadores únicos. } impl Context { - // Crea un nuevo contexto asociado a una solicitud HTTP. - // - // El contexto inicializa el idioma por defecto, sin favicon ni recursos cargados. + /// Crea un nuevo contexto asociado a una solicitud HTTP. + /// + /// El contexto inicializa el idioma, tema y composición por defecto, sin favicon ni recursos + /// cargados. #[rustfmt::skip] - pub(crate) fn new(request: HttpRequest) -> Self { + pub fn new(request: HttpRequest) -> Self { Context { request, langid : &DEFAULT_LANGID, theme : *DEFAULT_THEME, layout : "default", - params : HashMap::::new(), favicon : None, stylesheets: Assets::::new(), javascripts: Assets::::new(), + params : HashMap::<&str, String>::new(), id_counter : 0, } } @@ -141,37 +147,24 @@ impl Context { self } - /// Establece el tema que se usará para renderizar el documento. + /// Modifica el tema que se usará 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] - pub fn with_theme(mut self, theme_name: impl AsRef) -> Self { + pub fn with_theme(mut self, theme_name: &'static str) -> Self { self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); self } - /// Define el tipo de composición usado para renderizar el documento. + /// Modifica la composición para renderizar el documento. #[builder_fn] pub fn with_layout(mut self, layout_name: &'static str) -> Self { self.layout = layout_name; self } - /// Añade o modifica un parámetro del contexto almacenando el valor como [`String`]. - #[builder_fn] - pub fn with_param(mut self, key: impl AsRef, value: T) -> Self { - self.params - .insert(key.as_ref().to_string(), value.to_string()); - self - } - - /// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó. - pub fn remove_param(&mut self, key: impl AsRef) -> bool { - self.params.remove(key.as_ref()).is_some() - } - - /// Modifica información o recursos del contexto usando [`AssetsOp`]. + /// Define los recursos del contexto usando [`AssetsOp`]. #[builder_fn] pub fn with_assets(mut self, op: AssetsOp) -> Self { match op { @@ -209,7 +202,7 @@ impl Context { &self.request } - /// Devuelve el identificador del idioma asociado al documento. + /// Devuelve el identificador de idioma asociado al documento. pub fn langid(&self) -> &LanguageIdentifier { self.langid } @@ -219,23 +212,11 @@ impl Context { self.theme } - /// Devuelve el tipo de composición usado para renderizar el documento. El valor predeterminado - /// es `"default"`. + /// Devuelve la composición para renderizar el documento. Por defecto es `"default"`. pub fn layout(&self) -> &str { self.layout } - /// Recupera un parámetro del contexto convertido al tipo especificado. - /// - /// Devuelve un error si el parámetro no existe ([`ErrorParam::NotFound`]) o la conversión falla - /// ([`ErrorParam::ParseError`]). - pub fn param(&self, key: impl AsRef) -> Result { - self.params - .get(key.as_ref()) - .ok_or(ErrorParam::NotFound) - .and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone()))) - } - // Context RENDER ****************************************************************************** /// Renderiza los recursos del contexto. @@ -249,6 +230,31 @@ impl Context { } } + // Context PARAMS ****************************************************************************** + + /// Añade o modifica un parámetro del contexto almacenando el valor como [`String`]. + #[builder_fn] + pub fn with_param(mut self, key: &'static str, value: T) -> Self { + self.params.insert(key, value.to_string()); + self + } + + /// Recupera un parámetro del contexto convertido al tipo especificado. + /// + /// Devuelve un error si el parámetro no existe ([`ErrorParam::NotFound`]) o la conversión falla + /// ([`ErrorParam::ParseError`]). + pub fn get_param(&self, key: &'static str) -> Result { + self.params + .get(key) + .ok_or(ErrorParam::NotFound) + .and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone()))) + } + + /// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó. + pub fn remove_param(&mut self, key: &'static str) -> bool { + self.params.remove(key).is_some() + } + // Context EXTRAS ****************************************************************************** /// Genera un identificador único si no se proporciona uno explícito. diff --git a/src/prelude.rs b/src/prelude.rs index 9334f18..af2f511 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -43,7 +43,7 @@ pub use crate::core::component::*; pub use crate::core::extension::*; pub use crate::core::theme::*; -pub use crate::response::{json::*, redirect::*, ResponseError}; +pub use crate::response::{json::*, page::*, redirect::*, ResponseError}; pub use crate::base::action; pub use crate::base::component::*; diff --git a/src/response.rs b/src/response.rs index 1fce663..4078d42 100644 --- a/src/response.rs +++ b/src/response.rs @@ -2,6 +2,8 @@ pub use actix_web::ResponseError; +pub mod page; + pub mod json; pub mod redirect; diff --git a/src/response/page.rs b/src/response/page.rs new file mode 100644 index 0000000..b252848 --- /dev/null +++ b/src/response/page.rs @@ -0,0 +1,249 @@ +mod error; +pub use error::ErrorPage; + +pub use actix_web::Result as ResultPage; + +use crate::base::action; +use crate::builder_fn; +use crate::core::component::{Child, ChildOp, ComponentTrait}; +use crate::core::theme::{ChildrenInRegions, ThemeRef, CONTENT_REGION_NAME}; +use crate::html::{html, AssetsOp, Context, Markup, DOCTYPE}; +use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated}; +use crate::locale::{CharacterDirection, L10n, LanguageIdentifier}; +use crate::service::HttpRequest; + +/// Representa una página HTML completa lista para renderizar. +/// +/// Una instancia de `Page` se compone dinámicamente permitiendo establecer título, descripción, +/// regiones donde disponer los componentes, atributos de `` y otros aspectos del contexto de +/// renderizado. +#[rustfmt::skip] +pub struct Page { + title : OptionTranslated, + description : OptionTranslated, + metadata : Vec<(&'static str, &'static str)>, + properties : Vec<(&'static str, &'static str)>, + context : Context, + body_id : OptionId, + body_classes: OptionClasses, + regions : ChildrenInRegions, +} + +impl Page { + /// Crea una nueva instancia de página con contexto basado en la petición HTTP. + #[rustfmt::skip] + pub fn new(request: HttpRequest) -> Self { + Page { + title : OptionTranslated::default(), + description : OptionTranslated::default(), + metadata : Vec::default(), + properties : Vec::default(), + context : Context::new(request), + body_id : OptionId::default(), + body_classes: OptionClasses::default(), + regions : ChildrenInRegions::default(), + } + } + + // Page BUILDER ******************************************************************************** + + /// Establece el título de la página como un valor traducible. + #[builder_fn] + pub fn with_title(mut self, title: L10n) -> Self { + self.title.alter_value(title); + self + } + + /// Establece la descripción de la página como un valor traducible. + #[builder_fn] + pub fn with_description(mut self, description: L10n) -> Self { + self.description.alter_value(description); + self + } + + /// Añade una entrada `` al ``. + #[builder_fn] + pub fn with_metadata(mut self, name: &'static str, content: &'static str) -> Self { + self.metadata.push((name, content)); + self + } + + /// Añade una entrada `` al ``. + #[builder_fn] + pub fn with_property(mut self, property: &'static str, content: &'static str) -> Self { + self.metadata.push((property, content)); + self + } + + /// Modifica el identificador de idioma de la página ([`Context::with_langid`]). + #[builder_fn] + pub fn with_langid(mut self, langid: &'static LanguageIdentifier) -> Self { + self.context.alter_langid(langid); + self + } + + /// Modifica el tema que se usará para renderizar la página ([`Context::with_theme`]). + #[builder_fn] + pub fn with_theme(mut self, theme_name: &'static str) -> Self { + self.context.alter_theme(theme_name); + self + } + + /// Modifica la composición para renderizar la página ([`Context::with_layout`]). + #[builder_fn] + pub fn with_layout(mut self, layout_name: &'static str) -> Self { + self.context.alter_layout(layout_name); + self + } + + /// Define los recursos de la página usando [`AssetsOp`]. + #[builder_fn] + pub fn with_assets(mut self, op: AssetsOp) -> Self { + self.context.alter_assets(op); + self + } + + /// Establece el atributo `id` del elemento ``. + #[builder_fn] + pub fn with_body_id(mut self, id: impl AsRef) -> Self { + self.body_id.alter_value(id); + self + } + + /// Modifica las clases CSS del elemento `` con una operación sobre [`OptionClasses`]. + #[builder_fn] + pub fn with_body_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { + self.body_classes.alter_value(op, classes); + self + } + + /// Añade un componente a la región de contenido por defecto. + pub fn with_component(mut self, component: impl ComponentTrait) -> Self { + self.regions + .alter_child_in_region(CONTENT_REGION_NAME, ChildOp::Add(Child::with(component))); + self + } + + /// Añade un componente en una región (`region_name`) de la página. + pub fn with_component_in( + mut self, + region_name: &'static str, + component: impl ComponentTrait, + ) -> Self { + self.regions + .alter_child_in_region(region_name, ChildOp::Add(Child::with(component))); + self + } + + /// Opera con [`ChildOp`] en una región (`region_name`) de la página. + #[builder_fn] + pub fn with_child_in_region(mut self, region_name: &'static str, op: ChildOp) -> Self { + self.regions.alter_child_in_region(region_name, op); + self + } + + // Page GETTERS ******************************************************************************** + + /// Devuelve el título traducido para el idioma activo, si existe. + pub fn title(&mut self) -> Option { + self.title.using(self.context.langid()) + } + + /// Devuelve la descripción traducida para el idioma activo, si existe. + pub fn description(&mut self) -> Option { + self.description.using(self.context.langid()) + } + + /// Devuelve la lista de metadatos ``. + pub fn metadata(&self) -> &Vec<(&str, &str)> { + &self.metadata + } + + /// Devuelve la lista de propiedades ``. + pub fn properties(&self) -> &Vec<(&str, &str)> { + &self.properties + } + + /// Devuelve la solicitud HTTP asociada. + pub fn request(&self) -> &HttpRequest { + self.context.request() + } + + /// Devuelve el identificador de idioma asociado. + pub fn langid(&self) -> &LanguageIdentifier { + self.context.langid() + } + + /// Devuelve el tema que se usará para renderizar la página. + pub fn theme(&self) -> ThemeRef { + self.context.theme() + } + + /// Devuelve la composición para renderizar la página. Por defecto es `"default"`. + pub fn layout(&self) -> &str { + self.context.layout() + } + + /// Devuelve el identificador del elemento ``. + pub fn body_id(&self) -> &OptionId { + &self.body_id + } + + /// Devuelve las clases CSS del elemento ``. + pub fn body_classes(&self) -> &OptionClasses { + &self.body_classes + } + + // Page RENDER ********************************************************************************* + + /// Renderiza los componentes de una región (`regiona_name`) de la página. + pub fn render_region(&mut self, region_name: &'static str) -> Markup { + self.regions + .merge_all_components(self.context.theme(), region_name) + .render(&mut self.context) + } + + /// Renderiza los recursos de la página. + pub fn render_assets(&self) -> Markup { + self.context.render_assets() + } + + /// Renderiza la página completa en formato HTML. + /// + /// Ejecuta las acciones correspondientes antes y después de renderizar el ``, + /// así como del ``, e inserta los atributos `lang` y `dir` en la etiqueta ``. + pub fn render(&mut self) -> ResultPage { + // Acciones específicas del tema antes de renderizar el . + self.context.theme().before_render_page_body(self); + + // Acciones de las extensiones antes de renderizar el . + action::page::BeforeRenderBody::dispatch(self); + + // Renderiza el . + 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); + + // Acciones de las extensiones después de renderizar el . + action::page::AfterRenderBody::dispatch(self); + + // Renderiza el . + let head = self.context.theme().render_page_head(self); + + // Compone la página incluyendo los atributos de idioma y dirección del texto. + let lang = &self.context.langid().language; + let dir = match self.context.langid().character_direction() { + CharacterDirection::LTR => "ltr", + CharacterDirection::RTL => "rtl", + CharacterDirection::TTB => "auto", + }; + Ok(html! { + (DOCTYPE) + html lang=(lang) dir=(dir) { + (head) + (body) + } + }) + } +} diff --git a/src/response/page/error.rs b/src/response/page/error.rs new file mode 100644 index 0000000..3d952c6 --- /dev/null +++ b/src/response/page/error.rs @@ -0,0 +1,88 @@ +use crate::base::component::Html; +use crate::locale::L10n; +use crate::response::ResponseError; +use crate::service::http::{header::ContentType, StatusCode}; +use crate::service::{HttpRequest, HttpResponse}; + +use super::Page; + +use std::fmt; + +#[derive(Debug)] +pub enum ErrorPage { + NotModified(HttpRequest), + BadRequest(HttpRequest), + AccessDenied(HttpRequest), + NotFound(HttpRequest), + PreconditionFailed(HttpRequest), + InternalError(HttpRequest), + Timeout(HttpRequest), +} + +impl fmt::Display for ErrorPage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + // Error 304. + ErrorPage::NotModified(_) => write!(f, "Not Modified"), + // Error 400. + ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"), + // Error 403. + ErrorPage::AccessDenied(request) => { + let mut error_page = Page::new(request.clone()); + let error403 = error_page.theme().error403(&mut error_page); + if let Ok(page) = error_page + .with_title(L10n::n("Error FORBIDDEN")) + .with_layout("error") + .with_component(Html::with(error403)) + .render() + { + write!(f, "{}", page.into_string()) + } else { + write!(f, "Access Denied") + } + } + // Error 404. + ErrorPage::NotFound(request) => { + let mut error_page = Page::new(request.clone()); + let error404 = error_page.theme().error404(&mut error_page); + if let Ok(page) = error_page + .with_title(L10n::n("Error RESOURCE NOT FOUND")) + .with_layout("error") + .with_component(Html::with(error404)) + .render() + { + write!(f, "{}", page.into_string()) + } else { + write!(f, "Not Found") + } + } + // Error 412. + ErrorPage::PreconditionFailed(_) => write!(f, "Precondition Failed"), + // Error 500. + ErrorPage::InternalError(_) => write!(f, "Internal Error"), + // Error 504. + ErrorPage::Timeout(_) => write!(f, "Timeout"), + } + } +} + +impl ResponseError for ErrorPage { + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()) + .insert_header(ContentType::html()) + .body(self.to_string()) + } + + #[rustfmt::skip] + fn status_code(&self) -> StatusCode { + match self { + ErrorPage::NotModified(_) => StatusCode::NOT_MODIFIED, + ErrorPage::BadRequest(_) => StatusCode::BAD_REQUEST, + ErrorPage::AccessDenied(_) => StatusCode::FORBIDDEN, + ErrorPage::NotFound(_) => StatusCode::NOT_FOUND, + ErrorPage::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED, + ErrorPage::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::Timeout(_) => StatusCode::GATEWAY_TIMEOUT, + } + } +}