//! 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 `` y del ``, 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(crate) use error::render_error_pages; use crate::auth::CurrentUser; use crate::base::action; use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual}; use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{Attr, Props, PropsOp}; use crate::html::{DOCTYPE, Markup, html}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; use crate::web::HttpRequest; use crate::{AutoDefault, builder_fn}; // **< 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, /// regiones donde disponer los componentes, atributos de `` y otros aspectos del contexto de /// renderizado. #[rustfmt::skip] #[derive(AutoDefault)] pub struct Page { title : Attr, description : Attr, metadata : Vec<(&'static str, &'static str)>, properties : Vec<(&'static str, &'static str)>, context : Context, } impl Page { /// Crea una nueva instancia de página. /// /// La petición HTTP se guarda en el contexto de renderizado, que extrae automáticamente el /// [`CurrentUser`] inyectado por middleware en sus extensiones (ver /// [`Context::new`](crate::core::component::Context::new)). Cualquier handler tiene acceso al /// usuario actual desde el momento en que se crea la página, sin llamadas adicionales. #[rustfmt::skip] pub fn new(request: HttpRequest) -> Self { Page { title : Attr::::default(), description : Attr::::default(), metadata : Vec::default(), properties : Vec::default(), context : Context::new(Some(request)), } } // **< 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.properties.push((property, content)); self } // **< Page GETTERS >*************************************************************************** /// Devuelve el título traducido para el idioma de la página, si existe. pub fn title(&mut self) -> Option { self.title.lookup(&self.context) } /// Devuelve la descripción traducida para el idioma de la página, si existe. pub fn description(&mut self) -> Option { self.description.lookup(&self.context) } /// 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 una referencia mutable al [`Context`] de la página. /// /// El [`Context`] actúa como intermediario para muchos métodos de `Page` (idioma, tema, /// *layout*, recursos, petición HTTP, etc.). Resulta especialmente útil cuando un componente /// o un tema necesita recibir el contexto como parámetro. pub fn context(&mut self) -> &mut Context { &mut self.context } // **< Page RENDER >**************************************************************************** /// Renderiza la página completa en formato 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 ``. /// 2. Despacha [`action::page::BeforeRenderBody`] para que otras extensiones puedan realizar /// ajustes previos sobre la página. /// 3. **Construye el contenido del ``**: /// - 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 ``. /// 6. Renderiza el `` 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 ``. /// 8. Compone el documento HTML completo (``, ``, ``, ``) y /// devuelve un [`Result`] con el [`Markup`] final. pub async fn render(&mut self) -> Result { // 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 = html! { (ReservedRegion::PageTop.render(&mut self.context).await) (self.context.theme().render_page_body(self).await) (ReservedRegion::PageBottom.render(&mut self.context).await) }; // 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).await; // 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 { (head) } body (self.body_props()) { (body) } } }) } } /// Permite a [`Page`] actuar como proveedor de idioma usando el [`Context`] de la página. /// /// Resulta útil para usar [`Page`] directamente como fuente de traducción en [`L10n::lookup()`] o /// [`L10n::using()`]. impl LangId for Page { #[inline] fn langid(&self) -> &'static LanguageIdentifier { self.context.langid() } } impl Contextual for Page { // **< Contextual BUILDER >********************************************************************* #[builder_fn] fn with_request(mut self, request: Option) -> Self { self.context.alter_request(request); self } #[builder_fn] fn with_langid(mut self, language: &impl LangId) -> Self { self.context.alter_langid(language); self } #[builder_fn] fn with_theme(mut self, theme: ThemeRef) -> Self { self.context.alter_theme(theme); self } #[builder_fn] fn with_template(mut self, template: TemplateRef) -> Self { self.context.alter_template(template); self } #[builder_fn] fn with_param(mut self, key: &'static str, value: T) -> Self { self.context.alter_param(key, value); self } #[builder_fn] fn with_assets(mut self, op: AssetsOp) -> Self { self.context.alter_assets(op); self } #[builder_fn] fn with_body_props(mut self, op: PropsOp) -> Self { self.context.alter_body_props(op); self } #[builder_fn] fn with_child(mut self, op: impl Into) -> Self { self.context .alter_child_in(&DefaultRegion::Content, op.into()); self } #[builder_fn] fn with_child_in(mut self, region_ref: RegionRef, op: impl Into) -> Self { self.context.alter_child_in(region_ref, op.into()); self } // **< Contextual GETTERS >********************************************************************* fn request(&self) -> Option<&HttpRequest> { self.context.request() } fn current_user(&self) -> &CurrentUser { self.context.current_user() } fn theme(&self) -> ThemeRef { self.context.theme() } fn template(&self) -> TemplateRef { self.context.template() } fn param(&self, key: &'static str) -> Result<&T, ContextError> { self.context.param(key) } fn favicon(&self) -> Option<&Favicon> { self.context.favicon() } fn stylesheets(&self) -> &Assets { self.context.stylesheets() } fn javascripts(&self) -> &Assets { self.context.javascripts() } fn body_props(&self) -> &Props { self.context.body_props() } // **< Contextual HELPERS >********************************************************************* fn remove_param(&mut self, key: &'static str) -> bool { self.context.remove_param(key) } }