diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index dda79e45..6c288062 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -105,7 +105,7 @@ impl Extension for SuperMenu { )), )); - InRegion::Global(&DefaultRegions::Header).add( + InRegion::Global(&DefaultRegion::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 87d81b6c..55f256b1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,31 +3,20 @@ mod figfont; use crate::core::{extension, extension::ExtensionRef}; +use crate::html::Markup; use crate::locale::Locale; -use crate::response::page::{render_error_pages, response_for_panic, route_not_found}; -use crate::web::Router; +use crate::response::page::{ErrorPage, render_error_pages}; +use crate::web::{HttpRequest, 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 [`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. +/// 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). pub struct Application; impl Application { @@ -45,7 +34,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 { @@ -117,16 +106,12 @@ 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. @@ -170,3 +155,7 @@ 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 e9f96410..a59e23d9 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 71aebdd0..26090da3 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( - &DefaultRegions::Footer, + &DefaultRegion::Footer, ChildOp::AddIfEmpty(PoweredBy::new().into()), ); } diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 4dd7a1dc..b8081edf 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, DefaultRegions, RegionRef, TemplateRef, ThemeRef}; +use crate::core::theme::{ChildrenInRegions, DefaultRegion, 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(&DefaultTemplates::Standard) +/// .with_template(&DefaultTemplate::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,36 +306,13 @@ 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 : TemplateSource, // Plantilla usada para renderizar. + template : TemplateRef, // Plantilla usada para renderizar. favicon : Option, // Favicon, si se ha definido. preloads : Assets, // Recursos para precarga. stylesheets : Assets, // Hojas de estilo CSS. @@ -367,7 +344,7 @@ impl Context { locale, current_user, theme : *DEFAULT_THEME, - template : TemplateSource::Default, + template : DEFAULT_THEME.default_template(), favicon : None, preloads : Assets::::new(), stylesheets: Assets::::new(), @@ -380,7 +357,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 @@ -389,13 +366,6 @@ 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. @@ -564,7 +534,7 @@ impl Contextual for Context { #[builder_fn] fn with_template(mut self, template: TemplateRef) -> Self { - self.template = TemplateSource::Explicit(template); + self.template = template; self } @@ -621,7 +591,7 @@ impl Contextual for Context { #[builder_fn] fn with_child(mut self, op: impl Into) -> Self { self.regions - .alter_child_in(&DefaultRegions::Content, op.into()); + .alter_child_in(&DefaultRegion::Content, op.into()); self } @@ -646,7 +616,7 @@ impl Contextual for Context { } fn template(&self) -> TemplateRef { - self.template.resolve(self.theme) + self.template } fn param(&self, key: &'static str) -> Result<&T, ContextError> { diff --git a/src/core/extension/all.rs b/src/core/extension/all.rs index 3d8baab6..7ceb3fec 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 ad4aaf75..4bbf9c4c 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 6a5bf6a7..46b5d086 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -1,86 +1,31 @@ //! 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 regiones ([`Region`]). Cada región es un contenedor lógico -//! identificado por un nombre para agrupar y renderizar componentes. +//! 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. //! -//! 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. +//! 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. //! -//! 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()`]. +//! 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. //! -//! # Cómo crear un tema nuevo +//! 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()`]. //! -//! 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. +//! 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. use crate::async_trait; use crate::core::component::Context; @@ -99,7 +44,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, [`DefaultRegions::Header`] y `BootsierRegions::Header` mostrarían +/// forma diferente. Por ejemplo, [`DefaultRegion::Header`] y `BootsierRegion::Header` mostrarían /// los mismos componentes si ambas devuelven el nombre `"header"`, pero podrían maquetarse de /// manera distinta. /// @@ -157,7 +102,7 @@ pub trait Region: Send + Sync { /// Referencia estática a una región. pub type RegionRef = &'static dyn Region; -// **< DefaultRegions >***************************************************************************** +// **< DefaultRegion >****************************************************************************** /// Regiones básicas que PageTop proporciona por defecto. /// @@ -165,7 +110,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 DefaultRegions { +pub enum DefaultRegion { /// Región estándar para la **cabecera** del documento, de nombre `"header"`. /// /// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc. @@ -184,7 +129,7 @@ pub enum DefaultRegions { Footer, } -impl Region for DefaultRegions { +impl Region for DefaultRegion { #[inline] fn name(&self) -> &'static str { match self { @@ -215,8 +160,8 @@ impl Region for DefaultRegions { pub trait Template: Send + Sync { /// Renderiza el contenido de la plantilla. /// - /// Por defecto, renderiza las regiones básicas de [`DefaultRegions`] en este orden: - /// [`DefaultRegions::Header`], [`DefaultRegions::Content`] y [`DefaultRegions::Footer`]. + /// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden: + /// [`DefaultRegion::Header`], [`DefaultRegion::Content`] y [`DefaultRegion::Footer`]. /// /// Se puede sobrescribir este método para: /// @@ -230,9 +175,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! { - (DefaultRegions::Header.render(cx).await) - (DefaultRegions::Content.render(cx).await) - (DefaultRegions::Footer.render(cx).await) + (DefaultRegion::Header.render(cx).await) + (DefaultRegion::Content.render(cx).await) + (DefaultRegion::Footer.render(cx).await) } } } @@ -240,11 +185,11 @@ pub trait Template: Send + Sync { /// Referencia estática a una plantilla. pub type TemplateRef = &'static dyn Template; -// **< DefaultTemplates >*************************************************************************** +// **< DefaultTemplate >**************************************************************************** /// Plantillas que PageTop proporciona por defecto. #[derive(AutoDefault)] -pub enum DefaultTemplates { +pub enum DefaultTemplate { /// Plantilla predeterminada. /// /// Utiliza la implementación por defecto de [`Template::render()`] y se emplea cuando no se @@ -252,15 +197,15 @@ pub enum DefaultTemplates { #[default] Standard, - /// Plantilla para la **interfaz de administración**. + /// Plantilla de error. /// - /// Se utiliza para páginas de administración o paneles de control. Por defecto utiliza la misma + /// Se utiliza para páginas de error u otros estados excepcionales. Por defecto utiliza la misma /// implementación de [`Template::render()`] que [`Self::Standard`]. - Admin, + Error, } #[async_trait] -impl Template for DefaultTemplates {} +impl Template for DefaultTemplate {} // **< render_component! >************************************************************************** diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 318e8c39..720d128c 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::{DefaultRegions, DefaultTemplates, TemplateRef}; +use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef}; use crate::global; use crate::html::{Markup, html}; use crate::locale::L10n; @@ -66,25 +66,14 @@ 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 [`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. + /// La implementación por defecto devuelve la plantilla estándar ([`DefaultTemplate::Standard`]) + /// con una estructura básica para la página. Los temas pueden sobrescribir este método para + /// seleccionar otra plantilla predeterminada o una plantilla propia. #[inline] fn default_template(&self) -> TemplateRef { - 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()) + self.parent().map_or(&DefaultTemplate::Standard, |parent| { + parent.default_template() + }) } /// Acciones específicas del tema antes de renderizar el `` de la página. @@ -112,9 +101,9 @@ pub trait Theme: Extension + Send + Sync { /// partir de las regiones. /// /// Con la configuración por defecto, la plantilla estándar utiliza las regiones - /// [`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. + /// [`DefaultRegion::Header`](crate::core::theme::DefaultRegion::Header), + /// [`DefaultRegion::Content`](crate::core::theme::DefaultRegion::Content) y + /// [`DefaultRegion::Footer`](crate::core::theme::DefaultRegion::Footer) en ese orden. /// /// Los temas pueden sobrescribir este método para: /// @@ -247,101 +236,90 @@ pub trait Theme: Extension + Send + Sync { /// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado). /// - /// 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. + /// Los temas pueden sobrescribir este método para personalizar 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_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)) } + 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)) } + } } - } - }) - .into(), - ), - ); + }) + .into(), + ), + ); } /// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado). /// - /// 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. + /// Los temas pueden sobrescribir este método para personalizar 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_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)) } + 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)) } + } } - } - }) - .into(), - ), - ); + }) + .into(), + ), + ); } - /// Permite al tema preparar y componer una página de **error fatal controlado**. + /// Permite al tema preparar y componer una página de error fatal. /// - /// 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. + /// 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. /// /// 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_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(), - ), - ); + 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(), + ), + ); } } diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index abf19bfb..8ac9ce55 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::{DefaultRegions, RegionRef, ThemeRef}; +use crate::core::theme::{DefaultRegion, 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(&DefaultRegions::Header).add(Html::with(|_| html! { "Publicidad" })); +/// InRegion::Global(&DefaultRegion::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 [`DefaultRegions::Content`], cuyo nombre es + /// convención, esta región corresponde a [`DefaultRegion::Content`], cuyo nombre es /// `"content"`. Cualquier tema que renderice esa misma región de contenido, ya sea usando - /// directamente [`DefaultRegions::Content`] o cualquier otra implementación de + /// directamente [`DefaultRegion::Content`] o cualquier otra implementación de /// [`Region`](crate::core::theme::Region) que devuelva ese mismo nombre, mostrará los /// componentes registrados aquí, aunque lo harán según su propio método de renderizado /// ([`Region::render()`](crate::core::theme::Region::render)). @@ -163,19 +163,19 @@ impl InRegion { /// })); /// /// // Texto en la cabecera. - /// InRegion::Global(&DefaultRegions::Header).add(Html::with(|_| { + /// InRegion::Global(&DefaultRegion::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, &DefaultRegions::Footer).add(Html::with(|_| { + /// InRegion::ForTheme(&theme::Basic, &DefaultRegion::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(&DefaultRegions::Content, proto), + InRegion::Content => Self::add_to_common(&DefaultRegion::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 397ee689..1ddbff28 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 `