diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 6c288062..dda79e45 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -105,7 +105,7 @@ impl Extension for SuperMenu { )), )); - InRegion::Global(&DefaultRegion::Header).add( + InRegion::Global(&DefaultRegions::Header).add( bs::Container::new() .with_width(bs::container::Width::FluidMax(UnitValue::RelRem(75.0))) .with_child(navbar_menu), diff --git a/src/app.rs b/src/app.rs index 55f256b1..87d81b6c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,20 +3,31 @@ mod figfont; use crate::core::{extension, extension::ExtensionRef}; -use crate::html::Markup; use crate::locale::Locale; -use crate::response::page::{ErrorPage, render_error_pages}; -use crate::web::{HttpRequest, Router}; +use crate::response::page::{render_error_pages, response_for_panic, route_not_found}; +use crate::web::Router; use crate::{PAGETOP_VERSION, global, trace}; +use tower_http::catch_panic::CatchPanicLayer; + use std::io::Error; use std::sync::LazyLock; /// Punto de entrada de una aplicación PageTop. /// -/// Orquesta el arranque de la aplicación. Primero se instancia con [`new()`](Application::new) o -/// [`prepare()`](Application::prepare), y después se ejecuta usando [`run()`](Application::run) (o -/// usando [`test()`](Application::test) si se está preparando un entorno de pruebas). +/// Orquesta el arranque de la aplicación. Primero se instancia con [`Application::new()`] o +/// [`Application::prepare()`], y después se ejecuta usando [`run()`](Application::run). Si se está +/// preparando un entorno de pruebas, se usa [`test()`](Application::test). +/// +/// Los **errores controlados** (403, 404, o un fallo que un handler devuelva explícitamente como +/// [`ErrorPage`](crate::response::page::ErrorPage)) se renderizan usando el tema activo (ver +/// [`Theme::error_403()`](crate::core::theme::Theme::error_403), +/// [`Theme::error_404()`](crate::core::theme::Theme::error_404) y +/// [`Theme::error_fatal()`](crate::core::theme::Theme::error_fatal)). +/// +/// La última capa del router captura cualquier **fallo catastrófico** (`panic!`) de la aplicación +/// en lugar de abortar la conexión. Devuelve una respuesta mínima HTTP 500 que es independiente del +/// tema y del ciclo de renderizado de componentes. pub struct Application; impl Application { @@ -34,7 +45,7 @@ impl Application { /// que no dependen de ninguna otra, luego las que dependen de extensiones ya habilitadas, y así /// hasta habilitar la extensión raíz. /// - /// Es *async* porque cada extensión puede realizar operaciones asíncronas en su + /// Es `async` porque cada extensión puede realizar operaciones asíncronas en su /// [`initialize()`](crate::core::extension::Extension::initialize) (conexión a base de datos, /// migraciones, semillas de datos...). pub async fn prepare(root_extension: ExtensionRef) -> Self { @@ -106,12 +117,16 @@ impl Application { } // Construye el router con las rutas y el middleware de todas las extensiones habilitadas. + // + // Con `CatchPanicLayer` en la última capa se capturan incluso los `panic!` que se produzcan + // dentro del propio renderizado de una página de error. fn build_router() -> Router { let router = extension::all::configure_routes(Router::new()); let router = extension::all::configure_middleware(router); router .fallback(route_not_found) .layer(axum::middleware::from_fn(render_error_pages)) + .layer(CatchPanicLayer::custom(response_for_panic)) } /// Arranca el servidor web de la aplicación. @@ -155,7 +170,3 @@ impl Application { Self::build_router() } } - -async fn route_not_found(request: HttpRequest) -> Result { - Err(ErrorPage::NotFound(request)) -} diff --git a/src/auth.rs b/src/auth.rs index a59e23d9..e9f96410 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -17,7 +17,7 @@ use crate::{UniqueId, Weight}; /// /// Se almacena automáticamente en el [`Context`] a partir de la petición HTTP (ver /// [`Context::new()`](crate::core::component::Context::new)). La identidad se extrae de las -/// extensiones de la petición, que una extensión de autenticación inyecta mediante su *middleware*. +/// extensiones de la petición, que una extensión de autenticación inyecta mediante su middleware. /// /// Se accede con [`Contextual::current_user()`](crate::core::component::Contextual::current_user). /// @@ -72,9 +72,9 @@ impl CurrentUser { /// Se invoca con: /// /// - `cx`: el contexto de renderizado desde el que se puede acceder a la petición HTTP y a -/// cualquier dato inyectado por el *middleware* de autenticación. +/// cualquier dato inyectado por el middleware de autenticación. /// - `key`: clave del permiso a comprobar (p. ej. `"myapp.edit_posts"`). -/// - `granted`: referencia mutable; el *handler* debe asignarla a `true` si concede el permiso. +/// - `granted`: referencia mutable; el handler debe asignarla a `true` si concede el permiso. pub type FnCheckPermission = fn(cx: &Context, key: &str, granted: &mut bool); /// Acción para comprobar si el usuario actual tiene un permiso concreto. @@ -148,7 +148,7 @@ impl CheckPermission { /// Comprueba si el usuario actual tiene el permiso indicado. /// /// Despacha la acción [`CheckPermission`]: cualquier extensión registrada puede conceder el permiso -/// asignando `granted = true` en su *handler*. Si no hay extensiones de autenticación activas, +/// asignando `granted = true` en su handler. Si no hay extensiones de autenticación activas, /// devuelve `false` para cualquier usuario, incluido el anónimo. /// /// La decisión de conceder o denegar permisos al usuario anónimo también es responsabilidad de cada diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 26090da3..71aebdd0 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -25,7 +25,7 @@ impl Theme for Basic { .with_weight(-99), )) .alter_child_in( - &DefaultRegion::Footer, + &DefaultRegions::Footer, ChildOp::AddIfEmpty(PoweredBy::new().into()), ); } diff --git a/src/core/component/context.rs b/src/core/component/context.rs index b8081edf..4dd7a1dc 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -2,7 +2,7 @@ use crate::auth::CurrentUser; use crate::core::TypeInfo; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; -use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef}; +use crate::core::theme::{ChildrenInRegions, DefaultRegions, RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet}; use crate::html::{Markup, Props, PropsOp, RoutePath, html}; use crate::locale::L10n; @@ -96,7 +96,7 @@ impl std::error::Error for ContextError {} /// fn prepare_context(cx: C) -> C { /// cx.with_langid(&Locale::resolve("es-ES")) /// .with_theme(&Aliner) -/// .with_template(&DefaultTemplate::Standard) +/// .with_template(&DefaultTemplates::Standard) /// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) /// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css"))) /// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js"))) @@ -306,13 +306,36 @@ pub trait Contextual: LangId { /// let _unique_id = cx.build_id::(1); // => "menu-1" si es el primero /// } /// ``` + +// Cómo obtener la plantilla activa del contexto: la del tema (por defecto o de administración), o +// una fijada explícitamente. Se resuelve contra el tema activo al leer `Context::template()`, no al +// asignarla, para que un cambio de tema posterior con `with_theme()` se refleje automáticamente. +enum TemplateSource { + // Plantilla por defecto. + Default, + // Plantilla de administración del tema activo. + Admin, + // Plantilla fijada explícitamente con `with_template()`. + Explicit(TemplateRef), +} + +impl TemplateSource { + fn resolve(&self, theme: ThemeRef) -> TemplateRef { + match self { + Self::Default => theme.default_template(), + Self::Admin => theme.admin_template(), + Self::Explicit(template) => *template, + } + } +} + #[rustfmt::skip] pub struct Context { request : Option, // Petición HTTP de origen. locale : RequestLocale, // Idioma asociado a la petición. current_user: CurrentUser, // Identidad del usuario actual. theme : ThemeRef, // Referencia al tema usado para renderizar. - template : TemplateRef, // Plantilla usada para renderizar. + template : TemplateSource, // Plantilla usada para renderizar. favicon : Option, // Favicon, si se ha definido. preloads : Assets, // Recursos para precarga. stylesheets : Assets, // Hojas de estilo CSS. @@ -344,7 +367,7 @@ impl Context { locale, current_user, theme : *DEFAULT_THEME, - template : DEFAULT_THEME.default_template(), + template : TemplateSource::Default, favicon : None, preloads : Assets::::new(), stylesheets: Assets::::new(), @@ -357,7 +380,7 @@ impl Context { } } - // Extrae el `CurrentUser` inyectado por *middleware* en las extensiones de la petición, o + // Extrae el `CurrentUser` inyectado por middleware en las extensiones de la petición, o // `CurrentUser::Anonymous` si no hay petición o ninguna extensión de autenticación está activa. fn resolve_current_user(request: Option<&HttpRequest>) -> CurrentUser { request @@ -366,6 +389,13 @@ impl Context { .unwrap_or(CurrentUser::Anonymous) } + // Fuerza la plantilla de administración del tema activo (usada por `Page::admin()`). Se + // resuelve dinámicamente contra `self.theme`, igual que `TemplateSource::Default`, así que + // sigue reflejando cualquier cambio de tema posterior con `with_theme()`. + pub(crate) fn use_admin_template(&mut self) { + self.template = TemplateSource::Admin; + } + // **< Context RENDER >************************************************************************* /// Renderiza los recursos del contexto. @@ -534,7 +564,7 @@ impl Contextual for Context { #[builder_fn] fn with_template(mut self, template: TemplateRef) -> Self { - self.template = template; + self.template = TemplateSource::Explicit(template); self } @@ -591,7 +621,7 @@ impl Contextual for Context { #[builder_fn] fn with_child(mut self, op: impl Into) -> Self { self.regions - .alter_child_in(&DefaultRegion::Content, op.into()); + .alter_child_in(&DefaultRegions::Content, op.into()); self } @@ -616,7 +646,7 @@ impl Contextual for Context { } fn template(&self) -> TemplateRef { - self.template + self.template.resolve(self.theme) } fn param(&self, key: &'static str) -> Result<&T, ContextError> { diff --git a/src/core/extension/all.rs b/src/core/extension/all.rs index 7ceb3fec..3d8baab6 100644 --- a/src/core/extension/all.rs +++ b/src/core/extension/all.rs @@ -102,7 +102,7 @@ pub fn configure_routes(router: Router) -> Router { // **< CONFIGURA EL MIDDLEWARE GLOBAL >************************************************************* -/// Aplica las capas de *middleware* globales de todas las extensiones sobre el router ya enrutado. +/// Aplica las capas de middleware globales de todas las extensiones sobre el router ya enrutado. /// /// Se llama después de [`configure_routes`] para garantizar que las capas envuelven todas las /// rutas de la aplicación, independientemente del orden de las extensiones. diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index 4bbf9c4c..ad4aaf75 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -192,12 +192,12 @@ pub trait Extension: AnyInfo + Send + Sync { /// } /// ``` /// - /// ## Rutas con *middleware* acotado a esta extensión + /// ## Rutas con middleware acotado a esta extensión /// - /// Cada `Router` mantiene su propia pila de *middleware* independiente. Cuando se crea un + /// Cada `Router` mantiene su propia pila de middleware independiente. Cuando se crea un /// `Router::new()` separado y se llama a `.layer()` sobre él, esa capa sólo se aplica a las - /// rutas de ese *router* concreto. Al fusionarlo con `.merge()` en el `router` principal, cada - /// ruta se añade con su *middleware* asociado, sin tocar las demás. + /// rutas de ese router concreto. Al fusionarlo con `.merge()` en el `router` principal, cada + /// ruta se añade con su middleware asociado, sin tocar las demás. /// /// Otra cosa es llamar a `.layer()` directamente sobre el `router` principal que se recibe como /// parámetro, porque ese objeto ya contiene todas las rutas acumuladas por extensiones @@ -218,7 +218,7 @@ pub trait Extension: AnyInfo + Send + Sync { /// } /// ``` /// - /// Para *middleware* que deba cubrir **todas** las rutas, usar + /// Para middleware que deba cubrir **todas** las rutas, usar /// [`configure_middleware`](Self::configure_middleware). /// /// ## Archivos estáticos @@ -242,13 +242,13 @@ pub trait Extension: AnyInfo + Send + Sync { router } - /// Añade capas de *middleware* globales al *router* ya completamente preparado. + /// Añade capas de middleware globales al router ya completamente preparado. /// /// Se invoca **después** de que todas las extensiones hayan registrado sus rutas con /// [`configure_router`](Self::configure_router), de modo que las capas añadidas aquí se aplican /// a **todas** las rutas de la aplicación, independientemente del orden de las extensiones. /// - /// Usar este método cuando el *middleware* deba interceptar cualquier petición entrante (p. ej. + /// Usar este método cuando el middleware deba interceptar cualquier petición entrante (p. ej. /// resolución de sesión, autenticación, cabeceras de seguridad, ...). /// /// # Ejemplo diff --git a/src/core/theme.rs b/src/core/theme.rs index 46b5d086..6a5bf6a7 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -1,31 +1,86 @@ //! API para añadir y gestionar nuevos temas. //! -//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension) y -//! también [`Theme`], de modo que [`Extension::theme()`](crate::core::extension::Extension::theme) -//! permita identificar y registrar los temas disponibles. -//! //! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos //! interactivos. Para ello utiliza plantillas ([`Template`]) que describen cómo maquetar el cuerpo -//! del documento a partir de varias regiones ([`Region`]). Cada región es un contenedor lógico -//! identificado por un nombre, cuyo contenido se obtiene del [`Context`] de la página. +//! del documento a partir de regiones ([`Region`]). Cada región es un contenedor lógico +//! identificado por un nombre para agrupar y renderizar componentes. //! -//! Una página ([`Page`](crate::response::page::Page)) representa un documento HTML completo. -//! Implementa [`Contextual`](crate::core::component::Contextual) para gestionar su propio -//! [`Context`], donde mantiene el tema activo, la plantilla seleccionada y los componentes -//! asociados a cada región. +//! Una página ([`Page`](crate::response::page::Page)) es un documento HTML completo. Implementa +//! [`Contextual`](crate::core::component::Contextual) para gestionar su propio [`Context`], donde +//! mantiene el tema activo, la plantilla seleccionada y los componentes asociados a cada región a +//! renderizar. //! -//! De este modo, temas y extensiones colaboran sobre una estructura común: las aplicaciones -//! registran componentes en el [`Context`], las plantillas organizan las regiones y las páginas -//! generan el documento HTML resultante. +//! Además, PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre. Un +//! tema hijo hereda automáticamente todos los métodos del padre y puede sobrescribirlos +//! selectivamente. Por ejemplo, puede redefinir el renderizado de un componente a través de +//! [`Theme::handle_component()`] sin cambiar el resto del comportamiento heredado. Un tema hijo +//! puede ser a su vez padre de otro, basta declararlo cada vez con [`Theme::parent()`]. //! -//! PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre. Un tema -//! hijo hereda automáticamente todos los métodos del padre y puede sobrescribirlos selectivamente: -//! por ejemplo, puede redefinir el renderizado de un componente con [`Theme::handle_component()`] -//! sin modificar el resto del comportamiento heredado. Un tema hijo puede ser a su vez padre de -//! otro, basta declararlo cada vez con [`Theme::parent()`]. +//! # Cómo crear un tema nuevo //! -//! Los temas pueden definir sus propias implementaciones de [`Template`] y [`Region`] (por ejemplo, -//! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas. +//! Un tema mínimo es una extensión que implementa [`Extension`](crate::core::extension::Extension) +//! y también [`Theme`] para que [`Extension::theme()`](crate::core::extension::Extension::theme) +//! devuelva `Some(&Self)`. Basta con un `impl Theme for MyTheme {}` vacío, ya que todos los +//! métodos de [`Theme`] tienen implementación por defecto. +//! +//! Un tema puede personalizarse en tres pasos, cada uno necesario sólo si lo que ofrece PageTop por +//! defecto no basta: +//! +//! 1. **Definir regiones propias**. Por defecto PageTop define [`DefaultRegions`], con tres +//! regiones (`Header`, `Content` y `Footer`) que usan la implementación por defecto de +//! [`Region::render()`]. Un tema puede definir un *enum* propio que implemente [`Region`] para +//! exponer sus propias regiones (por ejemplo, una barra lateral) o para cambiar cómo se muestra +//! el contenido de una región ya existente (identificada por su nombre). +//! 2. **Definir plantillas propias**. Por defecto existe [`DefaultTemplates`], con dos plantillas +//! (`Standard` y `Admin`) que usan la implementación por defecto de [`Template::render()`] para +//! renderizar [`DefaultRegions::Header`], [`DefaultRegions::Content`] y +//! [`DefaultRegions::Footer`], en este orden. Un tema puede definir un *enum* propio que +//! implemente [`Template`] para crear nuevas plantillas, maquetar las regiones de otra forma, +//! cambiar su orden o envolverlas en contenedores adicionales. +//! 3. **Elegir las plantillas predeterminadas**. Por un lado, la plantilla por defecto vía +//! [`Theme::default_template()`] y, por otro, la plantilla para las páginas de administración, +//! [`Theme::admin_template()`]. De esta forma, las páginas creadas con `Page::new()` usarán +//! automáticamente la plantilla `default_template()` del tema activo, y las páginas creadas con +//! `Page::admin()` usarán la de `admin_template()`, sin tener que llamar manualmente a +//! [`with_template()`](crate::core::component::Contextual::with_template). Un tema que no +//! sobrescriba estos métodos sigue usando las plantillas por defecto de PageTop. +//! +//! Las páginas de error (403, 404, y otros errores fatales) no tienen una plantilla propia: se +//! renderizan con la plantilla ya activa en la página, para que el usuario no pierda el contexto de +//! navegación del sitio. Los temas pueden personalizar su contenido sobrescribiendo +//! [`Theme::error_403()`], [`Theme::error_404()`] o [`Theme::error_fatal()`], sin necesidad de una +//! plantilla distinta. +//! +//! El resto del comportamiento de un tema (renderizado del ``, o intervención en el +//! renderizado de componentes concretos con [`Theme::handle_component()`]) se sobrescribe de forma +//! independiente de estos tres pasos y no es necesario para tener un tema funcional. +//! +//! # Componentes que se procesan en todas las páginas +//! +//! Los componentes añadidos a una página con +//! [`with_child_in()`](crate::core::component::Contextual::with_child_in) sólo existen para esa +//! petición concreta: hay que volver a añadirlos cada vez que se construya la página. [`InRegion`] +//! resuelve el caso contrario: un componente que se debe procesar en todas las páginas, o en todas +//! las de un tema concreto, sin tener que registrarlo en el código de cada página. +//! +//! `InRegion` registra el componente una sola vez, normalmente al arrancar la aplicación o al +//! inicializar una extensión, y a partir de ahí se procesa automáticamente en todas las páginas que +//! correspondan: +//! +//! ```rust,no_run +//! # use pagetop::prelude::*; +//! InRegion::Global(&DefaultRegions::Footer).add(PoweredBy::new()); +//! ``` +//! +//! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del +//! renderizado, de modo que su `setup()` siempre parte de un estado inicial limpio y no acumula +//! mutaciones entre peticiones. +//! +//! Como cualquier otro componente, antes de renderizarse pasa por +//! [`is_renderable()`](crate::core::component::Component::is_renderable), el primer paso del +//! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una +//! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o +//! si el usuario actual está autenticado. use crate::async_trait; use crate::core::component::Context; @@ -44,7 +99,7 @@ use crate::{AutoDefault, util}; /// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas /// implementaciones de [`Region`] que devuelvan el mismo nombre compartirán el mismo conjunto de /// componentes registrados en el [`Context`], aunque cada región puede renderizar ese contenido de -/// forma diferente. Por ejemplo, [`DefaultRegion::Header`] y `BootsierRegion::Header` mostrarían +/// forma diferente. Por ejemplo, [`DefaultRegions::Header`] y `BootsierRegions::Header` mostrarían /// los mismos componentes si ambas devuelven el nombre `"header"`, pero podrían maquetarse de /// manera distinta. /// @@ -102,7 +157,7 @@ pub trait Region: Send + Sync { /// Referencia estática a una región. pub type RegionRef = &'static dyn Region; -// **< DefaultRegion >****************************************************************************** +// **< DefaultRegions >***************************************************************************** /// Regiones básicas que PageTop proporciona por defecto. /// @@ -110,7 +165,7 @@ pub type RegionRef = &'static dyn Region; /// equivalente definida por otros temas, por lo que comparten también el contenido registrado bajo /// esos nombres. #[derive(AutoDefault)] -pub enum DefaultRegion { +pub enum DefaultRegions { /// Región estándar para la **cabecera** del documento, de nombre `"header"`. /// /// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc. @@ -129,7 +184,7 @@ pub enum DefaultRegion { Footer, } -impl Region for DefaultRegion { +impl Region for DefaultRegions { #[inline] fn name(&self) -> &'static str { match self { @@ -160,8 +215,8 @@ impl Region for DefaultRegion { pub trait Template: Send + Sync { /// Renderiza el contenido de la plantilla. /// - /// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden: - /// [`DefaultRegion::Header`], [`DefaultRegion::Content`] y [`DefaultRegion::Footer`]. + /// Por defecto, renderiza las regiones básicas de [`DefaultRegions`] en este orden: + /// [`DefaultRegions::Header`], [`DefaultRegions::Content`] y [`DefaultRegions::Footer`]. /// /// Se puede sobrescribir este método para: /// @@ -175,9 +230,9 @@ pub trait Template: Send + Sync { /// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())). async fn render(&self, cx: &mut Context) -> Markup { html! { - (DefaultRegion::Header.render(cx).await) - (DefaultRegion::Content.render(cx).await) - (DefaultRegion::Footer.render(cx).await) + (DefaultRegions::Header.render(cx).await) + (DefaultRegions::Content.render(cx).await) + (DefaultRegions::Footer.render(cx).await) } } } @@ -185,11 +240,11 @@ pub trait Template: Send + Sync { /// Referencia estática a una plantilla. pub type TemplateRef = &'static dyn Template; -// **< DefaultTemplate >**************************************************************************** +// **< DefaultTemplates >*************************************************************************** /// Plantillas que PageTop proporciona por defecto. #[derive(AutoDefault)] -pub enum DefaultTemplate { +pub enum DefaultTemplates { /// Plantilla predeterminada. /// /// Utiliza la implementación por defecto de [`Template::render()`] y se emplea cuando no se @@ -197,15 +252,15 @@ pub enum DefaultTemplate { #[default] Standard, - /// Plantilla de error. + /// Plantilla para la **interfaz de administración**. /// - /// Se utiliza para páginas de error u otros estados excepcionales. Por defecto utiliza la misma + /// Se utiliza para páginas de administración o paneles de control. Por defecto utiliza la misma /// implementación de [`Template::render()`] que [`Self::Standard`]. - Error, + Admin, } #[async_trait] -impl Template for DefaultTemplate {} +impl Template for DefaultTemplates {} // **< render_component! >************************************************************************** diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 720d128c..318e8c39 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -2,7 +2,7 @@ use crate::async_trait; use crate::base::component::{Html, Intro, IntroOpening}; use crate::core::component::{ChildOp, Component, ComponentError, Context, Contextual}; use crate::core::extension::Extension; -use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef}; +use crate::core::theme::{DefaultRegions, DefaultTemplates, TemplateRef}; use crate::global; use crate::html::{Markup, html}; use crate::locale::L10n; @@ -66,14 +66,25 @@ pub trait Theme: Extension + Send + Sync { /// ([`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. + /// La implementación por defecto devuelve la plantilla [`DefaultTemplates::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 { - self.parent().map_or(&DefaultTemplate::Standard, |parent| { - parent.default_template() - }) + self.parent() + .map_or(&DefaultTemplates::Standard, |p| p.default_template()) + } + + /// Devuelve la plantilla ([`Template`](crate::core::theme::Template)) que el tema propone para + /// la interfaz de administración. + /// + /// La implementación por defecto devuelve la plantilla [`DefaultTemplates::Admin`] con una + /// estructura básica para la interfaz de administración. Los temas pueden sobrescribir este + /// método para seleccionar otra plantilla predeterminada o una plantilla propia. + #[inline] + fn admin_template(&self) -> TemplateRef { + self.parent() + .map_or(&DefaultTemplates::Admin, |p| p.admin_template()) } /// Acciones específicas del tema antes de renderizar el `` de la página. @@ -101,9 +112,9 @@ pub trait Theme: Extension + Send + Sync { /// 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. + /// [`DefaultRegions::Header`](crate::core::theme::DefaultRegions::Header), + /// [`DefaultRegions::Content`](crate::core::theme::DefaultRegions::Content) y + /// [`DefaultRegions::Footer`](crate::core::theme::DefaultRegions::Footer) en ese orden. /// /// Los temas pueden sobrescribir este método para: /// @@ -236,90 +247,101 @@ pub trait Theme: Extension + Send + Sync { /// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado). /// - /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la - /// página de error. + /// Normalmente se renderiza con la plantilla predeterminada del tema (por defecto suele ser + /// [`DefaultTemplates::Standard`]), para que el usuario no pierda el contexto de navegación del + /// sitio. Los temas pueden sobrescribir este método para personalizar completamente el diseño y + /// el contenido de la página de error. fn error_403(&self, page: &mut Page) { if let Some(parent) = self.parent() { return parent.error_403(page); } - page.alter_title(L10n::l("error403_title")) - .alter_template(&DefaultTemplate::Error) - .alter_child_in( - &DefaultRegion::Content, - ChildOp::Prepend( - Html::with(move |cx| { - html! { - div { - h1 { (L10n::l("error403_alert").using(cx)) } - p { (L10n::l("error403_help").using(cx)) } - } + page.alter_title(L10n::l("error403_title")).alter_child_in( + &DefaultRegions::Content, + ChildOp::Prepend( + Html::with(move |cx| { + html! { + div { + h1 { (L10n::l("error403_alert").using(cx)) } + p { (L10n::l("error403_help").using(cx)) } } - }) - .into(), - ), - ); + } + }) + .into(), + ), + ); } /// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado). /// - /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la - /// página de error. + /// Normalmente se renderiza con la plantilla predeterminada del tema (por defecto suele ser + /// [`DefaultTemplates::Standard`]). Los temas pueden sobrescribir este método para personalizar + /// completamente el diseño y el contenido de la página de error. fn error_404(&self, page: &mut Page) { if let Some(parent) = self.parent() { return parent.error_404(page); } - page.alter_title(L10n::l("error404_title")) - .alter_template(&DefaultTemplate::Error) - .alter_child_in( - &DefaultRegion::Content, - ChildOp::Prepend( - Html::with(move |cx| { - html! { - div { - h1 { (L10n::l("error404_alert").using(cx)) } - p { (L10n::l("error404_help").using(cx)) } - } + page.alter_title(L10n::l("error404_title")).alter_child_in( + &DefaultRegions::Content, + ChildOp::Prepend( + Html::with(move |cx| { + html! { + div { + h1 { (L10n::l("error404_alert").using(cx)) } + p { (L10n::l("error404_help").using(cx)) } } - }) - .into(), - ), - ); + } + }) + .into(), + ), + ); } - /// Permite al tema preparar y componer una página de error fatal. + /// Permite al tema preparar y componer una página de **error fatal controlado**. /// - /// Por defecto, asigna el título al documento (`title`) y muestra un componente [`Intro`] con - /// el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y `help`) como - /// descripción del error. + /// Esta función decide explícitamente devolver + /// [`ErrorPage::BadRequest`](crate::response::page::ErrorPage::BadRequest), + /// [`ErrorPage::InternalError`](crate::response::page::ErrorPage::InternalError), + /// [`ErrorPage::ServiceUnavailable`](crate::response::page::ErrorPage::ServiceUnavailable) o + /// [`ErrorPage::GatewayTimeout`](crate::response::page::ErrorPage::GatewayTimeout) porque algo + /// ha fallado, pero el servidor sigue activo y el tema, el renderizado y el resto de + /// componentes funcionan con normalidad. + /// + /// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya + /// activa en la página (normalmente [`DefaultTemplates::Standard`]) y muestra un componente + /// [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y + /// `help`) como descripción del error. /// /// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni /// [`Self::error_404()`], que definen su propio contenido específico. /// + /// Tampoco cubre los **fallos catastróficos** (un `panic!` en un handler, componente o + /// plantilla). Ese caso se intercepta en una última capa que responde con un HTML mínimo y + /// autónomo, sin pasar por el tema ni por el ciclo de renderizado de componentes, porque no es + /// seguro asumir que ese ciclo sigue funcionando tras un `panic!`. + /// /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la /// página de error. fn error_fatal(&self, page: &mut Page, code: StatusCode, title: L10n, alert: L10n, help: L10n) { if let Some(parent) = self.parent() { return parent.error_fatal(page, code, title, alert, help); } - page.alter_title(title) - .alter_template(&DefaultTemplate::Error) - .alter_child_in( - &DefaultRegion::Content, - ChildOp::Prepend( - Intro::new() - .with_title(L10n::l("error_code").with_arg("code", code.to_string())) - .with_slogan(L10n::n(code.to_string())) - .with_button(None) - .with_opening(IntroOpening::Custom) - .with_child(Html::with(move |cx| { - html! { - h1 { (alert.using(cx)) } - p { (help.using(cx)) } - } - })) - .into(), - ), - ); + page.alter_title(title).alter_child_in( + &DefaultRegions::Content, + ChildOp::Prepend( + Intro::new() + .with_title(L10n::l("error_code").with_arg("code", code.to_string())) + .with_slogan(L10n::n(code.to_string())) + .with_button(None) + .with_opening(IntroOpening::Custom) + .with_child(Html::with(move |cx| { + html! { + h1 { (alert.using(cx)) } + p { (help.using(cx)) } + } + })) + .into(), + ), + ); } } diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 8ac9ce55..abf19bfb 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,5 +1,5 @@ use crate::core::component::{Child, ChildOp, Children, Component}; -use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef}; +use crate::core::theme::{DefaultRegions, RegionRef, ThemeRef}; use crate::{AutoDefault, UniqueId, builder_fn}; use parking_lot::RwLock; @@ -118,15 +118,15 @@ impl ChildrenInRegions { /// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" })); /// /// // Texto en la cabecera, visible en todos los temas. -/// InRegion::Global(&DefaultRegion::Header).add(Html::with(|_| html! { "Publicidad" })); +/// InRegion::Global(&DefaultRegions::Header).add(Html::with(|_| html! { "Publicidad" })); /// ``` pub enum InRegion { /// 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 + /// convención, esta región corresponde a [`DefaultRegions::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 + /// directamente [`DefaultRegions::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)). @@ -163,19 +163,19 @@ impl InRegion { /// })); /// /// // Texto en la cabecera. - /// InRegion::Global(&DefaultRegion::Header).add(Html::with(|_| { + /// InRegion::Global(&DefaultRegions::Header).add(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(Html::with(|_| { + /// InRegion::ForTheme(&theme::Basic, &DefaultRegions::Footer).add(Html::with(|_| { /// html! { "Aviso legal" } /// })); /// ``` pub fn add(&self, component: impl Component + Clone + 'static) -> &Self { let proto: Arc = Arc::new(component); match self { - InRegion::Content => Self::add_to_common(&DefaultRegion::Content, proto), + InRegion::Content => Self::add_to_common(&DefaultRegions::Content, proto), InRegion::Global(region_ref) => Self::add_to_common(*region_ref, proto), InRegion::ForTheme(theme_ref, region_ref) => { THEME_REGIONS diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index 1ddbff28..397ee689 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -15,7 +15,7 @@ use crate::{AutoDefault, CowStr, Weight, util}; /// ejecuta en cuanto esté listo, **sin garantizar** el orden relativo respecto a otros scripts. /// - [`Inline`] - Inserta el código directamente en la etiqueta `