From b39ed38d0d7e7ff2a3534add6a7ba4f3ba4e369a Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Tue, 6 Jan 2026 01:16:09 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(pagetop):=20Optimiza=20ca?= =?UTF-8?q?denas=20con=20`CowStr`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pagetop-bootsier/src/theme/image/props.rs | 36 ++++- src/core/component/context.rs | 5 +- src/core/theme/definition.rs | 2 +- src/html/assets/favicon.rs | 141 ++++++++++++------ src/html/assets/javascript.rs | 75 +++++----- src/html/assets/stylesheet.rs | 46 +++--- src/html/classes.rs | 10 +- src/html/route.rs | 26 ++-- src/lib.rs | 7 + src/locale/l10n.rs | 38 +++-- src/prelude.rs | 2 +- 11 files changed, 229 insertions(+), 159 deletions(-) diff --git a/extensions/pagetop-bootsier/src/theme/image/props.rs b/extensions/pagetop-bootsier/src/theme/image/props.rs index 2041b2fd..1c6ff275 100644 --- a/extensions/pagetop-bootsier/src/theme/image/props.rs +++ b/extensions/pagetop-bootsier/src/theme/image/props.rs @@ -54,22 +54,46 @@ pub enum Source { Logo(PageTopSvg), /// Imagen que se adapta automáticamente a su contenedor. /// - /// El `String` asociado es la URL (o ruta) de la imagen. - Responsive(String), + /// Lleva asociada la URL (o ruta) de la imagen. + Responsive(CowStr), /// Imagen que aplica el estilo **miniatura** de Bootstrap. /// - /// El `String` asociado es la URL (o ruta) de la imagen. - Thumbnail(String), + /// Lleva asociada la URL (o ruta) de la imagen. + Thumbnail(CowStr), /// Imagen sin clases específicas de Bootstrap, útil para controlar con CSS propio. /// - /// El `String` asociado es la URL (o ruta) de la imagen. - Plain(String), + /// Lleva asociada la URL (o ruta) de la imagen. + Plain(CowStr), } impl Source { const IMG_FLUID: &str = "img-fluid"; const IMG_THUMBNAIL: &str = "img-thumbnail"; + /// Imagen con el logotipo de PageTop. + #[inline] + pub fn logo(svg: PageTopSvg) -> Self { + Self::Logo(svg) + } + + /// Imagen responsive (`img-fluid`). + #[inline] + pub fn responsive(url: impl Into) -> Self { + Self::Responsive(url.into()) + } + + /// Imagen miniatura (`img-thumbnail`). + #[inline] + pub fn thumbnail(url: impl Into) -> Self { + Self::Thumbnail(url.into()) + } + + /// Imagen sin clases adicionales. + #[inline] + pub fn plain(url: impl Into) -> Self { + Self::Plain(url.into()) + } + /// Devuelve la clase base asociada a la imagen según la fuente. #[inline] fn as_str(&self) -> &'static str { diff --git a/src/core/component/context.rs b/src/core/component/context.rs index ec013c14..5ac1ebe8 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -6,10 +6,9 @@ use crate::html::{html, Markup, RoutePath}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; use crate::service::HttpRequest; -use crate::{builder_fn, util}; +use crate::{builder_fn, util, CowStr}; use std::any::Any; -use std::borrow::Cow; use std::collections::HashMap; /// Operaciones para modificar recursos asociados al [`Context`] de un documento. @@ -376,7 +375,7 @@ impl Context { /// /// Esto garantiza que los enlaces generados desde el contexto preservan la preferencia de /// idioma del usuario cuando procede. - pub fn route(&self, path: impl Into>) -> RoutePath { + pub fn route(&self, path: impl Into) -> RoutePath { let mut route = RoutePath::new(path); if self.locale.needs_lang_query() { route.alter_param("lang", self.locale.langid().to_string()); diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 81e47756..1055a822 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -215,7 +215,7 @@ pub trait Theme: Extension + Send + Sync { &DefaultRegion::Content, ChildOp::Prepend(Child::with( Intro::new() - .with_title(L10n::l("error_code").with_arg("code", code.as_str())) + .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) diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs index 18cab15c..1a4174bf 100644 --- a/src/html/assets/favicon.rs +++ b/src/html/assets/favicon.rs @@ -1,6 +1,6 @@ use crate::core::component::Context; use crate::html::{html, Markup}; -use crate::AutoDefault; +use crate::{AutoDefault, CowStr}; /// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio. /// @@ -41,7 +41,32 @@ use crate::AutoDefault; /// .with_ms_tile_image("/icons/mstile-144x144.png"); /// ``` #[derive(AutoDefault)] -pub struct Favicon(Vec); +pub struct Favicon(Vec); + +/// Elementos que componen un favicon. +#[derive(Clone, Debug)] +enum Item { + /// Etiqueta `` para iconos. + /// + /// - `rel`: `"icon"`, `"apple-touch-icon"`, `"mask-icon"`, etc. + /// - `href`: URL/ruta del recurso. + /// - `sizes`: tamaños opcionales (p. ej. `"32x32"` o `"16x16 32x32"`). + /// - `color`: color opcional (relevante para `mask-icon`). + /// - `mime`: tipo MIME inferido por la extensión del recurso. + Icon { + rel: &'static str, + href: CowStr, + sizes: Option, + color: Option, + mime: Option<&'static str>, + }, + + /// Etiqueta `` para configuraciones del navegador/sistema. + /// + /// - `name`: `"theme-color"`, `"msapplication-TileColor"`, `"msapplication-TileImage"`, etc. + /// - `content`: valor asociado. + Meta { name: &'static str, content: CowStr }, +} impl Favicon { /// Crea un nuevo `Favicon` vacío. @@ -56,7 +81,7 @@ impl Favicon { /// Le añade un icono genérico apuntando a `image`. El tipo MIME se infiere automáticamente a /// partir de la extensión. - pub fn with_icon(self, image: impl Into) -> Self { + pub fn with_icon(self, image: impl Into) -> Self { self.add_icon_item("icon", image.into(), None, None) } @@ -67,14 +92,14 @@ impl Favicon { /// `"16x16 32x32 48x48"` o usar `any` para iconos escalables (SVG). /// /// No es imprescindible, pero puede mejorar la selección del icono más adecuado. - pub fn with_icon_for_sizes(self, image: impl Into, sizes: impl Into) -> Self { + pub fn with_icon_for_sizes(self, image: impl Into, sizes: impl Into) -> Self { self.add_icon_item("icon", image.into(), Some(sizes.into()), None) } /// Le añade un *Apple Touch Icon*, usado por dispositivos iOS para las pantallas de inicio. /// - /// Se recomienda indicar también el tamaño, p. ej. `"256x256"`. - pub fn with_apple_touch_icon(self, image: impl Into, sizes: impl Into) -> Self { + /// Se recomienda indicar también el tamaño, p. ej. `"180x180"`. + pub fn with_apple_touch_icon(self, image: impl Into, sizes: impl Into) -> Self { self.add_icon_item("apple-touch-icon", image.into(), Some(sizes.into()), None) } @@ -83,71 +108,85 @@ impl Favicon { /// El atributo `color` lo usa Safari para colorear el trazado SVG cuando el icono se muestra en /// modo *Pinned Tab*. Aunque Safari 12+ acepta *favicons normales*, este método garantiza /// compatibilidad con versiones anteriores. - pub fn with_mask_icon(self, image: impl Into, color: impl Into) -> Self { + pub fn with_mask_icon(self, image: impl Into, color: impl Into) -> Self { self.add_icon_item("mask-icon", image.into(), None, Some(color.into())) } /// Define el color del tema (``). /// /// Lo usan algunos navegadores para colorear la barra de direcciones o interfaces. - pub fn with_theme_color(mut self, color: impl Into) -> Self { - self.0.push(html! { - meta name="theme-color" content=(color.into()); + pub fn with_theme_color(mut self, color: impl Into) -> Self { + self.0.push(Item::Meta { + name: "theme-color", + content: color.into(), }); self } /// Define el color del *tile* en Windows (``). - pub fn with_ms_tile_color(mut self, color: impl Into) -> Self { - self.0.push(html! { - meta name="msapplication-TileColor" content=(color.into()); + pub fn with_ms_tile_color(mut self, color: impl Into) -> Self { + self.0.push(Item::Meta { + name: "msapplication-TileColor", + content: color.into(), }); self } /// Define la imagen del *tile* en Windows (``). - pub fn with_ms_tile_image(mut self, image: impl Into) -> Self { - self.0.push(html! { - meta name="msapplication-TileImage" content=(image.into()); + pub fn with_ms_tile_image(mut self, image: impl Into) -> Self { + self.0.push(Item::Meta { + name: "msapplication-TileImage", + content: image.into(), }); self } - /// Función interna que centraliza la creación de las etiquetas ``. + // **< Favicon HELPERS >************************************************************************ + + /// Infiere el tipo MIME (`type="..."`) a partir de la extensión del recurso. + #[inline] + fn infer_mime(href: &str) -> Option<&'static str> { + // Ignora query/fragment sin asignaciones (p. ej. ".png?v=1" o ".svg#v2"). + let href = href.split_once('#').map(|(s, _)| s).unwrap_or(href); + let href = href.split_once('?').map(|(s, _)| s).unwrap_or(href); + + let (_, ext) = href.rsplit_once('.')?; + + match ext.len() { + 3 if ext.eq_ignore_ascii_case("gif") => Some("image/gif"), + 3 if ext.eq_ignore_ascii_case("ico") => Some("image/x-icon"), + 3 if ext.eq_ignore_ascii_case("jpg") => Some("image/jpeg"), + 3 if ext.eq_ignore_ascii_case("png") => Some("image/png"), + 3 if ext.eq_ignore_ascii_case("svg") => Some("image/svg+xml"), + 4 if ext.eq_ignore_ascii_case("avif") => Some("image/avif"), + 4 if ext.eq_ignore_ascii_case("jpeg") => Some("image/jpeg"), + 4 if ext.eq_ignore_ascii_case("webp") => Some("image/webp"), + _ => None, + } + } + + /// Centraliza la creación de los elementos ``. /// /// - `icon_rel`: indica el tipo de recurso (`"icon"`, `"apple-touch-icon"`, etc.). - /// - `icon_source`: URL del recurso. - /// - `icon_sizes`: tamaños opcionales. - /// - `icon_color`: color opcional (solo relevante para `mask-icon`). + /// - `href`: URL del recurso. + /// - `sizes`: tamaños opcionales. + /// - `color`: color opcional (solo relevante para `mask-icon`). /// /// También infiere automáticamente el tipo MIME (`type`) según la extensión del archivo. fn add_icon_item( mut self, - icon_rel: &str, - icon_source: String, - icon_sizes: Option, - icon_color: Option, + icon_rel: &'static str, + icon_source: CowStr, + icon_sizes: Option, + icon_color: Option, ) -> Self { - let icon_type = match icon_source.rfind('.') { - Some(i) => match icon_source[i..].to_string().to_lowercase().as_str() { - ".avif" => Some("image/avif"), - ".gif" => Some("image/gif"), - ".ico" => Some("image/x-icon"), - ".jpg" | ".jpeg" => Some("image/jpeg"), - ".png" => Some("image/png"), - ".svg" => Some("image/svg+xml"), - ".webp" => Some("image/webp"), - _ => None, - }, - _ => None, - }; - self.0.push(html! { - link - rel=(icon_rel) - type=[(icon_type)] - sizes=[(icon_sizes)] - color=[(icon_color)] - href=(icon_source); + let mime = Self::infer_mime(icon_source.as_ref()); + self.0.push(Item::Icon { + rel: icon_rel, + href: icon_source, + sizes: icon_sizes, + color: icon_color, + mime, }); self } @@ -161,7 +200,19 @@ impl Favicon { pub fn render(&self, _cx: &mut Context) -> Markup { html! { @for item in &self.0 { - (item) + @match item { + Item::Icon { rel, href, sizes, color, mime } => { + link + rel=(rel) + type=[*mime] + sizes=[sizes.as_deref()] + color=[color.as_deref()] + href=(href.as_ref()); + } + Item::Meta { name, content } => { + meta name=(name) content=(content.as_ref()); + } + } } } } diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index 493ff863..8e55cce8 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -1,36 +1,36 @@ use crate::core::component::Context; use crate::html::assets::Asset; use crate::html::{html, Markup, PreEscaped}; -use crate::{util, AutoDefault, Weight}; +use crate::{util, AutoDefault, CowStr, Weight}; -// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador. -// -// Los distintos modos de carga permiten optimizar el rendimiento y controlar el comportamiento del -// script en relación con el análisis del documento HTML y la ejecución del resto de scripts. -// -// - [`From`] - Carga estándar con la etiqueta `