diff --git a/src/app.rs b/src/app.rs index 94d901f..c8ffba1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -84,7 +84,7 @@ impl Application { if let Some((Width(term_width), _)) = terminal_size() { if term_width >= 80 { let maxlen: usize = ((term_width / 10) - 2).into(); - let mut app = app_name.substring(0, maxlen).to_owned(); + let mut app = app_name.substring(0, maxlen).to_string(); if app_name.len() > maxlen { app = format!("{app}..."); } diff --git a/src/base/component/html.rs b/src/base/component/html.rs index 7bde94a..8fa5690 100644 --- a/src/base/component/html.rs +++ b/src/base/component/html.rs @@ -25,7 +25,7 @@ use crate::prelude::*; /// use pagetop::prelude::*; /// /// let component = Html::with(|cx| { -/// let user = cx.param::("username").cloned().unwrap_or(String::from("visitor")); +/// let user = cx.param::("username").cloned().unwrap_or("visitor".to_string()); /// html! { /// h1 { "Hello, " (user) } /// } diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index d547c4b..333cf69 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -1,6 +1,6 @@ use crate::base::action; use crate::core::{AnyInfo, TypeInfo}; -use crate::html::{html, Context, Markup, PrepareMarkup, Render}; +use crate::html::{html, Context, Markup, PrepareMarkup}; /// Define la función de renderizado para todos los componentes. /// diff --git a/src/core/theme.rs b/src/core/theme.rs index 5889dcf..61d820b 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -7,7 +7,7 @@ //! Un tema **declara las regiones** (*cabecera*, *barra lateral*, *pie*, etc.) que estarán //! disponibles para colocar contenido. Los temas son responsables últimos de los estilos, //! tipografías, espaciados y cualquier otro detalle visual o de comportamiento (como animaciones, -//! *scripts* de interfaz, etc.). +//! 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; diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 1a2e0fb..8082aac 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -36,7 +36,7 @@ impl Default for Region { fn default() -> Self { Self { key: REGION_CONTENT, - name: String::from(REGION_CONTENT), + name: REGION_CONTENT.to_string(), } } } diff --git a/src/html.rs b/src/html.rs index 9f3d70c..4858bbf 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,7 +1,7 @@ //! HTML en código. mod maud; -pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE}; +pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE}; // HTML DOCUMENT ASSETS **************************************************************************** @@ -71,11 +71,11 @@ pub type OptionComponent = core::component::Typed /// use pagetop::prelude::*; /// /// // Texto normal, se escapa automáticamente para evitar inyección de HTML. -/// let fragment = PrepareMarkup::Escaped(String::from("Hola mundo")); +/// let fragment = PrepareMarkup::Escaped("Hola mundo".to_string()); /// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>"); /// /// // HTML literal, se inserta directamente, sin escapado adicional. -/// let raw_html = PrepareMarkup::Raw(String::from("negrita")); +/// let raw_html = PrepareMarkup::Raw("negrita".to_string()); /// assert_eq!(raw_html.render().into_string(), "negrita"); /// /// // Fragmento ya preparado con la macro `html!`. @@ -119,11 +119,9 @@ impl PrepareMarkup { PrepareMarkup::With(markup) => markup.is_empty(), } } -} -impl Render for PrepareMarkup { /// Integra el renderizado fácilmente en la macro [`html!`]. - fn render(&self) -> Markup { + pub fn render(&self) -> Markup { match self { PrepareMarkup::None => html! {}, PrepareMarkup::Escaped(text) => html! { (text) }, diff --git a/src/html/assets.rs b/src/html/assets.rs index ee5431f..41cd471 100644 --- a/src/html/assets.rs +++ b/src/html/assets.rs @@ -2,10 +2,10 @@ pub mod favicon; pub mod javascript; pub mod stylesheet; -use crate::html::{html, Markup, Render}; +use crate::html::{html, Context, Markup}; use crate::{AutoDefault, Weight}; -/// Representación genérica de un *script* [`JavaScript`](crate::html::JavaScript) o una hoja de +/// Representación genérica de un script [`JavaScript`](crate::html::JavaScript) o una hoja de /// estilos [`StyleSheet`](crate::html::StyleSheet). /// /// Estos recursos se incluyen en los conjuntos de recursos ([`Assets`]) que suelen renderizarse en @@ -13,12 +13,15 @@ use crate::{AutoDefault, Weight}; /// /// Cada recurso se identifica por un **nombre único** ([`Asset::name()`]), usado como clave; y un /// **peso** ([`Asset::weight()`]), que determina su orden relativo de renderizado. -pub trait Asset: Render { +pub trait Asset { /// Devuelve el nombre del recurso, utilizado como clave única. fn name(&self) -> &str; /// Devuelve el peso del recurso, usado para ordenar el renderizado de menor a mayor peso. fn weight(&self) -> Weight; + + /// Renderiza el recurso en el contexto proporcionado. + fn render(&self, cx: &mut Context) -> Markup; } /// Gestión común para conjuntos de recursos como [`JavaScript`](crate::html::JavaScript) y @@ -77,16 +80,13 @@ impl Assets { false } } -} -impl Render for Assets { - fn render(&self) -> Markup { + pub fn render(&self, cx: &mut Context) -> Markup { let mut assets = self.0.iter().collect::>(); assets.sort_by_key(|a| a.weight()); - html! { @for a in assets { - (a) + (a.render(cx)) } } } diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs index 1a8b29e..d731b8f 100644 --- a/src/html/assets/favicon.rs +++ b/src/html/assets/favicon.rs @@ -1,4 +1,4 @@ -use crate::html::{html, Markup, Render}; +use crate::html::{html, Context, Markup}; use crate::AutoDefault; /// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio. @@ -129,7 +129,7 @@ impl Favicon { icon_color: Option, ) -> Self { let icon_type = match icon_source.rfind('.') { - Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() { + 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"), @@ -151,10 +151,12 @@ impl Favicon { }); self } -} -impl Render for Favicon { - fn render(&self) -> Markup { + /// Renderiza el **Favicon** completo con todas las etiquetas declaradas. + /// + /// El parámetro `Context` se acepta por coherencia con el resto de *assets*, aunque en este + /// caso es ignorado. + pub fn render(&self, _cx: &mut Context) -> Markup { html! { @for item in &self.0 { (item) diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index be6f906..a8ed3e8 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -1,35 +1,45 @@ use crate::html::assets::Asset; -use crate::html::{html, Markup, Render}; +use crate::html::{html, Context, Markup, PreEscaped}; use crate::{join, join_pair, AutoDefault, 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. +// script en relación con el análisis del documento HTML y la ejecución del resto de scripts. // -// - [`From`] – Carga el script de forma estándar con la etiqueta ``. El parámetro `name` se usa como identificador interno del - /// *script*. - pub fn inline(name: impl Into, script: impl Into) -> Self { + /// script. + /// + /// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado. + pub fn inline(name: impl Into, f: F) -> Self + where + F: Fn(&mut Context) -> String + Send + Sync + 'static, + { JavaScript { - source: Source::Inline(name.into(), script.into()), + source: Source::Inline(name.into(), Box::new(f)), ..Default::default() } } - /// Crea un **script embebido** que se ejecuta automáticamente al terminar de cargarse el - /// documento HTML. + /// Crea un **script embebido** que se ejecuta cuando **el DOM está listo**. /// - /// El código se envuelve automáticamente en un `addEventListener('DOMContentLoaded', ...)`. El - /// parámetro `name` se usa como identificador interno del *script*. - pub fn on_load(name: impl Into, script: impl Into) -> Self { + /// El código se envuelve en un `addEventListener('DOMContentLoaded',function(){...})` que lo + /// ejecuta tras analizar el documento HTML, **no** espera imágenes ni otros recursos externos. + /// Ú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`. + /// + /// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado. + pub fn on_load(name: impl Into, f: F) -> Self + where + F: Fn(&mut Context) -> String + Send + Sync + 'static, + { JavaScript { - source: Source::OnLoad(name.into(), script.into()), + source: Source::OnLoad(name.into(), Box::new(f)), + ..Default::default() + } + } + + /// Crea un **script embebido** con un **manejador asíncrono**. + /// + /// El código se envuelve en un `addEventListener('DOMContentLoaded',async()=>{...})`, que + /// emplea una función `async` para que el cuerpo devuelto por la función *closure* pueda usar + /// `await`. Ideal para hidratar la interfaz, cargar módulos dinámicos o realizar lecturas + /// iniciales. + /// + /// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado. + pub fn on_load_async(name: impl Into, f: F) -> Self + where + F: Fn(&mut Context) -> String + Send + Sync + 'static, + { + JavaScript { + source: Source::OnLoadAsync(name.into(), Box::new(f)), ..Default::default() } } // JavaScript BUILDER ************************************************************************** - /// Asocia una versión al recurso (usada para control de la caché del navegador). + /// 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. + /// 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) -> Self { self.version = version.into(); self } - /// Modifica el peso del recurso. + /// Modifica el **peso** del recurso. /// /// Los recursos se renderizan de menor a mayor peso. Por defecto es `0`, que respeta el orden /// de creación. @@ -140,7 +194,7 @@ impl JavaScript { impl Asset for JavaScript { /// Devuelve el nombre del recurso, utilizado como clave única. /// - /// Para *scripts* externos es la ruta del recurso; para *scripts* embebidos, un identificador. + /// Para scripts externos es la ruta del recurso; para scripts embebidos, un identificador. fn name(&self) -> &str { match &self.source { Source::From(path) => path, @@ -148,16 +202,15 @@ impl Asset for JavaScript { Source::Async(path) => path, Source::Inline(name, _) => name, Source::OnLoad(name, _) => name, + Source::OnLoadAsync(name, _) => name, } } fn weight(&self) -> Weight { self.weight } -} -impl Render for JavaScript { - fn render(&self) -> Markup { + fn render(&self, cx: &mut Context) -> Markup { match &self.source { Source::From(path) => html! { script src=(join_pair!(path, "?v=", self.version.as_str())) {}; @@ -168,12 +221,15 @@ impl Render for JavaScript { Source::Async(path) => html! { script src=(join_pair!(path, "?v=", self.version.as_str())) async {}; }, - Source::Inline(_, code) => html! { - script { (code) }; + Source::Inline(_, f) => html! { + script { (PreEscaped((f)(cx))) }; }, - Source::OnLoad(_, code) => html! { (join!( - "document.addEventListener('DOMContentLoaded',function(){", code, "});" - )) }, + Source::OnLoad(_, f) => html! { script { (PreEscaped(join!( + "document.addEventListener(\"DOMContentLoaded\",function(){", (f)(cx), "});" + ))) } }, + Source::OnLoadAsync(_, f) => html! { script { (PreEscaped(join!( + "document.addEventListener(\"DOMContentLoaded\",async()=>{", (f)(cx), "});" + ))) } }, } } } diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs index 38a97d7..3ecc77f 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -1,5 +1,5 @@ use crate::html::assets::Asset; -use crate::html::{html, Markup, PreEscaped, Render}; +use crate::html::{html, Context, Markup, PreEscaped}; use crate::{join_pair, AutoDefault, Weight}; // Define el origen del recurso CSS y cómo se incluye en el documento. @@ -14,7 +14,8 @@ use crate::{join_pair, AutoDefault, Weight}; enum Source { #[default] From(String), - Inline(String, String), + // `name`, `closure(Context) -> String`. + Inline(String, Box String + Send + Sync>), } /// Define el medio objetivo para la hoja de estilos. @@ -34,7 +35,7 @@ pub enum TargetMedia { Speech, } -/// Devuelve el texto asociado al punto de interrupción usado por Bootstrap. +/// Devuelve el valor para el atributo `media` (`Some(...)`) o `None` para `Default`. #[rustfmt::skip] impl TargetMedia { fn as_str_opt(&self) -> Option<&str> { @@ -69,12 +70,12 @@ impl TargetMedia { /// .with_weight(-10); /// /// // Crea una hoja de estilos embebida en el documento HTML. -/// let embedded = StyleSheet::inline("custom_theme", r#" +/// let embedded = StyleSheet::inline("custom_theme", |_| r#" /// body { /// background-color: #f5f5f5; /// font-family: 'Segoe UI', sans-serif; /// } -/// "#); +/// "#.to_string()); /// ``` #[rustfmt::skip] #[derive(AutoDefault)] @@ -100,9 +101,14 @@ impl StyleSheet { /// /// Equivale a ``. El parámetro `name` se usa como identificador interno del /// recurso. - pub fn inline(name: impl Into, styles: impl Into) -> Self { + /// + /// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado. + pub fn inline(name: impl Into, f: F) -> Self + where + F: Fn(&mut Context) -> String + Send + Sync + 'static, + { StyleSheet { - source: Source::Inline(name.into(), styles.into()), + source: Source::Inline(name.into(), Box::new(f)), ..Default::default() } } @@ -133,9 +139,9 @@ impl StyleSheet { /// Según el argumento `media`: /// /// - `TargetMedia::Default` - Se aplica en todos los casos (medio por defecto). - /// - `TargetMedia::Print` - Se aplican cuando el documento se imprime. - /// - `TargetMedia::Screen` - Se aplican en pantallas. - /// - `TargetMedia::Speech` - Se aplican en dispositivos que convierten el texto a voz. + /// - `TargetMedia::Print` - Se aplica cuando el documento se imprime. + /// - `TargetMedia::Screen` - Se aplica en pantallas. + /// - `TargetMedia::Speech` - Se aplica en dispositivos que convierten el texto a voz. pub fn for_media(mut self, media: TargetMedia) -> Self { self.media = media; self @@ -156,10 +162,8 @@ impl Asset for StyleSheet { fn weight(&self) -> Weight { self.weight } -} -impl Render for StyleSheet { - fn render(&self) -> Markup { + fn render(&self, cx: &mut Context) -> Markup { match &self.source { Source::From(path) => html! { link @@ -167,8 +171,8 @@ impl Render for StyleSheet { href=(join_pair!(path, "?v=", self.version.as_str())) media=[self.media.as_str_opt()]; }, - Source::Inline(_, code) => html! { - style { (PreEscaped(code)) }; + Source::Inline(_, f) => html! { + style { (PreEscaped((f)(cx))) }; }, } } diff --git a/src/html/attr_classes.rs b/src/html/attr_classes.rs index 91ccfaf..098c26c 100644 --- a/src/html/attr_classes.rs +++ b/src/html/attr_classes.rs @@ -37,7 +37,7 @@ pub enum ClassesOp { /// .with_value(ClassesOp::Add, "Active") /// .with_value(ClassesOp::Remove, "btn-primary"); /// -/// assert_eq!(classes.get(), Some(String::from("btn active"))); +/// assert_eq!(classes.get(), Some("btn active".to_string())); /// assert!(classes.contains("active")); /// ``` #[derive(AutoDefault, Clone, Debug)] diff --git a/src/html/attr_l10n.rs b/src/html/attr_l10n.rs index 3e8a4e4..8250c74 100644 --- a/src/html/attr_l10n.rs +++ b/src/html/attr_l10n.rs @@ -17,13 +17,13 @@ use crate::{builder_fn, AutoDefault}; /// // Español disponible. /// assert_eq!( /// hello.lookup(&LangMatch::resolve("es-ES")), -/// Some(String::from("¡Hola mundo!")) +/// Some("¡Hola mundo!".to_string()) /// ); /// /// // Japonés no disponible, traduce al idioma de respaldo ("en-US"). /// assert_eq!( /// hello.lookup(&LangMatch::resolve("ja-JP")), -/// Some(String::from("Hello world!")) +/// Some("Hello world!".to_string()) /// ); /// /// // Uso típico en un atributo: diff --git a/src/html/attr_value.rs b/src/html/attr_value.rs index c70229f..4e03120 100644 --- a/src/html/attr_value.rs +++ b/src/html/attr_value.rs @@ -36,7 +36,7 @@ impl AttrValue { self.0 = if value.is_empty() { None } else { - Some(value.to_owned()) + Some(value.to_string()) }; self } diff --git a/src/html/context.rs b/src/html/context.rs index 26e2478..2f3e0f0 100644 --- a/src/html/context.rs +++ b/src/html/context.rs @@ -25,9 +25,9 @@ pub enum AssetsOp { RemoveStyleSheet(&'static str), // JavaScripts. - /// Añade un *script* JavaScript al documento. + /// Añade un script JavaScript al documento. AddJavaScript(JavaScript), - /// Elimina un *script* por su ruta o identificador. + /// Elimina un script por su ruta o identificador. RemoveJavaScript(&'static str), } @@ -55,7 +55,7 @@ pub enum ErrorParam { /// - Almacenar la **solicitud HTTP** de origen. /// - Seleccionar **tema** y **composición** (*layout*) de renderizado. /// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo -/// [`StyleSheet`] o los *scripts* [`JavaScript`] mediante [`AssetsOp`]. +/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`]. /// - Leer y mantener **parámetros dinámicos tipados** de contexto. /// - Generar **identificadores únicos** por tipo de componente. /// @@ -139,7 +139,7 @@ pub trait Contextual: LangId { /// Devuelve las hojas de estilo de los recursos del contexto. fn stylesheets(&self) -> &Assets; - /// Devuelve los *scripts* JavaScript de los recursos del contexto. + /// Devuelve los scripts JavaScript de los recursos del contexto. fn javascripts(&self) -> &Assets; // Contextual HELPERS ************************************************************************** @@ -155,7 +155,7 @@ pub trait Contextual: LangId { /// /// Extiende [`Contextual`] con métodos para **instanciar** y configurar un nuevo contexto, /// **renderizar los recursos** del documento (incluyendo el [`Favicon`], las hojas de estilo -/// [`StyleSheet`] y los *scripts* [`JavaScript`]), o extender el uso de **parámetros dinámicos +/// [`StyleSheet`] y los scripts [`JavaScript`]), o extender el uso de **parámetros dinámicos /// tipados** con nuevos métodos. /// /// # Ejemplos @@ -258,14 +258,29 @@ impl Context { // Context RENDER ****************************************************************************** /// Renderiza los recursos del contexto. - pub fn render_assets(&self) -> Markup { - html! { - @if let Some(favicon) = &self.favicon { - (favicon) + pub fn render_assets(&mut self) -> Markup { + use std::mem::take as mem_take; + + // Extrae temporalmente los recursos. + let favicon = mem_take(&mut self.favicon); // Deja valor por defecto (None) en self. + let stylesheets = mem_take(&mut self.stylesheets); // Assets::default() en self. + let javascripts = mem_take(&mut self.javascripts); // Assets::default() en self. + + // Renderiza con `&mut self` como contexto. + let markup = html! { + @if let Some(fi) = &favicon { + (fi.render(self)) } - (self.stylesheets) - (self.javascripts) - } + (stylesheets.render(self)) + (javascripts.render(self)) + }; + + // Restaura los campos tal y como estaban. + self.favicon = favicon; + self.stylesheets = stylesheets; + self.javascripts = javascripts; + + markup } // Context PARAMS ****************************************************************************** @@ -285,7 +300,7 @@ impl Context { /// /// let cx = Context::new(None) /// .with_param("usuario_id", 42_i32) - /// .with_param("titulo", String::from("Hola")); + /// .with_param("titulo", "Hola".to_string()); /// /// let id: &i32 = cx.get_param("usuario_id").unwrap(); /// let titulo: &String = cx.get_param("titulo").unwrap(); @@ -318,7 +333,7 @@ impl Context { /// /// let mut cx = Context::new(None) /// .with_param("contador", 7_i32) - /// .with_param("titulo", String::from("Hola")); + /// .with_param("titulo", "Hola".to_string()); /// /// let n: i32 = cx.take_param("contador").unwrap(); /// assert!(cx.get_param::("contador").is_err()); // ya no está @@ -416,7 +431,7 @@ impl Contextual for Context { /// /// let cx = Context::new(None) /// .with_param("usuario_id", 42_i32) - /// .with_param("titulo", String::from("Hola")) + /// .with_param("titulo", "Hola".to_string()) /// .with_param("flags", vec!["a", "b"]); /// ``` #[builder_fn] @@ -484,7 +499,7 @@ impl Contextual for Context { /// ```rust /// use pagetop::prelude::*; /// - /// let cx = Context::new(None).with_param("username", String::from("Alice")); + /// let cx = Context::new(None).with_param("username", "Alice".to_string()); /// /// // Devuelve Some(&String) si existe y coincide el tipo. /// assert_eq!(cx.param::("username").map(|s| s.as_str()), Some("Alice")); @@ -533,7 +548,7 @@ impl Contextual for Context { .replace(' ', "_") .to_lowercase(); let prefix = if prefix.is_empty() { - "prefix".to_owned() + "prefix".to_string() } else { prefix }; diff --git a/src/html/maud.rs b/src/html/maud.rs index 9bf179e..6536036 100644 --- a/src/html/maud.rs +++ b/src/html/maud.rs @@ -69,23 +69,6 @@ impl fmt::Write for Escaper<'_> { /// `.render()` or `.render_to()`. Since the default definitions of /// these methods call each other, not doing this will result in /// infinite recursion. -/// -/// # Example -/// -/// ```rust -/// use pagetop::prelude::*; -/// -/// /// Provides a shorthand for linking to a CSS stylesheet. -/// pub struct Stylesheet(&'static str); -/// -/// impl Render for Stylesheet { -/// fn render(&self) -> Markup { -/// html! { -/// link rel="stylesheet" type="text/css" href=(self.0); -/// } -/// } -/// } -/// ``` pub trait Render { /// Renders `self` as a block of `Markup`. fn render(&self) -> Markup { @@ -238,6 +221,10 @@ impl Markup { pub fn is_empty(&self) -> bool { self.0.is_empty() } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } } impl> PreEscaped { diff --git a/src/locale.rs b/src/locale.rs index cf44dd8..2bf0da9 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -165,7 +165,7 @@ pub trait LangId { /// /// // Idioma no soportado. /// let lang = LangMatch::resolve("ja-JP"); -/// assert_eq!(lang, LangMatch::Unsupported(String::from("ja-JP"))); +/// assert_eq!(lang, LangMatch::Unsupported("ja-JP".to_string())); /// ``` /// /// Con la siguiente instrucción siempre se obtiene un [`LanguageIdentifier`] válido, ya sea porque @@ -222,7 +222,7 @@ impl LangMatch { } // En caso contrario, indica que el idioma no está soportado. - Self::Unsupported(String::from(language)) + Self::Unsupported(language.to_string()) } /// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido. diff --git a/src/response/page.rs b/src/response/page.rs index 86a0bdc..2dc27f9 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -202,7 +202,7 @@ impl Page { } /// Renderiza los recursos de la página. - pub fn render_assets(&self) -> Markup { + pub fn render_assets(&mut self) -> Markup { self.context.render_assets() } diff --git a/src/util.rs b/src/util.rs index 808014b..56b098d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -110,15 +110,15 @@ macro_rules! hm { /// /// // Concatena todos los fragmentos directamente. /// let result = join!("Hello", " ", "World"); -/// assert_eq!(result, String::from("Hello World")); +/// assert_eq!(result, "Hello World".to_string()); /// /// // También funciona con valores vacíos. /// let result_with_empty = join!("Hello", "", "World"); -/// assert_eq!(result_with_empty, String::from("HelloWorld")); +/// assert_eq!(result_with_empty, "HelloWorld".to_string()); /// /// // Un único fragmento devuelve el mismo valor. /// let single_result = join!("Hello"); -/// assert_eq!(single_result, String::from("Hello")); +/// assert_eq!(single_result, "Hello".to_string()); /// ``` #[macro_export] macro_rules! join { @@ -141,11 +141,11 @@ macro_rules! join { /// /// // Concatena los fragmentos no vacíos con un espacio como separador. /// let result_with_separator = join_opt!(["Hello", "", "World"]; " "); -/// assert_eq!(result_with_separator, Some(String::from("Hello World"))); +/// assert_eq!(result_with_separator, Some("Hello World".to_string())); /// /// // Concatena los fragmentos no vacíos sin un separador. /// let result_without_separator = join_opt!(["Hello", "", "World"]); -/// assert_eq!(result_without_separator, Some(String::from("HelloWorld"))); +/// assert_eq!(result_without_separator, Some("HelloWorld".to_string())); /// /// // Devuelve `None` si todos los fragmentos están vacíos. /// let result_empty = join_opt!(["", "", ""]); @@ -185,19 +185,19 @@ macro_rules! join_opt { /// /// // Concatena los dos fragmentos cuando ambos no están vacíos. /// let result = join_pair!(first, separator, second); -/// assert_eq!(result, String::from("Hello-World")); +/// assert_eq!(result, "Hello-World".to_string()); /// /// // Si el primer fragmento está vacío, devuelve el segundo. /// let result_empty_first = join_pair!("", separator, second); -/// assert_eq!(result_empty_first, String::from("World")); +/// assert_eq!(result_empty_first, "World".to_string()); /// /// // Si el segundo fragmento está vacío, devuelve el primero. /// let result_empty_second = join_pair!(first, separator, ""); -/// assert_eq!(result_empty_second, String::from("Hello")); +/// assert_eq!(result_empty_second, "Hello".to_string()); /// /// // Si ambos fragmentos están vacíos, devuelve una cadena vacía. /// let result_both_empty = join_pair!("", separator, ""); -/// assert_eq!(result_both_empty, String::from("")); +/// assert_eq!(result_both_empty, "".to_string()); /// ``` #[macro_export] macro_rules! join_pair { @@ -224,11 +224,11 @@ macro_rules! join_pair { /// /// // Concatena los fragmentos. /// let result = join_strict!(["Hello", "World"]); -/// assert_eq!(result, Some(String::from("HelloWorld"))); +/// assert_eq!(result, Some("HelloWorld".to_string())); /// /// // Concatena los fragmentos con un separador. /// let result_with_separator = join_strict!(["Hello", "World"]; " "); -/// assert_eq!(result_with_separator, Some(String::from("Hello World"))); +/// assert_eq!(result_with_separator, Some("Hello World".to_string())); /// /// // Devuelve `None` si alguno de los fragmentos está vacío. /// let result_with_empty = join_strict!(["Hello", "", "World"]); diff --git a/tests/component_html.rs b/tests/component_html.rs index bd7f3c0..851315a 100644 --- a/tests/component_html.rs +++ b/tests/component_html.rs @@ -17,7 +17,7 @@ async fn component_html_renders_static_markup() { #[pagetop::test] async fn component_html_renders_using_context_param() { - let mut cx = Context::new(None).with_param("username", String::from("Alice")); + let mut cx = Context::new(None).with_param("username", "Alice".to_string()); let component = Html::with(|cx| { let name = cx.param::("username").cloned().unwrap_or_default(); diff --git a/tests/component_poweredby.rs b/tests/component_poweredby.rs index 9f8e822..e4551d1 100644 --- a/tests/component_poweredby.rs +++ b/tests/component_poweredby.rs @@ -8,10 +8,10 @@ async fn poweredby_default_shows_only_pagetop_recognition() { let html = render_component(&p); // Debe mostrar el bloque de reconocimiento a PageTop. - assert!(html.contains("poweredby__pagetop")); + assert!(html.as_str().contains("poweredby__pagetop")); // Y NO debe mostrar el bloque de copyright. - assert!(!html.contains("poweredby__copyright")); + assert!(!html.as_str().contains("poweredby__copyright")); } #[pagetop::test] @@ -22,17 +22,20 @@ async fn poweredby_new_includes_current_year_and_app_name() { let html = render_component(&p); let year = Utc::now().format("%Y").to_string(); - assert!(html.contains(&year), "HTML should include the current year"); + assert!( + html.as_str().contains(&year), + "HTML should include the current year" + ); // El nombre de la app proviene de `global::SETTINGS.app.name`. let app_name = &global::SETTINGS.app.name; assert!( - html.contains(app_name), + html.as_str().contains(app_name), "HTML should include the application name" ); // Debe existir el span de copyright. - assert!(html.contains("poweredby__copyright")); + assert!(html.as_str().contains("poweredby__copyright")); } #[pagetop::test] @@ -43,8 +46,8 @@ async fn poweredby_with_copyright_overrides_text() { let p = PoweredBy::default().with_copyright(Some(custom)); let html = render_component(&p); - assert!(html.contains(custom)); - assert!(html.contains("poweredby__copyright")); + assert!(html.as_str().contains(custom)); + assert!(html.as_str().contains("poweredby__copyright")); } #[pagetop::test] @@ -54,9 +57,9 @@ async fn poweredby_with_copyright_none_hides_text() { let p = PoweredBy::new().with_copyright(None::); let html = render_component(&p); - assert!(!html.contains("poweredby__copyright")); + assert!(!html.as_str().contains("poweredby__copyright")); // El reconocimiento a PageTop siempre debe aparecer. - assert!(html.contains("poweredby__pagetop")); + assert!(html.as_str().contains("poweredby__pagetop")); } #[pagetop::test] @@ -67,7 +70,7 @@ async fn poweredby_link_points_to_crates_io() { let html = render_component(&p); assert!( - html.contains("https://pagetop.cillero.es"), + html.as_str().contains("https://pagetop.cillero.es"), "Link should point to pagetop.cillero.es" ); } @@ -89,12 +92,8 @@ async fn poweredby_getter_reflects_internal_state() { // HELPERS ***************************************************************************************** -fn render(x: &impl Render) -> String { - x.render().into_string() -} - -fn render_component(c: &C) -> String { +fn render_component(c: &C) -> Markup { let mut cx = Context::default(); let pm = c.prepare_component(&mut cx); - render(&pm) + pm.render() } diff --git a/tests/html.rs b/tests/html.rs index 1499c70..ae4517b 100644 --- a/tests/html.rs +++ b/tests/html.rs @@ -2,19 +2,19 @@ use pagetop::prelude::*; #[pagetop::test] async fn prepare_markup_render_none_is_empty_string() { - assert_eq!(render(&PrepareMarkup::None), ""); + assert_eq!(PrepareMarkup::None.render().as_str(), ""); } #[pagetop::test] async fn prepare_markup_render_escaped_escapes_html_and_ampersands() { - let pm = PrepareMarkup::Escaped(String::from("& \" ' ")); - assert_eq!(render(&pm), "<b>& " ' </b>"); + let pm = PrepareMarkup::Escaped("& \" ' ".to_string()); + assert_eq!(pm.render().as_str(), "<b>& " ' </b>"); } #[pagetop::test] async fn prepare_markup_render_raw_is_inserted_verbatim() { - let pm = PrepareMarkup::Raw(String::from("bold")); - assert_eq!(render(&pm), "bold"); + let pm = PrepareMarkup::Raw("bold".to_string()); + assert_eq!(pm.render().as_str(), "bold"); } #[pagetop::test] @@ -24,7 +24,7 @@ async fn prepare_markup_render_with_keeps_structure() { p { "This is a paragraph." } }); assert_eq!( - render(&pm), + pm.render().as_str(), "

Sample title

This is a paragraph.

" ); } @@ -33,7 +33,7 @@ async fn prepare_markup_render_with_keeps_structure() { async fn prepare_markup_does_not_double_escape_when_wrapped_in_html_macro() { // Escaped: dentro de `html!` no debe volver a escaparse. let escaped = PrepareMarkup::Escaped("x".into()); - let wrapped_escaped = html! { div { (escaped) } }; + let wrapped_escaped = html! { div { (escaped.render()) } }; assert_eq!( wrapped_escaped.into_string(), "
<i>x</i>
" @@ -41,12 +41,12 @@ async fn prepare_markup_does_not_double_escape_when_wrapped_in_html_macro() { // Raw: tampoco debe escaparse al integrarlo. let raw = PrepareMarkup::Raw("x".into()); - let wrapped_raw = html! { div { (raw) } }; + let wrapped_raw = html! { div { (raw.render()) } }; assert_eq!(wrapped_raw.into_string(), "
x
"); // With: debe incrustar el Markup tal cual. let with = PrepareMarkup::With(html! { span.title { "ok" } }); - let wrapped_with = html! { div { (with) } }; + let wrapped_with = html! { div { (with.render()) } }; assert_eq!( wrapped_with.into_string(), "
ok
" @@ -57,11 +57,14 @@ async fn prepare_markup_does_not_double_escape_when_wrapped_in_html_macro() { async fn prepare_markup_unicode_is_preserved() { // Texto con acentos y emojis debe conservarse (salvo el escape HTML de signos). let esc = PrepareMarkup::Escaped("Hello, tomorrow coffee ☕ & donuts!".into()); - assert_eq!(render(&esc), "Hello, tomorrow coffee ☕ & donuts!"); + assert_eq!( + esc.render().as_str(), + "Hello, tomorrow coffee ☕ & donuts!" + ); // Raw debe pasar íntegro. let raw = PrepareMarkup::Raw("Title — section © 2025".into()); - assert_eq!(render(&raw), "Title — section © 2025"); + assert_eq!(raw.render().as_str(), "Title — section © 2025"); } #[pagetop::test] @@ -69,11 +72,11 @@ async fn prepare_markup_is_empty_semantics() { assert!(PrepareMarkup::None.is_empty()); assert!(PrepareMarkup::Escaped(String::new()).is_empty()); - assert!(PrepareMarkup::Escaped(String::from("")).is_empty()); - assert!(!PrepareMarkup::Escaped(String::from("x")).is_empty()); + assert!(PrepareMarkup::Escaped("".to_string()).is_empty()); + assert!(!PrepareMarkup::Escaped("x".to_string()).is_empty()); assert!(PrepareMarkup::Raw(String::new()).is_empty()); - assert!(PrepareMarkup::Raw(String::from("")).is_empty()); + assert!(PrepareMarkup::Raw("".to_string()).is_empty()); assert!(!PrepareMarkup::Raw("a".into()).is_empty()); assert!(PrepareMarkup::With(html! {}).is_empty()); @@ -94,17 +97,12 @@ async fn prepare_markup_equivalence_between_render_and_inline_in_html_macro() { ]; for pm in cases { - let rendered = render(&pm); - let in_macro = html! { (pm) }.into_string(); + let rendered = pm.render(); + let in_macro = html! { (rendered) }.into_string(); assert_eq!( - rendered, in_macro, + rendered.as_str(), + in_macro, "The output of Render and (pm) inside html! must match" ); } } - -// HELPERS ***************************************************************************************** - -fn render(x: &impl Render) -> String { - x.render().into_string() -}