Compare commits

..

No commits in common. "e3ca6079ffc76d226be2f0afae641e8d5190cdfc" and "2a4d6a78909a66280d0695dd072a5f3616ab65da" have entirely different histories.

21 changed files with 175 additions and 234 deletions

View file

@ -84,7 +84,7 @@ impl Application {
if let Some((Width(term_width), _)) = terminal_size() { if let Some((Width(term_width), _)) = terminal_size() {
if term_width >= 80 { if term_width >= 80 {
let maxlen: usize = ((term_width / 10) - 2).into(); let maxlen: usize = ((term_width / 10) - 2).into();
let mut app = app_name.substring(0, maxlen).to_string(); let mut app = app_name.substring(0, maxlen).to_owned();
if app_name.len() > maxlen { if app_name.len() > maxlen {
app = format!("{app}..."); app = format!("{app}...");
} }

View file

@ -25,7 +25,7 @@ use crate::prelude::*;
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// let component = Html::with(|cx| { /// let component = Html::with(|cx| {
/// let user = cx.param::<String>("username").cloned().unwrap_or("visitor".to_string()); /// let user = cx.param::<String>("username").cloned().unwrap_or(String::from("visitor"));
/// html! { /// html! {
/// h1 { "Hello, " (user) } /// h1 { "Hello, " (user) }
/// } /// }

View file

@ -1,6 +1,6 @@
use crate::base::action; use crate::base::action;
use crate::core::{AnyInfo, TypeInfo}; use crate::core::{AnyInfo, TypeInfo};
use crate::html::{html, Context, Markup, PrepareMarkup}; use crate::html::{html, Context, Markup, PrepareMarkup, Render};
/// Define la función de renderizado para todos los componentes. /// Define la función de renderizado para todos los componentes.
/// ///

View file

@ -7,7 +7,7 @@
//! Un tema **declara las regiones** (*cabecera*, *barra lateral*, *pie*, etc.) que estarán //! 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, //! disponibles para colocar contenido. Los temas son responsables últimos de los estilos,
//! tipografías, espaciados y cualquier otro detalle visual o de comportamiento (comoanimaciones, //! tipografías, espaciados y cualquier otro detalle visual o de comportamiento (comoanimaciones,
//! scripts de interfaz, etc.). //! *scripts* de interfaz, etc.).
//! //!
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension); por //! 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; //! lo que se instancian, declaran sus dependencias y se inician igual que el resto de extensiones;

View file

@ -36,7 +36,7 @@ impl Default for Region {
fn default() -> Self { fn default() -> Self {
Self { Self {
key: REGION_CONTENT, key: REGION_CONTENT,
name: REGION_CONTENT.to_string(), name: String::from(REGION_CONTENT),
} }
} }
} }

View file

@ -1,7 +1,7 @@
//! HTML en código. //! HTML en código.
mod maud; mod maud;
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE}; pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE};
// HTML DOCUMENT ASSETS **************************************************************************** // HTML DOCUMENT ASSETS ****************************************************************************
@ -71,11 +71,11 @@ pub type OptionComponent<C: core::component::Component> = core::component::Typed
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// // Texto normal, se escapa automáticamente para evitar inyección de HTML. /// // Texto normal, se escapa automáticamente para evitar inyección de HTML.
/// let fragment = PrepareMarkup::Escaped("Hola <b>mundo</b>".to_string()); /// let fragment = PrepareMarkup::Escaped(String::from("Hola <b>mundo</b>"));
/// assert_eq!(fragment.render().into_string(), "Hola &lt;b&gt;mundo&lt;/b&gt;"); /// assert_eq!(fragment.render().into_string(), "Hola &lt;b&gt;mundo&lt;/b&gt;");
/// ///
/// // HTML literal, se inserta directamente, sin escapado adicional. /// // HTML literal, se inserta directamente, sin escapado adicional.
/// let raw_html = PrepareMarkup::Raw("<b>negrita</b>".to_string()); /// let raw_html = PrepareMarkup::Raw(String::from("<b>negrita</b>"));
/// assert_eq!(raw_html.render().into_string(), "<b>negrita</b>"); /// assert_eq!(raw_html.render().into_string(), "<b>negrita</b>");
/// ///
/// // Fragmento ya preparado con la macro `html!`. /// // Fragmento ya preparado con la macro `html!`.
@ -119,9 +119,11 @@ impl PrepareMarkup {
PrepareMarkup::With(markup) => markup.is_empty(), PrepareMarkup::With(markup) => markup.is_empty(),
} }
} }
}
impl Render for PrepareMarkup {
/// Integra el renderizado fácilmente en la macro [`html!`]. /// Integra el renderizado fácilmente en la macro [`html!`].
pub fn render(&self) -> Markup { fn render(&self) -> Markup {
match self { match self {
PrepareMarkup::None => html! {}, PrepareMarkup::None => html! {},
PrepareMarkup::Escaped(text) => html! { (text) }, PrepareMarkup::Escaped(text) => html! { (text) },

View file

@ -2,10 +2,10 @@ pub mod favicon;
pub mod javascript; pub mod javascript;
pub mod stylesheet; pub mod stylesheet;
use crate::html::{html, Context, Markup}; use crate::html::{html, Markup, Render};
use crate::{AutoDefault, Weight}; 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). /// estilos [`StyleSheet`](crate::html::StyleSheet).
/// ///
/// Estos recursos se incluyen en los conjuntos de recursos ([`Assets`]) que suelen renderizarse en /// Estos recursos se incluyen en los conjuntos de recursos ([`Assets`]) que suelen renderizarse en
@ -13,15 +13,12 @@ use crate::{AutoDefault, Weight};
/// ///
/// Cada recurso se identifica por un **nombre único** ([`Asset::name()`]), usado como clave; y un /// 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. /// **peso** ([`Asset::weight()`]), que determina su orden relativo de renderizado.
pub trait Asset { pub trait Asset: Render {
/// Devuelve el nombre del recurso, utilizado como clave única. /// Devuelve el nombre del recurso, utilizado como clave única.
fn name(&self) -> &str; fn name(&self) -> &str;
/// Devuelve el peso del recurso, usado para ordenar el renderizado de menor a mayor peso. /// Devuelve el peso del recurso, usado para ordenar el renderizado de menor a mayor peso.
fn weight(&self) -> Weight; 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 /// Gestión común para conjuntos de recursos como [`JavaScript`](crate::html::JavaScript) y
@ -80,13 +77,16 @@ impl<T: Asset> Assets<T> {
false false
} }
} }
}
pub fn render(&self, cx: &mut Context) -> Markup { impl<T: Asset> Render for Assets<T> {
fn render(&self) -> Markup {
let mut assets = self.0.iter().collect::<Vec<_>>(); let mut assets = self.0.iter().collect::<Vec<_>>();
assets.sort_by_key(|a| a.weight()); assets.sort_by_key(|a| a.weight());
html! { html! {
@for a in assets { @for a in assets {
(a.render(cx)) (a)
} }
} }
} }

View file

@ -1,4 +1,4 @@
use crate::html::{html, Context, Markup}; use crate::html::{html, Markup, Render};
use crate::AutoDefault; use crate::AutoDefault;
/// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio. /// 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<String>, icon_color: Option<String>,
) -> Self { ) -> Self {
let icon_type = match icon_source.rfind('.') { let icon_type = match icon_source.rfind('.') {
Some(i) => match icon_source[i..].to_string().to_lowercase().as_str() { Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() {
".avif" => Some("image/avif"), ".avif" => Some("image/avif"),
".gif" => Some("image/gif"), ".gif" => Some("image/gif"),
".ico" => Some("image/x-icon"), ".ico" => Some("image/x-icon"),
@ -151,12 +151,10 @@ impl Favicon {
}); });
self self
} }
}
/// Renderiza el **Favicon** completo con todas las etiquetas declaradas. impl Render for Favicon {
/// fn render(&self) -> Markup {
/// 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! { html! {
@for item in &self.0 { @for item in &self.0 {
(item) (item)

View file

@ -1,45 +1,35 @@
use crate::html::assets::Asset; use crate::html::assets::Asset;
use crate::html::{html, Context, Markup, PreEscaped}; use crate::html::{html, Markup, Render};
use crate::{join, join_pair, AutoDefault, Weight}; use crate::{join, join_pair, AutoDefault, Weight};
// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador. // 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 // 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. // script.
// //
// - [`From`] Carga estándar con la etiqueta `<script src="...">`. // - [`From`] Carga el script de forma estándar con la etiqueta `<script src="...">`.
// - [`Defer`] Igual que [`From`], pero con el atributo `defer`, descarga en paralelo y se // - [`Defer`] Igual que [`From`], pero con el atributo `defer`.
// ejecuta tras el análisis del documento HTML, respetando el orden de // - [`Async`] Igual que [`From`], pero con el atributo `async`.
// aparición. // - [`Inline`] Inserta el código directamente en la etiqueta `<script>`.
// - [`Async`] Igual que [`From`], pero con el atributo `async`, descarga en paralelo y se // - [`OnLoad`] Inserta el código JavaScript y lo ejecuta tras el evento `DOMContentLoaded`.
// 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)] #[derive(AutoDefault)]
enum Source { enum Source {
#[default] #[default]
From(String), From(String),
Defer(String), Defer(String),
Async(String), Async(String),
// `name`, `closure(Context) -> String`. Inline(String, String),
Inline(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>), OnLoad(String, String),
// `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>),
} }
/// Define un recurso **JavaScript** para incluir en un documento HTML. /// Define un recurso **JavaScript** para incluir en un documento HTML.
/// ///
/// Este tipo permite añadir scripts externos o embebidos con distintas estrategias de carga /// Este tipo permite añadir *scripts* externos o embebidos con distintas estrategias de carga
/// (`defer`, `async`, *inline*, etc.) y [pesos](crate::Weight) para controlar el orden de inserción /// (`defer`, `async`, *inline*, etc.) y [pesos](crate::Weight) para controlar el orden de inserción
/// en el documento. /// en el documento.
/// ///
/// > **Nota** /// > **Nota**
/// > Los archivos de los scripts deben estar disponibles en el servidor web de la aplicación. /// > Los archivos de los *scripts* deben estar disponibles en el servidor web de la aplicación.
/// > Pueden servirse usando [`static_files_service!`](crate::static_files_service). /// > Pueden servirse usando [`static_files_service!`](crate::static_files_service).
/// ///
/// # Ejemplo /// # Ejemplo
@ -47,37 +37,23 @@ enum Source {
/// ```rust /// ```rust
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// // Script externo con carga diferida, versión de caché y prioridad en el renderizado. /// // Script externo con carga diferida, versión para control de caché y prioriza el renderizado.
/// let script = JavaScript::defer("/assets/js/app.js") /// let script = JavaScript::defer("/assets/js/app.js")
/// .with_version("1.2.3") /// .with_version("1.2.3")
/// .with_weight(-10); /// .with_weight(-10);
/// ///
/// // Script embebido que se ejecuta tras la carga del documento. /// // Script embebido que se ejecuta tras la carga del documento.
/// let script = JavaScript::on_load("init_tooltips", |_| r#" /// let script = JavaScript::on_load("init_tooltips", r#"
/// const tooltips = document.querySelectorAll('[data-tooltip]'); /// const tooltips = document.querySelectorAll('[data-tooltip]');
/// for (const el of tooltips) { /// for (const el of tooltips) {
/// el.addEventListener('mouseenter', showTooltip); /// el.addEventListener('mouseenter', showTooltip);
/// } /// }
/// "#.to_string()); /// "#);
///
/// // Script embebido con manejador asíncrono (`async`) que puede usar `await`.
/// let mut cx = Context::new(None).with_param("user_id", 7u32);
///
/// let js = JavaScript::on_load_async("hydrate", |cx| {
/// // Ejemplo: lectura de un parámetro del contexto para inyectarlo en el código.
/// let uid: u32 = cx.param_or_default("user_id");
/// format!(r#"
/// const USER_ID = {};
/// await Promise.resolve(USER_ID);
/// // Aquí se podría hidratar la interfaz o cargar módulos dinámicos:
/// // await import('/assets/js/hydrate.js');
/// "#, uid)
/// });
/// ``` /// ```
#[rustfmt::skip] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct JavaScript { pub struct JavaScript {
source : Source, // Fuente y estrategia de carga del script. source : Source, // Fuente y modo de carga del script.
version: String, // Versión del recurso para la caché del navegador. version: String, // Versión del recurso para la caché del navegador.
weight : Weight, // Peso que determina el orden. weight : Weight, // Peso que determina el orden.
} }
@ -94,11 +70,11 @@ impl JavaScript {
} }
} }
/// Crea un **script externo** con el atributo `defer`, que se descarga en paralelo y se ejecuta /// Crea un **script externo** con el atributo `defer`, que se carga en segundo plano y se
/// tras analizar completamente el documento HTML, **respetando el orden** de inserción. /// ejecuta tras analizar completamente el documento HTML.
/// ///
/// Equivale a `<script src="..." defer>`. Suele ser la opción recomendada para scripts no /// Equivale a `<script src="..." defer>`. Útil para mantener el orden de ejecución y evitar
/// críticos. /// bloquear el análisis del documento HTML.
pub fn defer(path: impl Into<String>) -> Self { pub fn defer(path: impl Into<String>) -> Self {
JavaScript { JavaScript {
source: Source::Defer(path.into()), source: Source::Defer(path.into()),
@ -106,10 +82,11 @@ impl JavaScript {
} }
} }
/// Crea un **script externo** con el atributo `async`, que se descarga en paralelo y se ejecuta /// Crea un **script externo** con el atributo `async`, que se carga y ejecuta de forma
/// tan pronto como esté disponible. /// asíncrona tan pronto como esté disponible.
/// ///
/// Equivale a `<script src="..." async>`. **No garantiza** el orden relativo con otros scripts. /// Equivale a `<script src="..." async>`. La ejecución puede producirse fuera de orden respecto
/// a otros *scripts*.
pub fn asynchronous(path: impl Into<String>) -> Self { pub fn asynchronous(path: impl Into<String>) -> Self {
JavaScript { JavaScript {
source: Source::Async(path.into()), source: Source::Async(path.into()),
@ -120,68 +97,37 @@ impl JavaScript {
/// Crea un **script embebido** directamente en el documento HTML. /// Crea un **script embebido** directamente en el documento HTML.
/// ///
/// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del /// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del
/// script. /// *script*.
/// pub fn inline(name: impl Into<String>, script: impl Into<String>) -> Self {
/// 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
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
{
JavaScript { JavaScript {
source: Source::Inline(name.into(), Box::new(f)), source: Source::Inline(name.into(), script.into()),
..Default::default() ..Default::default()
} }
} }
/// Crea un **script embebido** que se ejecuta cuando **el DOM está listo**. /// Crea un **script embebido** que se ejecuta automáticamente al terminar de cargarse el
/// documento HTML.
/// ///
/// El código se envuelve en un `addEventListener('DOMContentLoaded',function(){...})` que lo /// El código se envuelve automáticamente en un `addEventListener('DOMContentLoaded', ...)`. El
/// ejecuta tras analizar el documento HTML, **no** espera imágenes ni otros recursos externos. /// parámetro `name` se usa como identificador interno del *script*.
/// Útil para inicializaciones que no dependen de `await`. El parámetro `name` se usa como pub fn on_load(name: impl Into<String>, script: impl Into<String>) -> Self {
/// 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<F>(name: impl Into<String>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
{
JavaScript { JavaScript {
source: Source::OnLoad(name.into(), Box::new(f)), source: Source::OnLoad(name.into(), script.into()),
..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<F>(name: impl Into<String>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
{
JavaScript {
source: Source::OnLoadAsync(name.into(), Box::new(f)),
..Default::default() ..Default::default()
} }
} }
// JavaScript BUILDER ************************************************************************** // 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<String>) -> Self { pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = version.into(); self.version = version.into();
self 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 /// Los recursos se renderizan de menor a mayor peso. Por defecto es `0`, que respeta el orden
/// de creación. /// de creación.
@ -194,7 +140,7 @@ impl JavaScript {
impl Asset for JavaScript { impl Asset for JavaScript {
/// Devuelve el nombre del recurso, utilizado como clave única. /// 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 { fn name(&self) -> &str {
match &self.source { match &self.source {
Source::From(path) => path, Source::From(path) => path,
@ -202,15 +148,16 @@ impl Asset for JavaScript {
Source::Async(path) => path, Source::Async(path) => path,
Source::Inline(name, _) => name, Source::Inline(name, _) => name,
Source::OnLoad(name, _) => name, Source::OnLoad(name, _) => name,
Source::OnLoadAsync(name, _) => name,
} }
} }
fn weight(&self) -> Weight { fn weight(&self) -> Weight {
self.weight self.weight
} }
}
fn render(&self, cx: &mut Context) -> Markup { impl Render for JavaScript {
fn render(&self) -> Markup {
match &self.source { match &self.source {
Source::From(path) => html! { Source::From(path) => html! {
script src=(join_pair!(path, "?v=", self.version.as_str())) {}; script src=(join_pair!(path, "?v=", self.version.as_str())) {};
@ -221,15 +168,12 @@ impl Asset for JavaScript {
Source::Async(path) => html! { Source::Async(path) => html! {
script src=(join_pair!(path, "?v=", self.version.as_str())) async {}; script src=(join_pair!(path, "?v=", self.version.as_str())) async {};
}, },
Source::Inline(_, f) => html! { Source::Inline(_, code) => html! {
script { (PreEscaped((f)(cx))) }; script { (code) };
}, },
Source::OnLoad(_, f) => html! { script { (PreEscaped(join!( Source::OnLoad(_, code) => html! { (join!(
"document.addEventListener(\"DOMContentLoaded\",function(){", (f)(cx), "});" "document.addEventListener('DOMContentLoaded',function(){", code, "});"
))) } }, )) },
Source::OnLoadAsync(_, f) => html! { script { (PreEscaped(join!(
"document.addEventListener(\"DOMContentLoaded\",async()=>{", (f)(cx), "});"
))) } },
} }
} }
} }

View file

@ -1,5 +1,5 @@
use crate::html::assets::Asset; use crate::html::assets::Asset;
use crate::html::{html, Context, Markup, PreEscaped}; use crate::html::{html, Markup, PreEscaped, Render};
use crate::{join_pair, AutoDefault, Weight}; use crate::{join_pair, AutoDefault, Weight};
// Define el origen del recurso CSS y cómo se incluye en el documento. // Define el origen del recurso CSS y cómo se incluye en el documento.
@ -14,8 +14,7 @@ use crate::{join_pair, AutoDefault, Weight};
enum Source { enum Source {
#[default] #[default]
From(String), From(String),
// `name`, `closure(Context) -> String`. Inline(String, String),
Inline(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
} }
/// Define el medio objetivo para la hoja de estilos. /// Define el medio objetivo para la hoja de estilos.
@ -35,7 +34,7 @@ pub enum TargetMedia {
Speech, Speech,
} }
/// Devuelve el valor para el atributo `media` (`Some(...)`) o `None` para `Default`. /// Devuelve el texto asociado al punto de interrupción usado por Bootstrap.
#[rustfmt::skip] #[rustfmt::skip]
impl TargetMedia { impl TargetMedia {
fn as_str_opt(&self) -> Option<&str> { fn as_str_opt(&self) -> Option<&str> {
@ -70,12 +69,12 @@ impl TargetMedia {
/// .with_weight(-10); /// .with_weight(-10);
/// ///
/// // Crea una hoja de estilos embebida en el documento HTML. /// // 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 { /// body {
/// background-color: #f5f5f5; /// background-color: #f5f5f5;
/// font-family: 'Segoe UI', sans-serif; /// font-family: 'Segoe UI', sans-serif;
/// } /// }
/// "#.to_string()); /// "#);
/// ``` /// ```
#[rustfmt::skip] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
@ -101,14 +100,9 @@ impl StyleSheet {
/// ///
/// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del /// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del
/// recurso. /// recurso.
/// pub fn inline(name: impl Into<String>, styles: impl Into<String>) -> Self {
/// 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
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
{
StyleSheet { StyleSheet {
source: Source::Inline(name.into(), Box::new(f)), source: Source::Inline(name.into(), styles.into()),
..Default::default() ..Default::default()
} }
} }
@ -139,9 +133,9 @@ impl StyleSheet {
/// Según el argumento `media`: /// Según el argumento `media`:
/// ///
/// - `TargetMedia::Default` - Se aplica en todos los casos (medio por defecto). /// - `TargetMedia::Default` - Se aplica en todos los casos (medio por defecto).
/// - `TargetMedia::Print` - Se aplica cuando el documento se imprime. /// - `TargetMedia::Print` - Se aplican cuando el documento se imprime.
/// - `TargetMedia::Screen` - Se aplica en pantallas. /// - `TargetMedia::Screen` - Se aplican en pantallas.
/// - `TargetMedia::Speech` - Se aplica en dispositivos que convierten el texto a voz. /// - `TargetMedia::Speech` - Se aplican en dispositivos que convierten el texto a voz.
pub fn for_media(mut self, media: TargetMedia) -> Self { pub fn for_media(mut self, media: TargetMedia) -> Self {
self.media = media; self.media = media;
self self
@ -162,8 +156,10 @@ impl Asset for StyleSheet {
fn weight(&self) -> Weight { fn weight(&self) -> Weight {
self.weight self.weight
} }
}
fn render(&self, cx: &mut Context) -> Markup { impl Render for StyleSheet {
fn render(&self) -> Markup {
match &self.source { match &self.source {
Source::From(path) => html! { Source::From(path) => html! {
link link
@ -171,8 +167,8 @@ impl Asset for StyleSheet {
href=(join_pair!(path, "?v=", self.version.as_str())) href=(join_pair!(path, "?v=", self.version.as_str()))
media=[self.media.as_str_opt()]; media=[self.media.as_str_opt()];
}, },
Source::Inline(_, f) => html! { Source::Inline(_, code) => html! {
style { (PreEscaped((f)(cx))) }; style { (PreEscaped(code)) };
}, },
} }
} }

View file

@ -37,7 +37,7 @@ pub enum ClassesOp {
/// .with_value(ClassesOp::Add, "Active") /// .with_value(ClassesOp::Add, "Active")
/// .with_value(ClassesOp::Remove, "btn-primary"); /// .with_value(ClassesOp::Remove, "btn-primary");
/// ///
/// assert_eq!(classes.get(), Some("btn active".to_string())); /// assert_eq!(classes.get(), Some(String::from("btn active")));
/// assert!(classes.contains("active")); /// assert!(classes.contains("active"));
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]

View file

@ -17,13 +17,13 @@ use crate::{builder_fn, AutoDefault};
/// // Español disponible. /// // Español disponible.
/// assert_eq!( /// assert_eq!(
/// hello.lookup(&LangMatch::resolve("es-ES")), /// hello.lookup(&LangMatch::resolve("es-ES")),
/// Some("¡Hola mundo!".to_string()) /// Some(String::from("¡Hola mundo!"))
/// ); /// );
/// ///
/// // Japonés no disponible, traduce al idioma de respaldo ("en-US"). /// // Japonés no disponible, traduce al idioma de respaldo ("en-US").
/// assert_eq!( /// assert_eq!(
/// hello.lookup(&LangMatch::resolve("ja-JP")), /// hello.lookup(&LangMatch::resolve("ja-JP")),
/// Some("Hello world!".to_string()) /// Some(String::from("Hello world!"))
/// ); /// );
/// ///
/// // Uso típico en un atributo: /// // Uso típico en un atributo:

View file

@ -36,7 +36,7 @@ impl AttrValue {
self.0 = if value.is_empty() { self.0 = if value.is_empty() {
None None
} else { } else {
Some(value.to_string()) Some(value.to_owned())
}; };
self self
} }

View file

@ -25,9 +25,9 @@ pub enum AssetsOp {
RemoveStyleSheet(&'static str), RemoveStyleSheet(&'static str),
// JavaScripts. // JavaScripts.
/// Añade un script JavaScript al documento. /// Añade un *script* JavaScript al documento.
AddJavaScript(JavaScript), AddJavaScript(JavaScript),
/// Elimina un script por su ruta o identificador. /// Elimina un *script* por su ruta o identificador.
RemoveJavaScript(&'static str), RemoveJavaScript(&'static str),
} }
@ -55,7 +55,7 @@ pub enum ErrorParam {
/// - Almacenar la **solicitud HTTP** de origen. /// - Almacenar la **solicitud HTTP** de origen.
/// - Seleccionar **tema** y **composición** (*layout*) de renderizado. /// - Seleccionar **tema** y **composición** (*layout*) de renderizado.
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo /// - 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. /// - Leer y mantener **parámetros dinámicos tipados** de contexto.
/// - Generar **identificadores únicos** por tipo de componente. /// - 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. /// Devuelve las hojas de estilo de los recursos del contexto.
fn stylesheets(&self) -> &Assets<StyleSheet>; fn stylesheets(&self) -> &Assets<StyleSheet>;
/// Devuelve los scripts JavaScript de los recursos del contexto. /// Devuelve los *scripts* JavaScript de los recursos del contexto.
fn javascripts(&self) -> &Assets<JavaScript>; fn javascripts(&self) -> &Assets<JavaScript>;
// Contextual HELPERS ************************************************************************** // Contextual HELPERS **************************************************************************
@ -155,7 +155,7 @@ pub trait Contextual: LangId {
/// ///
/// Extiende [`Contextual`] con métodos para **instanciar** y configurar un nuevo contexto, /// Extiende [`Contextual`] con métodos para **instanciar** y configurar un nuevo contexto,
/// **renderizar los recursos** del documento (incluyendo el [`Favicon`], las hojas de estilo /// **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. /// tipados** con nuevos métodos.
/// ///
/// # Ejemplos /// # Ejemplos
@ -258,29 +258,14 @@ impl Context {
// Context RENDER ****************************************************************************** // Context RENDER ******************************************************************************
/// Renderiza los recursos del contexto. /// Renderiza los recursos del contexto.
pub fn render_assets(&mut self) -> Markup { pub fn render_assets(&self) -> Markup {
use std::mem::take as mem_take; html! {
@if let Some(favicon) = &self.favicon {
// Extrae temporalmente los recursos. (favicon)
let favicon = mem_take(&mut self.favicon); // Deja valor por defecto (None) en self.
let stylesheets = mem_take(&mut self.stylesheets); // Assets<StyleSheet>::default() en self.
let javascripts = mem_take(&mut self.javascripts); // Assets<JavaScript>::default() en self.
// Renderiza con `&mut self` como contexto.
let markup = html! {
@if let Some(fi) = &favicon {
(fi.render(self))
} }
(stylesheets.render(self)) (self.stylesheets)
(javascripts.render(self)) (self.javascripts)
}; }
// Restaura los campos tal y como estaban.
self.favicon = favicon;
self.stylesheets = stylesheets;
self.javascripts = javascripts;
markup
} }
// Context PARAMS ****************************************************************************** // Context PARAMS ******************************************************************************
@ -300,7 +285,7 @@ impl Context {
/// ///
/// let cx = Context::new(None) /// let cx = Context::new(None)
/// .with_param("usuario_id", 42_i32) /// .with_param("usuario_id", 42_i32)
/// .with_param("titulo", "Hola".to_string()); /// .with_param("titulo", String::from("Hola"));
/// ///
/// let id: &i32 = cx.get_param("usuario_id").unwrap(); /// let id: &i32 = cx.get_param("usuario_id").unwrap();
/// let titulo: &String = cx.get_param("titulo").unwrap(); /// let titulo: &String = cx.get_param("titulo").unwrap();
@ -333,7 +318,7 @@ impl Context {
/// ///
/// let mut cx = Context::new(None) /// let mut cx = Context::new(None)
/// .with_param("contador", 7_i32) /// .with_param("contador", 7_i32)
/// .with_param("titulo", "Hola".to_string()); /// .with_param("titulo", String::from("Hola"));
/// ///
/// let n: i32 = cx.take_param("contador").unwrap(); /// let n: i32 = cx.take_param("contador").unwrap();
/// assert!(cx.get_param::<i32>("contador").is_err()); // ya no está /// assert!(cx.get_param::<i32>("contador").is_err()); // ya no está
@ -431,7 +416,7 @@ impl Contextual for Context {
/// ///
/// let cx = Context::new(None) /// let cx = Context::new(None)
/// .with_param("usuario_id", 42_i32) /// .with_param("usuario_id", 42_i32)
/// .with_param("titulo", "Hola".to_string()) /// .with_param("titulo", String::from("Hola"))
/// .with_param("flags", vec!["a", "b"]); /// .with_param("flags", vec!["a", "b"]);
/// ``` /// ```
#[builder_fn] #[builder_fn]
@ -499,7 +484,7 @@ impl Contextual for Context {
/// ```rust /// ```rust
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// let cx = Context::new(None).with_param("username", "Alice".to_string()); /// let cx = Context::new(None).with_param("username", String::from("Alice"));
/// ///
/// // Devuelve Some(&String) si existe y coincide el tipo. /// // Devuelve Some(&String) si existe y coincide el tipo.
/// assert_eq!(cx.param::<String>("username").map(|s| s.as_str()), Some("Alice")); /// assert_eq!(cx.param::<String>("username").map(|s| s.as_str()), Some("Alice"));
@ -548,7 +533,7 @@ impl Contextual for Context {
.replace(' ', "_") .replace(' ', "_")
.to_lowercase(); .to_lowercase();
let prefix = if prefix.is_empty() { let prefix = if prefix.is_empty() {
"prefix".to_string() "prefix".to_owned()
} else { } else {
prefix prefix
}; };

View file

@ -69,6 +69,23 @@ impl fmt::Write for Escaper<'_> {
/// `.render()` or `.render_to()`. Since the default definitions of /// `.render()` or `.render_to()`. Since the default definitions of
/// these methods call each other, not doing this will result in /// these methods call each other, not doing this will result in
/// infinite recursion. /// 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 { pub trait Render {
/// Renders `self` as a block of `Markup`. /// Renders `self` as a block of `Markup`.
fn render(&self) -> Markup { fn render(&self) -> Markup {
@ -221,10 +238,6 @@ impl Markup {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0.is_empty() self.0.is_empty()
} }
pub fn as_str(&self) -> &str {
self.0.as_str()
}
} }
impl<T: Into<String>> PreEscaped<T> { impl<T: Into<String>> PreEscaped<T> {

View file

@ -165,7 +165,7 @@ pub trait LangId {
/// ///
/// // Idioma no soportado. /// // Idioma no soportado.
/// let lang = LangMatch::resolve("ja-JP"); /// let lang = LangMatch::resolve("ja-JP");
/// assert_eq!(lang, LangMatch::Unsupported("ja-JP".to_string())); /// assert_eq!(lang, LangMatch::Unsupported(String::from("ja-JP")));
/// ``` /// ```
/// ///
/// Con la siguiente instrucción siempre se obtiene un [`LanguageIdentifier`] válido, ya sea porque /// 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. // En caso contrario, indica que el idioma no está soportado.
Self::Unsupported(language.to_string()) Self::Unsupported(String::from(language))
} }
/// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido. /// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido.

View file

@ -202,7 +202,7 @@ impl Page {
} }
/// Renderiza los recursos de la página. /// Renderiza los recursos de la página.
pub fn render_assets(&mut self) -> Markup { pub fn render_assets(&self) -> Markup {
self.context.render_assets() self.context.render_assets()
} }

View file

@ -110,15 +110,15 @@ macro_rules! hm {
/// ///
/// // Concatena todos los fragmentos directamente. /// // Concatena todos los fragmentos directamente.
/// let result = join!("Hello", " ", "World"); /// let result = join!("Hello", " ", "World");
/// assert_eq!(result, "Hello World".to_string()); /// assert_eq!(result, String::from("Hello World"));
/// ///
/// // También funciona con valores vacíos. /// // También funciona con valores vacíos.
/// let result_with_empty = join!("Hello", "", "World"); /// let result_with_empty = join!("Hello", "", "World");
/// assert_eq!(result_with_empty, "HelloWorld".to_string()); /// assert_eq!(result_with_empty, String::from("HelloWorld"));
/// ///
/// // Un único fragmento devuelve el mismo valor. /// // Un único fragmento devuelve el mismo valor.
/// let single_result = join!("Hello"); /// let single_result = join!("Hello");
/// assert_eq!(single_result, "Hello".to_string()); /// assert_eq!(single_result, String::from("Hello"));
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! join { macro_rules! join {
@ -141,11 +141,11 @@ macro_rules! join {
/// ///
/// // Concatena los fragmentos no vacíos con un espacio como separador. /// // Concatena los fragmentos no vacíos con un espacio como separador.
/// let result_with_separator = join_opt!(["Hello", "", "World"]; " "); /// let result_with_separator = join_opt!(["Hello", "", "World"]; " ");
/// assert_eq!(result_with_separator, Some("Hello World".to_string())); /// assert_eq!(result_with_separator, Some(String::from("Hello World")));
/// ///
/// // Concatena los fragmentos no vacíos sin un separador. /// // Concatena los fragmentos no vacíos sin un separador.
/// let result_without_separator = join_opt!(["Hello", "", "World"]); /// let result_without_separator = join_opt!(["Hello", "", "World"]);
/// assert_eq!(result_without_separator, Some("HelloWorld".to_string())); /// assert_eq!(result_without_separator, Some(String::from("HelloWorld")));
/// ///
/// // Devuelve `None` si todos los fragmentos están vacíos. /// // Devuelve `None` si todos los fragmentos están vacíos.
/// let result_empty = join_opt!(["", "", ""]); /// let result_empty = join_opt!(["", "", ""]);
@ -185,19 +185,19 @@ macro_rules! join_opt {
/// ///
/// // Concatena los dos fragmentos cuando ambos no están vacíos. /// // Concatena los dos fragmentos cuando ambos no están vacíos.
/// let result = join_pair!(first, separator, second); /// let result = join_pair!(first, separator, second);
/// assert_eq!(result, "Hello-World".to_string()); /// assert_eq!(result, String::from("Hello-World"));
/// ///
/// // Si el primer fragmento está vacío, devuelve el segundo. /// // Si el primer fragmento está vacío, devuelve el segundo.
/// let result_empty_first = join_pair!("", separator, second); /// let result_empty_first = join_pair!("", separator, second);
/// assert_eq!(result_empty_first, "World".to_string()); /// assert_eq!(result_empty_first, String::from("World"));
/// ///
/// // Si el segundo fragmento está vacío, devuelve el primero. /// // Si el segundo fragmento está vacío, devuelve el primero.
/// let result_empty_second = join_pair!(first, separator, ""); /// let result_empty_second = join_pair!(first, separator, "");
/// assert_eq!(result_empty_second, "Hello".to_string()); /// assert_eq!(result_empty_second, String::from("Hello"));
/// ///
/// // Si ambos fragmentos están vacíos, devuelve una cadena vacía. /// // Si ambos fragmentos están vacíos, devuelve una cadena vacía.
/// let result_both_empty = join_pair!("", separator, ""); /// let result_both_empty = join_pair!("", separator, "");
/// assert_eq!(result_both_empty, "".to_string()); /// assert_eq!(result_both_empty, String::from(""));
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! join_pair { macro_rules! join_pair {
@ -224,11 +224,11 @@ macro_rules! join_pair {
/// ///
/// // Concatena los fragmentos. /// // Concatena los fragmentos.
/// let result = join_strict!(["Hello", "World"]); /// let result = join_strict!(["Hello", "World"]);
/// assert_eq!(result, Some("HelloWorld".to_string())); /// assert_eq!(result, Some(String::from("HelloWorld")));
/// ///
/// // Concatena los fragmentos con un separador. /// // Concatena los fragmentos con un separador.
/// let result_with_separator = join_strict!(["Hello", "World"]; " "); /// let result_with_separator = join_strict!(["Hello", "World"]; " ");
/// assert_eq!(result_with_separator, Some("Hello World".to_string())); /// assert_eq!(result_with_separator, Some(String::from("Hello World")));
/// ///
/// // Devuelve `None` si alguno de los fragmentos está vacío. /// // Devuelve `None` si alguno de los fragmentos está vacío.
/// let result_with_empty = join_strict!(["Hello", "", "World"]); /// let result_with_empty = join_strict!(["Hello", "", "World"]);

View file

@ -17,7 +17,7 @@ async fn component_html_renders_static_markup() {
#[pagetop::test] #[pagetop::test]
async fn component_html_renders_using_context_param() { async fn component_html_renders_using_context_param() {
let mut cx = Context::new(None).with_param("username", "Alice".to_string()); let mut cx = Context::new(None).with_param("username", String::from("Alice"));
let component = Html::with(|cx| { let component = Html::with(|cx| {
let name = cx.param::<String>("username").cloned().unwrap_or_default(); let name = cx.param::<String>("username").cloned().unwrap_or_default();

View file

@ -8,10 +8,10 @@ async fn poweredby_default_shows_only_pagetop_recognition() {
let html = render_component(&p); let html = render_component(&p);
// Debe mostrar el bloque de reconocimiento a PageTop. // Debe mostrar el bloque de reconocimiento a PageTop.
assert!(html.as_str().contains("poweredby__pagetop")); assert!(html.contains("poweredby__pagetop"));
// Y NO debe mostrar el bloque de copyright. // Y NO debe mostrar el bloque de copyright.
assert!(!html.as_str().contains("poweredby__copyright")); assert!(!html.contains("poweredby__copyright"));
} }
#[pagetop::test] #[pagetop::test]
@ -22,20 +22,17 @@ async fn poweredby_new_includes_current_year_and_app_name() {
let html = render_component(&p); let html = render_component(&p);
let year = Utc::now().format("%Y").to_string(); let year = Utc::now().format("%Y").to_string();
assert!( assert!(html.contains(&year), "HTML should include the current year");
html.as_str().contains(&year),
"HTML should include the current year"
);
// El nombre de la app proviene de `global::SETTINGS.app.name`. // El nombre de la app proviene de `global::SETTINGS.app.name`.
let app_name = &global::SETTINGS.app.name; let app_name = &global::SETTINGS.app.name;
assert!( assert!(
html.as_str().contains(app_name), html.contains(app_name),
"HTML should include the application name" "HTML should include the application name"
); );
// Debe existir el span de copyright. // Debe existir el span de copyright.
assert!(html.as_str().contains("poweredby__copyright")); assert!(html.contains("poweredby__copyright"));
} }
#[pagetop::test] #[pagetop::test]
@ -46,8 +43,8 @@ async fn poweredby_with_copyright_overrides_text() {
let p = PoweredBy::default().with_copyright(Some(custom)); let p = PoweredBy::default().with_copyright(Some(custom));
let html = render_component(&p); let html = render_component(&p);
assert!(html.as_str().contains(custom)); assert!(html.contains(custom));
assert!(html.as_str().contains("poweredby__copyright")); assert!(html.contains("poweredby__copyright"));
} }
#[pagetop::test] #[pagetop::test]
@ -57,9 +54,9 @@ async fn poweredby_with_copyright_none_hides_text() {
let p = PoweredBy::new().with_copyright(None::<String>); let p = PoweredBy::new().with_copyright(None::<String>);
let html = render_component(&p); let html = render_component(&p);
assert!(!html.as_str().contains("poweredby__copyright")); assert!(!html.contains("poweredby__copyright"));
// El reconocimiento a PageTop siempre debe aparecer. // El reconocimiento a PageTop siempre debe aparecer.
assert!(html.as_str().contains("poweredby__pagetop")); assert!(html.contains("poweredby__pagetop"));
} }
#[pagetop::test] #[pagetop::test]
@ -70,7 +67,7 @@ async fn poweredby_link_points_to_crates_io() {
let html = render_component(&p); let html = render_component(&p);
assert!( assert!(
html.as_str().contains("https://pagetop.cillero.es"), html.contains("https://pagetop.cillero.es"),
"Link should point to pagetop.cillero.es" "Link should point to pagetop.cillero.es"
); );
} }
@ -92,8 +89,12 @@ async fn poweredby_getter_reflects_internal_state() {
// HELPERS ***************************************************************************************** // HELPERS *****************************************************************************************
fn render_component<C: Component>(c: &C) -> Markup { fn render(x: &impl Render) -> String {
x.render().into_string()
}
fn render_component<C: Component>(c: &C) -> String {
let mut cx = Context::default(); let mut cx = Context::default();
let pm = c.prepare_component(&mut cx); let pm = c.prepare_component(&mut cx);
pm.render() render(&pm)
} }

View file

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