diff --git a/src/app.rs b/src/app.rs index 400b0cd..c3576fc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,9 +3,6 @@ mod figfont; use crate::core::{extension, extension::ExtensionRef}; -use crate::html::Markup; -use crate::response::page::{ErrorPage, ResultPage}; -use crate::service::HttpRequest; use crate::{global, locale, service, trace}; use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle}; @@ -173,12 +170,6 @@ impl Application { InitError = (), >, > { - service::App::new() - .configure(extension::all::configure_services) - .default_service(service::web::route().to(service_not_found)) + service::App::new().configure(extension::all::configure_services) } } - -async fn service_not_found(request: HttpRequest) -> ResultPage { - Err(ErrorPage::NotFound(request)) -} diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index 6df7042..ac29259 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -26,7 +26,7 @@ pub type ExtensionRef = &'static dyn Extension; /// } /// ``` pub trait Extension: AnyInfo + Send + Sync { - /// Nombre localizado de la extensión legible para el usuario. + /// Nombre legible para el usuario. /// /// Predeterminado por el [`short_name()`](AnyInfo::short_name) del tipo asociado a la /// extensión. @@ -34,15 +34,18 @@ pub trait Extension: AnyInfo + Send + Sync { L10n::n(self.short_name()) } - /// Descripción corta localizada de la extensión para paneles, listados, etc. + /// Descripción corta para paneles, listados, etc. fn description(&self) -> L10n { L10n::default() } - /// Devuelve una referencia a esta misma extensión cuando se trata de un tema. + /// Los temas son extensiones que implementan [`Extension`] y también + /// [`Theme`](crate::core::theme::Theme). /// - /// Para ello, debe implementar [`Extension`] y también [`Theme`](crate::core::theme::Theme). Si - /// la extensión no es un tema, este método devuelve `None` por defecto. + /// Si la extensión no es un tema, este método devuelve `None` por defecto. + /// + /// En caso contrario, este método debe implementarse para devolver una referencia de sí mismo + /// como tema. Por ejemplo: /// /// ```rust /// use pagetop::prelude::*; @@ -78,7 +81,7 @@ pub trait Extension: AnyInfo + Send + Sync { actions_boxed![] } - /// Inicializa la extensión durante la fase de arranque de la aplicación. + /// Inicializa la extensión durante la lógica de arranque de la aplicación. /// /// Se llama una sola vez, después de que todas las dependencias se han inicializado y antes de /// aceptar cualquier petición HTTP. @@ -101,8 +104,8 @@ pub trait Extension: AnyInfo + Send + Sync { #[allow(unused_variables)] fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {} - /// Permite crear extensiones para deshabilitar y desinstalar recursos de otras de versiones - /// anteriores de la aplicación. + /// Permite crear extensiones para deshabilitar y desinstalar los recursos de otras extensiones + /// utilizadas en versiones anteriores de la aplicación. /// /// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones. fn drop_extensions(&self) -> Vec { diff --git a/src/core/theme.rs b/src/core/theme.rs index aa526f1..0a0f819 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -9,16 +9,18 @@ //! tipografías, espaciados y cualquier otro detalle visual o de comportamiento (como animaciones, //! *scripts* de interfaz, etc.). //! -//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension); por -//! lo que se instancian, declaran sus dependencias y se inician igual que el resto de extensiones; -//! pero serán temas si además implementan [`theme()`](crate::core::extension::Extension::theme) y -//! [`Theme`]. +//! Es una extensión más (implementando [`Extension`](crate::core::extension::Extension)). Se +//! instala, activa y declara dependencias igual que el resto de extensiones; y se señala a sí misma +//! como tema (implementando [`theme()`](crate::core::extension::Extension::theme) y [`Theme`]). mod definition; pub use definition::{Theme, ThemeRef}; mod regions; pub(crate) use regions::ChildrenInRegions; -pub use regions::{InRegion, Region, REGION_CONTENT}; +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/definition.rs b/src/core/theme/definition.rs index 8d1b632..8de88bd 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -1,12 +1,10 @@ use crate::core::extension::Extension; -use crate::core::theme::Region; +use crate::core::theme::CONTENT_REGION_NAME; use crate::global; use crate::html::{html, Markup}; use crate::locale::L10n; use crate::response::page::Page; -use std::sync::LazyLock; - /// Representa una referencia a un tema. /// /// Los temas son también extensiones. Por tanto se deben definir igual, es decir, como instancias @@ -16,7 +14,7 @@ pub type ThemeRef = &'static dyn Theme; /// Interfaz común que debe implementar cualquier tema de `PageTop`. /// /// Un tema implementará [`Theme`] y los métodos que sean necesarios de [`Extension`], aunque el -/// único obligatorio será [`theme()`](Extension::theme). +/// único obligatorio es [`theme()`](Extension::theme). /// /// ```rust /// use pagetop::prelude::*; @@ -24,13 +22,8 @@ pub type ThemeRef = &'static dyn Theme; /// pub struct MyTheme; /// /// impl Extension for MyTheme { -/// fn name(&self) -> L10n { -/// L10n::n("My theme") -/// } -/// -/// fn description(&self) -> L10n { -/// L10n::n("A personal theme") -/// } +/// fn name(&self) -> L10n { L10n::n("My theme") } +/// fn description(&self) -> L10n { L10n::n("Un tema personal") } /// /// fn theme(&self) -> Option { /// Some(&Self) @@ -40,67 +33,21 @@ pub type ThemeRef = &'static dyn Theme; /// impl Theme for MyTheme {} /// ``` pub trait Theme: Extension + Send + Sync { - /// **Obsoleto desde la versión 0.4.0**: usar [`declared_regions()`](Self::declared_regions) en - /// su lugar. - #[deprecated(since = "0.4.0", note = "Use `declared_regions()` instead")] fn regions(&self) -> Vec<(&'static str, L10n)> { - vec![("content", L10n::l("content"))] + vec![(CONTENT_REGION_NAME, L10n::l("content"))] } - /// Declaración ordenada de las regiones disponibles en la página. - /// - /// Devuelve una lista estática de pares `(Region, L10n)` que se usará para renderizar en el - /// orden indicado todas las regiones que componen una página. Los identificadores deben ser - /// **estables** como `"sidebar-left"` o `"content"`. La etiqueta `L10n` devuelve el nombre de la - /// región en el idioma activo de la página. - /// - /// Si el tema requiere un conjunto distinto de regiones, se puede sobrescribir este método para - /// devolver una lista diferente. Si no, se usará la lista predeterminada: - /// - /// - `"header"`: cabecera. - /// - `"content"`: contenido principal (**obligatoria**). - /// - `"footer"`: pie. - /// - /// Sólo la región `"content"` es obligatoria, usa [`Region::default()`] para declararla. - #[inline] - fn declared_regions(&self) -> &'static [(Region, L10n)] { - static REGIONS: LazyLock<[(Region, L10n); 3]> = LazyLock::new(|| { - [ - (Region::declare("header"), L10n::l("region_header")), - (Region::default(), L10n::l("region_content")), - (Region::declare("footer"), L10n::l("region_footer")), - ] - }); - ®IONS[..] - } - - /// Acciones específicas del tema antes de renderizar el `` de la página. - /// - /// Útil para preparar clases, inyectar recursos o ajustar metadatos. #[allow(unused_variables)] fn before_render_page_body(&self, page: &mut Page) {} - /// Renderiza el contenido del `` de la página. - /// - /// Por defecto, recorre [`declared_regions()`](Self::declared_regions) **en el orden que se han - /// declarado** y, para cada región con contenido, genera un contenedor con `role="region"` y - /// `aria-label` localizado. fn render_page_body(&self, page: &mut Page) -> Markup { html! { body id=[page.body_id().get()] class=[page.body_classes().get()] { - @for (region, region_label) in self.declared_regions() { - @let output = page.render_region(region.key()); + @for (region_name, _) in self.regions() { + @let output = page.render_region(region_name); @if !output.is_empty() { - @let region_name = region.name(); - div - id=(region_name) - class="region" - role="region" - aria-label=[region_label.using(page)] - { - div class={ "region__" (region_name) } { - (output) - } + div id=(region_name) class={ "region-container region-" (region_name) } { + (output) } } } @@ -108,16 +55,9 @@ pub trait Theme: Extension + Send + Sync { } } - /// Acciones específicas del tema después de renderizar el `` de la página. - /// - /// Útil para *tracing*, métricas o ajustes finales del estado de la página. #[allow(unused_variables)] fn after_render_page_body(&self, page: &mut Page) {} - /// Renderiza el contenido del `` de la página. - /// - /// Por defecto, genera las etiquetas básicas (`charset`, `title`, `description`, `viewport`, - /// `X-UA-Compatible`), los metadatos y propiedades de la página y los recursos (CSS/JS). fn render_page_head(&self, page: &mut Page) -> Markup { let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; html! { @@ -149,17 +89,11 @@ pub trait Theme: Extension + Send + Sync { } } - /// Página de error "*403 – Forbidden*" predeterminada. - /// - /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema. - fn error403(&self, page: &mut Page) -> Markup { - html! { div { h1 { (L10n::l("error403_notice").to_markup(page)) } } } + fn error403(&self, _page: &mut Page) -> Markup { + html! { div { h1 { ("FORBIDDEN ACCESS") } } } } - /// Página de error "*404 – Not Found*" predeterminada. - /// - /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema. - fn error404(&self, page: &mut Page) -> Markup { - html! { div { h1 { (L10n::l("error404_notice").to_markup(page)) } } } + 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 c8a0555..22ab6f2 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,71 +7,15 @@ use parking_lot::RwLock; use std::collections::HashMap; use std::sync::LazyLock; -// Conjunto de regiones globales asociadas a un tema específico. +// Regiones globales con componentes para un tema dado. static THEME_REGIONS: LazyLock>> = LazyLock::new(|| RwLock::new(HashMap::new())); -// Conjunto de regiones globales comunes a todos los temas. +// Regiones globales con componentes para cualquier tema. static COMMON_REGIONS: LazyLock> = LazyLock::new(|| RwLock::new(ChildrenInRegions::default())); -/// Nombre de la región de contenido por defecto (`"content"`). -pub const REGION_CONTENT: &str = "content"; - -/// Identificador de una región de página. -/// -/// Incluye una **clave estática** ([`key()`](Self::key)) que identifica la región en el tema, y un -/// **nombre normalizado** ([`name()`](Self::name)) en minúsculas para su uso en atributos HTML -/// (p.ej., clases `region__{name}`). -/// -/// Se utiliza para declarar las regiones que componen una página en un tema (ver -/// [`declared_regions()`](crate::core::theme::Theme::declared_regions)). -pub struct Region { - key: &'static str, - name: String, -} - -impl Default for Region { - #[inline] - fn default() -> Self { - Self { - key: REGION_CONTENT, - name: String::from(REGION_CONTENT), - } - } -} - -impl Region { - /// Declara una región a partir de su clave estática. - /// - /// Genera además un nombre normalizado de la clave, eliminando espacios iniciales y finales, - /// convirtiendo a minúsculas y sustituyendo los espacios intermedios por guiones (`-`). - /// - /// Esta clave se usará para añadir componentes a la región; por ello se recomiendan nombres - /// sencillos, limitando los caracteres a `[a-z0-9-]` (p.ej., `"sidebar"` o `"main-menu"`), cuyo - /// nombre normalizado coincidirá con la clave. - #[inline] - pub fn declare(key: &'static str) -> Self { - Self { - key, - name: key.trim().to_ascii_lowercase().replace(' ', "-"), - } - } - - /// Devuelve la clave estática asignada a la región. - #[inline] - pub fn key(&self) -> &'static str { - self.key - } - - /// Devuelve el nombre normalizado de la región (para atributos y búsquedas). - #[inline] - pub fn name(&self) -> &str { - &self.name - } -} - -// Contenedor interno de componentes agrupados por región. +// Estructura interna para mantener los componentes de una región. #[derive(AutoDefault)] pub struct ChildrenInRegions(HashMap<&'static str, Children>); @@ -104,24 +48,25 @@ impl ChildrenInRegions { } } -/// Punto de acceso para añadir componentes a regiones globales o específicas de un tema. +/// Permite añadir componentes a regiones globales o regiones de temas concretos. /// -/// Según la variante, se pueden añadir componentes ([`add()`](Self::add)) que permanecerán -/// disponibles durante toda la ejecución. +/// Dada una región, según la variante seleccionada, se le podrán añadir ([`add()`](Self::add)) +/// componentes que se mantendrán durante la ejecución de la aplicación. /// -/// Estos componentes se renderizarán automáticamente al procesar los documentos HTML que incluyen -/// estas regiones, como las páginas de contenido ([`Page`](crate::response::page::Page)). +/// Estas estructuras de componentes se renderizarán automáticamente al procesar los documentos HTML +/// que las usan, como las páginas de contenido ([`Page`](crate::response::page::Page)), por +/// ejemplo. pub enum InRegion { - /// Región de contenido por defecto. + /// Representa la región por defecto en la que se pueden añadir componentes. Content, - /// Región identificada por el nombre proporcionado. + /// Representa la región con el nombre del argumento. Named(&'static str), - /// Región identificada por un nombre y asociada a un tema concreto. + /// Representa la región con el nombre y del tema especificado en los argumentos. OfTheme(&'static str, ThemeRef), } impl InRegion { - /// Añade un componente a la región indicada por la variante. + /// Permite añadir un componente en la región de la variante seleccionada. /// /// # Ejemplo /// @@ -143,7 +88,7 @@ impl InRegion { InRegion::Content => { COMMON_REGIONS .write() - .alter_child_in_region(REGION_CONTENT, ChildOp::Add(child)); + .alter_child_in_region(CONTENT_REGION_NAME, ChildOp::Add(child)); } InRegion::Named(name) => { COMMON_REGIONS diff --git a/src/html/context.rs b/src/html/context.rs index 72c0ff2..5fbb39b 100644 --- a/src/html/context.rs +++ b/src/html/context.rs @@ -9,9 +9,10 @@ use crate::{builder_fn, join}; use std::collections::HashMap; use std::error::Error; -use std::fmt::{self, Display}; use std::str::FromStr; +use std::fmt; + /// Operaciones para modificar el contexto ([`Context`]) del documento. pub enum AssetsOp { // Favicon. @@ -42,7 +43,7 @@ pub enum ErrorParam { ParseError(String), } -impl Display for ErrorParam { +impl fmt::Display for ErrorParam { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ErrorParam::NotFound => write!(f, "Parameter not found"), diff --git a/src/html/opt_classes.rs b/src/html/opt_classes.rs index a985762..abb3ba4 100644 --- a/src/html/opt_classes.rs +++ b/src/html/opt_classes.rs @@ -25,7 +25,6 @@ pub enum ClassesOp { /// /// - El [orden de las clases no es relevante](https://stackoverflow.com/a/1321712) en CSS. /// - No se permiten clases duplicadas. -/// - Las clases se convierten a minúsculas. /// - Las clases vacías se ignoran. /// /// # Ejemplo @@ -33,8 +32,8 @@ pub enum ClassesOp { /// ```rust /// use pagetop::prelude::*; /// -/// let classes = OptionClasses::new("Btn btn-primary") -/// .with_value(ClassesOp::Add, "Active") +/// let classes = OptionClasses::new("btn btn-primary") +/// .with_value(ClassesOp::Add, "active") /// .with_value(ClassesOp::Remove, "btn-primary"); /// /// assert_eq!(classes.get(), Some(String::from("btn active"))); @@ -52,7 +51,7 @@ impl OptionClasses { #[builder_fn] pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - let classes = classes.as_ref().to_ascii_lowercase(); + let classes: &str = classes.as_ref(); let classes: Vec<&str> = classes.split_ascii_whitespace().collect(); if classes.is_empty() { diff --git a/src/html/opt_id.rs b/src/html/opt_id.rs index 139fdcd..893ac6d 100644 --- a/src/html/opt_id.rs +++ b/src/html/opt_id.rs @@ -7,7 +7,6 @@ use crate::{builder_fn, AutoDefault}; /// # Normalización /// /// - Se eliminan los espacios al principio y al final. -/// - Se convierte a minúsculas. /// - Se sustituyen los espacios intermedios por guiones bajos (`_`). /// - Si el resultado es una cadena vacía, se guarda `None`. /// @@ -16,7 +15,7 @@ use crate::{builder_fn, AutoDefault}; /// ```rust /// use pagetop::prelude::*; /// -/// let id = OptionId::new(" main Section "); +/// let id = OptionId::new("main section"); /// assert_eq!(id.get(), Some(String::from("main_section"))); /// /// let empty = OptionId::default(); @@ -40,7 +39,7 @@ impl OptionId { /// El valor se normaliza automáticamente. #[builder_fn] pub fn with_value(mut self, value: impl AsRef) -> Self { - let value = value.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); + let value = value.as_ref().trim().replace(' ', "_"); self.0 = (!value.is_empty()).then_some(value); self } diff --git a/src/html/opt_name.rs b/src/html/opt_name.rs index ffb0b98..aa74e3b 100644 --- a/src/html/opt_name.rs +++ b/src/html/opt_name.rs @@ -7,7 +7,6 @@ use crate::{builder_fn, AutoDefault}; /// # Normalización /// /// - Se eliminan los espacios al principio y al final. -/// - Se convierte a minúsculas. /// - Se sustituyen los espacios intermedios por guiones bajos (`_`). /// - Si el resultado es una cadena vacía, se guarda `None`. /// @@ -16,7 +15,7 @@ use crate::{builder_fn, AutoDefault}; /// ```rust /// use pagetop::prelude::*; /// -/// let name = OptionName::new(" DISplay name "); +/// let name = OptionName::new(" display name "); /// assert_eq!(name.get(), Some(String::from("display_name"))); /// /// let empty = OptionName::default(); @@ -40,7 +39,7 @@ impl OptionName { /// El valor se normaliza automáticamente. #[builder_fn] pub fn with_value(mut self, value: impl AsRef) -> Self { - let value = value.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); + let value = value.as_ref().trim().replace(' ', "_"); self.0 = (!value.is_empty()).then_some(value); self } diff --git a/src/locale/en-US/theme.ftl b/src/locale/en-US/theme.ftl index f766766..9c71e6b 100644 --- a/src/locale/en-US/theme.ftl +++ b/src/locale/en-US/theme.ftl @@ -1,9 +1,2 @@ -# Regions. -region_header = Header -region_content = Content -region_footer = Footer - -error403_notice = FORBIDDEN ACCESS -error404_notice = RESOURCE NOT FOUND - +content = Content pagetop_logo = PageTop Logo diff --git a/src/locale/es-ES/theme.ftl b/src/locale/es-ES/theme.ftl index b8b9144..f193c53 100644 --- a/src/locale/es-ES/theme.ftl +++ b/src/locale/es-ES/theme.ftl @@ -1,9 +1,2 @@ -# Regions. -region_header = Cabecera -region_content = Contenido -region_footer = Pie de página - -error403_notice = ACCESO NO PERMITIDO -error404_notice = RECURSO NO ENCONTRADO - +content = Contenido pagetop_logo = Logotipo de PageTop diff --git a/src/response/page.rs b/src/response/page.rs index f30e299..44cab72 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -6,7 +6,7 @@ pub use actix_web::Result as ResultPage; use crate::base::action; use crate::builder_fn; use crate::core::component::{Child, ChildOp, Component}; -use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT}; +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, LangId, LanguageIdentifier}; @@ -123,7 +123,7 @@ impl Page { /// Añade un componente a la región de contenido por defecto. pub fn with_component(mut self, component: impl Component) -> Self { self.regions - .alter_child_in_region(REGION_CONTENT, ChildOp::Add(Child::with(component))); + .alter_child_in_region(CONTENT_REGION_NAME, ChildOp::Add(Child::with(component))); self } @@ -172,6 +172,11 @@ impl Page { 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() @@ -245,9 +250,3 @@ impl Page { }) } } - -impl LangId for Page { - fn langid(&self) -> &'static LanguageIdentifier { - self.context.langid() - } -} diff --git a/src/response/page/error.rs b/src/response/page/error.rs index 99ba62b..3a2511c 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -6,7 +6,7 @@ use crate::service::{HttpRequest, HttpResponse}; use super::Page; -use std::fmt::{self, Display}; +use std::fmt; #[derive(Debug)] pub enum ErrorPage { @@ -19,7 +19,7 @@ pub enum ErrorPage { Timeout(HttpRequest), } -impl Display for ErrorPage { +impl fmt::Display for ErrorPage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { // Error 304. diff --git a/src/util.rs b/src/util.rs index e70b099..21537c5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -56,10 +56,8 @@ pub fn resolve_absolute_dir>(path: P) -> io::Result { } } -/// **Obsoleto desde la versión 0.3.0**: usar [`resolve_absolute_dir()`] en su lugar. -/// /// Devuelve la ruta absoluta a un directorio existente. -#[deprecated(since = "0.3.0", note = "Use `resolve_absolute_dir()` instead")] +#[deprecated(since = "0.3.0", note = "Use [`resolve_absolute_dir`] instead")] pub fn absolute_dir(root_path: P, relative_path: Q) -> io::Result where P: AsRef, diff --git a/static/css/welcome.css b/static/css/welcome.css index 76b042b..8f60348 100644 --- a/static/css/welcome.css +++ b/static/css/welcome.css @@ -295,6 +295,11 @@ a:hover:visited { transform: translateX(-100%); } } +#poweredby-link:hover { + transition: all .5s; + transform: rotate(-3deg) scale(1.1); + box-shadow: 0px 3px 5px rgba(0,0,0,.4); +} #poweredby-link:hover span { animation-play-state: paused; } @@ -318,11 +323,6 @@ a:hover:visited { max-width: 29.375rem; margin-bottom: 0; } - #poweredby-link:hover { - transition: all .5s; - transform: rotate(-3deg) scale(1.1); - box-shadow: 0px 3px 5px rgba(0,0,0,.4); - } } .content-text {