♻️ (pagetop): Optimiza cadenas con CowStr

This commit is contained in:
Manuel Cillero 2026-01-06 01:16:09 +01:00
parent cf7aba2b53
commit b39ed38d0d
11 changed files with 229 additions and 159 deletions

View file

@ -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<CowStr>) -> Self {
Self::Responsive(url.into())
}
/// Imagen miniatura (`img-thumbnail`).
#[inline]
pub fn thumbnail(url: impl Into<CowStr>) -> Self {
Self::Thumbnail(url.into())
}
/// Imagen sin clases adicionales.
#[inline]
pub fn plain(url: impl Into<CowStr>) -> Self {
Self::Plain(url.into())
}
/// Devuelve la clase base asociada a la imagen según la fuente.
#[inline]
fn as_str(&self) -> &'static str {

View file

@ -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<Cow<'static, str>>) -> RoutePath {
pub fn route(&self, path: impl Into<CowStr>) -> RoutePath {
let mut route = RoutePath::new(path);
if self.locale.needs_lang_query() {
route.alter_param("lang", self.locale.langid().to_string());

View file

@ -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)

View file

@ -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<Markup>);
pub struct Favicon(Vec<Item>);
/// Elementos que componen un favicon.
#[derive(Clone, Debug)]
enum Item {
/// Etiqueta `<link>` 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<CowStr>,
color: Option<CowStr>,
mime: Option<&'static str>,
},
/// Etiqueta `<meta>` 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<String>) -> Self {
pub fn with_icon(self, image: impl Into<CowStr>) -> 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<String>, sizes: impl Into<String>) -> Self {
pub fn with_icon_for_sizes(self, image: impl Into<CowStr>, sizes: impl Into<CowStr>) -> 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<String>, sizes: impl Into<String>) -> Self {
/// Se recomienda indicar también el tamaño, p. ej. `"180x180"`.
pub fn with_apple_touch_icon(self, image: impl Into<CowStr>, sizes: impl Into<CowStr>) -> 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<String>, color: impl Into<String>) -> Self {
pub fn with_mask_icon(self, image: impl Into<CowStr>, color: impl Into<CowStr>) -> Self {
self.add_icon_item("mask-icon", image.into(), None, Some(color.into()))
}
/// Define el color del tema (`<meta name="theme-color">`).
///
/// Lo usan algunos navegadores para colorear la barra de direcciones o interfaces.
pub fn with_theme_color(mut self, color: impl Into<String>) -> Self {
self.0.push(html! {
meta name="theme-color" content=(color.into());
pub fn with_theme_color(mut self, color: impl Into<CowStr>) -> Self {
self.0.push(Item::Meta {
name: "theme-color",
content: color.into(),
});
self
}
/// Define el color del *tile* en Windows (`<meta name="msapplication-TileColor">`).
pub fn with_ms_tile_color(mut self, color: impl Into<String>) -> Self {
self.0.push(html! {
meta name="msapplication-TileColor" content=(color.into());
pub fn with_ms_tile_color(mut self, color: impl Into<CowStr>) -> Self {
self.0.push(Item::Meta {
name: "msapplication-TileColor",
content: color.into(),
});
self
}
/// Define la imagen del *tile* en Windows (`<meta name="msapplication-TileImage">`).
pub fn with_ms_tile_image(mut self, image: impl Into<String>) -> Self {
self.0.push(html! {
meta name="msapplication-TileImage" content=(image.into());
pub fn with_ms_tile_image(mut self, image: impl Into<CowStr>) -> Self {
self.0.push(Item::Meta {
name: "msapplication-TileImage",
content: image.into(),
});
self
}
/// Función interna que centraliza la creación de las etiquetas `<link>`.
// **< 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 `<link>`.
///
/// - `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<String>,
icon_color: Option<String>,
icon_rel: &'static str,
icon_source: CowStr,
icon_sizes: Option<CowStr>,
icon_color: Option<CowStr>,
) -> 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());
}
}
}
}
}

View file

@ -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 `<script src="...">`.
// - [`Defer`] - Igual que [`From`], pero con el atributo `defer`, descarga en paralelo y se
// ejecuta tras el análisis del documento HTML, respetando el orden de
// aparición.
// - [`Async`] - Igual que [`From`], pero con el atributo `async`, descarga en paralelo y se
// ejecuta en cuanto esté listo, **sin garantizar** el orden relativo respecto a
// otros scripts.
// - [`Inline`] - Inserta el código directamente en la etiqueta `<script>`.
// - [`OnLoad`] - Inserta el código JavaScript y lo ejecuta tras el evento `DOMContentLoaded`.
// - [`OnLoadAsync`] - Igual que [`OnLoad`], pero con manejador asíncrono (`async`), útil si dentro
// del código JavaScript se utiliza `await`.
/// 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 `<script src="...">`.
/// - [`Defer`] - Igual que [`From`], pero con el atributo `defer`, descarga en paralelo y se
/// ejecuta tras el análisis del documento HTML, respetando el orden de
/// aparición.
/// - [`Async`] - Igual que [`From`], pero con el atributo `async`, descarga en paralelo y se
/// ejecuta en cuanto esté listo, **sin garantizar** el orden relativo respecto
/// a otros scripts.
/// - [`Inline`] - Inserta el código directamente en la etiqueta `<script>`.
/// - [`OnLoad`] - Inserta el código JavaScript y lo ejecuta tras el evento `DOMContentLoaded`.
/// - [`OnLoadAsync`] - Igual que [`OnLoad`], pero con manejador asíncrono (`async`), útil si dentro
/// del código JavaScript se utiliza `await`.
#[derive(AutoDefault)]
enum Source {
#[default]
From(String),
Defer(String),
Async(String),
// `name`, `closure(Context) -> String`.
Inline(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
// `name`, `closure(Context) -> String` (se ejecuta tras `DOMContentLoaded`).
OnLoad(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
// `name`, `closure(Context) -> String` (manejador `async` tras `DOMContentLoaded`).
OnLoadAsync(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
From(CowStr),
Defer(CowStr),
Async(CowStr),
/// `name`, `closure(&mut Context) -> String`.
Inline(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
/// `name`, `closure(&mut Context) -> String` (se ejecuta tras `DOMContentLoaded`).
OnLoad(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
/// `name`, `closure(&mut Context) -> String` (manejador `async` tras `DOMContentLoaded`).
OnLoadAsync(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
}
/// Define un recurso **JavaScript** para incluir en un documento HTML.
@ -74,12 +74,11 @@ enum Source {
/// "#, uid)
/// });
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct JavaScript {
source : Source, // Fuente y estrategia de carga del script.
version: String, // Versión del recurso para la caché del navegador.
weight : Weight, // Peso que determina el orden.
source: Source, // Fuente y estrategia de carga del script.
version: CowStr, // Versión del recurso para la caché del navegador.
weight: Weight, // Peso que determina el orden.
}
impl JavaScript {
@ -87,7 +86,7 @@ impl JavaScript {
/// del documento HTML.
///
/// Equivale a `<script src="...">`.
pub fn from(path: impl Into<String>) -> Self {
pub fn from(path: impl Into<CowStr>) -> Self {
Self {
source: Source::From(path.into()),
..Default::default()
@ -99,7 +98,7 @@ impl JavaScript {
///
/// Equivale a `<script src="..." defer>`. Suele ser la opción recomendada para scripts no
/// críticos.
pub fn defer(path: impl Into<String>) -> Self {
pub fn defer(path: impl Into<CowStr>) -> Self {
Self {
source: Source::Defer(path.into()),
..Default::default()
@ -110,7 +109,7 @@ impl JavaScript {
/// tan pronto como esté disponible.
///
/// Equivale a `<script src="..." async>`. **No garantiza** el orden relativo con otros scripts.
pub fn asynchronous(path: impl Into<String>) -> Self {
pub fn asynchronous(path: impl Into<CowStr>) -> Self {
Self {
source: Source::Async(path.into()),
..Default::default()
@ -123,7 +122,7 @@ impl JavaScript {
/// script.
///
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
pub fn inline<F>(name: impl Into<String>, f: F) -> Self
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
{
@ -140,10 +139,10 @@ impl JavaScript {
/// Útil para inicializaciones que no dependen de `await`. El parámetro `name` se usa como
/// identificador interno del script.
///
/// Los scripts con `defer` se ejecutan antes de `DOMContentLoaded`.
/// En condiciones normales, los scripts con `defer` se ejecutan antes de `DOMContentLoaded`.
///
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
pub fn on_load<F>(name: impl Into<String>, f: F) -> Self
pub fn on_load<F>(name: impl Into<CowStr>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
{
@ -161,7 +160,7 @@ impl JavaScript {
/// iniciales.
///
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
pub fn on_load_async<F>(name: impl Into<String>, f: F) -> Self
pub fn on_load_async<F>(name: impl Into<CowStr>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
{
@ -176,7 +175,7 @@ impl JavaScript {
/// Asocia una **versión** al recurso (usada para control de la caché del navegador).
///
/// Si `version` está vacío, **no** se añade ningún parámetro a la URL.
pub fn with_version(mut self, version: impl Into<String>) -> Self {
pub fn with_version(mut self, version: impl Into<CowStr>) -> Self {
self.version = version.into();
self
}

View file

@ -1,22 +1,22 @@
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 CSS y cómo se incluye en el documento.
//
// Los estilos pueden cargarse desde un archivo externo o estar embebidos directamente en una
// etiqueta `<style>`.
//
// - [`From`] - Carga la hoja de estilos desde un archivo externo, insertándola mediante una
// etiqueta `<link>` con `rel="stylesheet"`.
// - [`Inline`] - Inserta directamente el contenido CSS dentro de una etiqueta `<style>`.
/// Define el origen del recurso CSS y cómo se incluye en el documento.
///
/// Los estilos pueden cargarse desde un archivo externo o estar embebidos directamente en una
/// etiqueta `<style>`.
///
/// - [`From`] - Carga la hoja de estilos desde un archivo externo, insertándola mediante una
/// etiqueta `<link>` con `rel="stylesheet"`.
/// - [`Inline`] - Inserta directamente el contenido CSS dentro de una etiqueta `<style>`.
#[derive(AutoDefault)]
enum Source {
#[default]
From(String),
// `name`, `closure(Context) -> String`.
Inline(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
From(CowStr),
/// `name`, `closure(&mut Context) -> String`.
Inline(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
}
/// Define el medio objetivo para la hoja de estilos.
@ -37,14 +37,13 @@ pub enum TargetMedia {
}
/// Devuelve el valor para el atributo `media` (`Some(...)`) o `None` para `Default`.
#[rustfmt::skip]
impl TargetMedia {
const fn as_str(self) -> Option<&'static str> {
match self {
TargetMedia::Default => None,
TargetMedia::Print => Some("print"),
TargetMedia::Screen => Some("screen"),
TargetMedia::Speech => Some("speech"),
TargetMedia::Print => Some("print"),
TargetMedia::Screen => Some("screen"),
TargetMedia::Speech => Some("speech"),
}
}
}
@ -77,20 +76,19 @@ impl TargetMedia {
/// }
/// "#.to_string());
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct StyleSheet {
source : Source, // Fuente y modo de inclusión del CSS.
version: String, // Versión del recurso para la caché del navegador.
media : TargetMedia, // Medio objetivo para los estilos (`print`, `screen`, ...).
weight : Weight, // Peso que determina el orden.
source: Source, // Fuente y modo de inclusión del CSS.
version: CowStr, // Versión del recurso para la caché del navegador.
media: TargetMedia, // Medio objetivo para los estilos (`print`, `screen`, ...).
weight: Weight, // Peso que determina el orden.
}
impl StyleSheet {
/// Crea una hoja de estilos externa.
///
/// Equivale a `<link rel="stylesheet" href="...">`.
pub fn from(path: impl Into<String>) -> Self {
pub fn from(path: impl Into<CowStr>) -> Self {
Self {
source: Source::From(path.into()),
..Default::default()
@ -103,7 +101,7 @@ impl StyleSheet {
/// recurso.
///
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
pub fn inline<F>(name: impl Into<String>, f: F) -> Self
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
{
@ -118,7 +116,7 @@ impl StyleSheet {
/// Asocia una versión al recurso (usada para control de la caché del navegador).
///
/// Si `version` está vacío, no se añade ningún parámetro a la URL.
pub fn with_version(mut self, version: impl Into<String>) -> Self {
pub fn with_version(mut self, version: impl Into<CowStr>) -> Self {
self.version = version.into();
self
}

View file

@ -1,6 +1,5 @@
use crate::{builder_fn, util, AutoDefault};
use crate::{builder_fn, util, AutoDefault, CowStr};
use std::borrow::Cow;
use std::collections::HashSet;
/// Operaciones disponibles sobre la lista de clases en [`Classes`].
@ -19,7 +18,7 @@ pub enum ClassesOp {
Remove,
/// Sustituye una o varias clases existentes (indicadas en la variante) por las clases
/// proporcionadas.
Replace(Cow<'static, str>),
Replace(CowStr),
/// Alterna presencia/ausencia de una o más clases.
///
/// Si en una misma llamada se repite una clase (p. ej. `"a a"`) que ya existe, el resultado
@ -52,10 +51,11 @@ pub enum ClassesOp {
/// # use pagetop::prelude::*;
/// let classes = Classes::new("Btn btn-primary")
/// .with_classes(ClassesOp::Add, "Active")
/// .with_classes(ClassesOp::Replace("active".into()), "Disabled")
/// .with_classes(ClassesOp::Remove, "btn-primary");
///
/// assert_eq!(classes.get(), Some("btn active".to_string()));
/// assert!(classes.contains("active"));
/// assert_eq!(classes.get(), Some("btn disabled".to_string()));
/// assert!(classes.contains("disabled"));
/// ```
#[derive(AutoDefault, Clone, Debug)]
pub struct Classes(Vec<String>);

View file

@ -1,6 +1,5 @@
use crate::{builder_fn, AutoDefault};
use crate::{builder_fn, AutoDefault, CowStr};
use std::borrow::Cow;
use std::fmt;
/// Representa una ruta como un *path* inicial más una lista opcional de parámetros.
@ -28,19 +27,16 @@ use std::fmt;
/// ```
#[derive(AutoDefault)]
pub struct RoutePath {
// *Path* inicial sobre el que se añadirán los parámetros.
//
// Puede ser relativo (p. ej. `/about`) o una ruta completa (`https://example.com/about`).
// `RoutePath` no realiza ninguna validación ni normalización.
//
// Se almacena como `Cow<'static, str>` para reutilizar literales estáticos sin asignación
// adicional y, al mismo tiempo, aceptar rutas dinámicas representadas como `String`.
path: Cow<'static, str>,
/// *Path* inicial sobre el que se añadirán los parámetros.
///
/// Puede ser relativo (p. ej. `/about`) o una ruta completa (`https://example.com/about`).
/// `RoutePath` no realiza ninguna validación ni normalización.
path: CowStr,
// Conjunto de parámetros asociados a la ruta.
//
// Cada clave es única y se mantiene el orden de inserción. El valor vacío se utiliza para
// representar *flags* sin valor explícito (por ejemplo `?debug`).
/// Conjunto de parámetros asociados a la ruta.
///
/// Cada clave es única y se mantiene el orden de inserción. El valor vacío se utiliza para
/// representar *flags* sin valor explícito (por ejemplo `?debug`).
query: indexmap::IndexMap<String, String>,
}
@ -48,7 +44,7 @@ impl RoutePath {
/// Crea un `RoutePath` a partir de un *path* inicial.
///
/// Por ejemplo: `RoutePath::new("/about")`.
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
pub fn new(path: impl Into<CowStr>) -> Self {
Self {
path: path.into(),
query: indexmap::IndexMap::new(),

View file

@ -161,6 +161,13 @@ impl Deref for StaticResources {
}
}
/// Alias para `Cow<'static, str>`.
///
/// Es un puntero inteligente con semántica *copy-on-write* para cadenas. Permite reutilizar
/// literales estáticos sin asignación de memoria adicional y, al mismo tiempo, aceptar cadenas
/// dinámicas representadas como `String`.
pub type CowStr = std::borrow::Cow<'static, str>;
/// Identificador único de un tipo estático durante la ejecución de la aplicación.
///
/// **Nota:** El valor es único sólo dentro del proceso actual y cambia en cada compilación.

View file

@ -1,12 +1,11 @@
use crate::html::{Markup, PreEscaped};
use crate::{include_locales, AutoDefault};
use crate::{include_locales, AutoDefault, CowStr};
use super::{LangId, Locale};
use fluent_templates::Loader;
use fluent_templates::StaticLoader as Locales;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
@ -22,8 +21,8 @@ include_locales!(LOCALES_PAGETOP);
enum L10nOp {
#[default]
None,
Text(Cow<'static, str>),
Translate(Cow<'static, str>),
Text(CowStr),
Translate(CowStr),
}
/// Crea instancias para traducir *textos localizados*.
@ -60,12 +59,12 @@ pub struct L10n {
op: L10nOp,
#[default(&LOCALES_PAGETOP)]
locales: &'static Locales,
args: HashMap<String, String>,
args: Vec<(CowStr, CowStr)>,
}
impl L10n {
/// **n** = *“native”*. Crea una instancia con una cadena literal sin traducción.
pub fn n(text: impl Into<Cow<'static, str>>) -> Self {
pub fn n(text: impl Into<CowStr>) -> Self {
Self {
op: L10nOp::Text(text.into()),
..Default::default()
@ -74,7 +73,7 @@ impl L10n {
/// **l** = *“lookup”*. Crea una instancia para traducir usando una clave del conjunto de
/// traducciones predefinidas.
pub fn l(key: impl Into<Cow<'static, str>>) -> Self {
pub fn l(key: impl Into<CowStr>) -> Self {
Self {
op: L10nOp::Translate(key.into()),
..Default::default()
@ -83,7 +82,7 @@ impl L10n {
/// **t** = *“translate”*. Crea una instancia para traducir usando una clave de un conjunto de
/// traducciones específico.
pub fn t(key: impl Into<Cow<'static, str>>, locales: &'static Locales) -> Self {
pub fn t(key: impl Into<CowStr>, locales: &'static Locales) -> Self {
Self {
op: L10nOp::Translate(key.into()),
locales,
@ -92,8 +91,8 @@ impl L10n {
}
/// Añade un argumento `{$arg}` => `value` a la traducción.
pub fn with_arg(mut self, arg: impl Into<String>, value: impl Into<String>) -> Self {
self.args.insert(arg.into(), value.into());
pub fn with_arg(mut self, arg: impl Into<CowStr>, value: impl Into<CowStr>) -> Self {
self.args.push((arg.into(), value.into()));
self
}
@ -103,8 +102,8 @@ impl L10n {
pub fn with_args<I, K, V>(mut self, args: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
K: Into<CowStr>,
V: Into<CowStr>,
{
self.args
.extend(args.into_iter().map(|(k, v)| (k.into(), v.into())));
@ -153,15 +152,12 @@ impl L10n {
if self.args.is_empty() {
self.locales.try_lookup(language.langid(), key.as_ref())
} else {
self.locales.try_lookup_with_args(
language.langid(),
key.as_ref(),
&self
.args
.iter()
.map(|(k, v)| (Cow::Owned(k.clone()), v.clone().into()))
.collect::<HashMap<_, _>>(),
)
let mut args = HashMap::with_capacity(self.args.len());
for (k, v) in self.args.iter() {
args.insert(k.clone(), v.as_ref().into());
}
self.locales
.try_lookup_with_args(language.langid(), key.as_ref(), &args)
}
}
}

View file

@ -6,7 +6,7 @@ pub use crate::PAGETOP_VERSION;
pub use crate::{builder_fn, html, main, test};
pub use crate::{AutoDefault, Getters, StaticResources, UniqueId, Weight};
pub use crate::{AutoDefault, CowStr, Getters, StaticResources, UniqueId, Weight};
// MACROS.