diff --git a/src/html.rs b/src/html.rs index 14e72b9..14a89ed 100644 --- a/src/html.rs +++ b/src/html.rs @@ -2,3 +2,77 @@ mod maud; pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE}; + +mod assets; +pub use assets::favicon::Favicon; +pub use assets::javascript::JavaScript; +pub use assets::stylesheet::{StyleSheet, TargetMedia}; +pub(crate) use assets::Assets; + +mod context; +pub use context::{Context, ErrorParam}; + +use crate::AutoDefault; + +/// Prepara contenido HTML para su conversión a [`Markup`]. +/// +/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML escapado o marcado +/// ya procesado) para renderizar de forma homogénea en plantillas sin interferir con el uso +/// estándar de [`Markup`]. +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// let fragment = PrepareMarkup::Text(String::from("Hola mundo")); +/// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>"); +/// +/// let raw_html = PrepareMarkup::Escaped(String::from("negrita")); +/// assert_eq!(raw_html.render().into_string(), "negrita"); +/// +/// let prepared = PrepareMarkup::With(html! { +/// h2 { "Título de ejemplo" } +/// p { "Este es un párrafo con contenido dinámico." } +/// }); +/// assert_eq!( +/// prepared.render().into_string(), +/// "

Título de ejemplo

Este es un párrafo con contenido dinámico.

" +/// ); +/// ``` +#[derive(AutoDefault)] +pub enum PrepareMarkup { + /// No se genera contenido HTML (devuelve `html! {}`). + #[default] + None, + /// Texto estático que se escapará automáticamente para no ser interpretado como HTML. + Text(String), + /// Contenido sin escapado adicional, útil para HTML generado externamente. + Escaped(String), + /// Fragmento HTML ya preparado como [`Markup`], listo para insertarse directamente. + With(Markup), +} + +impl PrepareMarkup { + /// Devuelve `true` si el contenido está vacío y no generará HTML al renderizar. + pub fn is_empty(&self) -> bool { + match self { + PrepareMarkup::None => true, + PrepareMarkup::Text(text) => text.is_empty(), + PrepareMarkup::Escaped(string) => string.is_empty(), + PrepareMarkup::With(markup) => markup.is_empty(), + } + } +} + +impl Render for PrepareMarkup { + /// Integra el renderizado fácilmente en la macro [`html!`]. + fn render(&self) -> Markup { + match self { + PrepareMarkup::None => html! {}, + PrepareMarkup::Text(text) => html! { (text) }, + PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) }, + PrepareMarkup::With(markup) => html! { (markup) }, + } + } +} diff --git a/src/html/assets.rs b/src/html/assets.rs new file mode 100644 index 0000000..894b7e8 --- /dev/null +++ b/src/html/assets.rs @@ -0,0 +1,63 @@ +pub mod favicon; +pub mod javascript; +pub mod stylesheet; + +use crate::html::{html, Markup, Render}; +use crate::{AutoDefault, Weight}; + +pub trait AssetsTrait: Render { + // Devuelve el nombre del recurso, utilizado como clave única. + fn name(&self) -> &str; + + // Devuelve el peso del recurso, durante el renderizado se procesan de menor a mayor peso. + fn weight(&self) -> Weight; +} + +#[derive(AutoDefault)] +pub(crate) struct Assets(Vec); + +impl Assets { + pub fn new() -> Self { + Assets::(Vec::::new()) + } + + pub fn add(&mut self, asset: T) -> bool { + match self.0.iter().position(|x| x.name() == asset.name()) { + Some(index) => { + if self.0[index].weight() > asset.weight() { + self.0.remove(index); + self.0.push(asset); + true + } else { + false + } + } + _ => { + self.0.push(asset); + true + } + } + } + + pub fn remove(&mut self, name: impl AsRef) -> bool { + if let Some(index) = self.0.iter().position(|x| x.name() == name.as_ref()) { + self.0.remove(index); + true + } else { + false + } + } +} + +impl Render for Assets { + fn render(&self) -> Markup { + let mut assets = self.0.iter().collect::>(); + assets.sort_by_key(|a| a.weight()); + + html! { + @for a in assets { + (a.render()) + } + } + } +} diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs new file mode 100644 index 0000000..97a44e8 --- /dev/null +++ b/src/html/assets/favicon.rs @@ -0,0 +1,165 @@ +use crate::html::{html, Markup, Render}; +use crate::AutoDefault; + +/// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio. +/// +/// Es universalmente aceptado para mostrar el icono del sitio (`.ico`, `.png`, `.svg`, …) en +/// pestañas, marcadores o accesos directos. +/// +/// Este tipo permite construir de forma fluida las distintas variantes de un *favicon*, ya sea un +/// icono estándar, un icono Apple para la pantalla de inicio, o un icono para Safari con color. +/// También puede aplicar colores al tema o configuraciones específicas para *tiles* de Windows. +/// +/// > **Nota** +/// > Los archivos de los iconos deben estar disponibles en el servidor web de la aplicación. Pueden +/// > incluirse en el proyecto utilizando [`include_files`](crate::include_files) y servirse con +/// > [`include_files_service`](crate::include_files_service). +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// let favicon = Favicon::new() +/// // Estándar de facto admitido por todos los navegadores. +/// .with_icon("/icons/favicon.ico") +/// +/// // Variante del favicon con tamaños explícitos: 32×32 y 16×16. +/// .with_icon_for_sizes("/icons/favicon-32.png", "32x32") +/// .with_icon_for_sizes("/icons/favicon-16.png", "16x16") +/// +/// // Icono específico para accesos directos en la pantalla de inicio de iOS. +/// .with_apple_touch_icon("/icons/apple-touch-icon.png", "180x180") +/// +/// // Icono vectorial con color dinámico para pestañas ancladas en Safari. +/// .with_mask_icon("/icons/safari-pinned-tab.svg", "#5bbad5") +/// +/// // Personaliza la barra superior del navegador en Android Chrome (y soportado en otros). +/// .with_theme_color("#ffffff") +/// +/// // Personalizaciones específicas para "tiles" en Windows. +/// .with_ms_tile_color("#da532c") +/// .with_ms_tile_image("/icons/mstile-144x144.png"); +/// ``` +#[derive(AutoDefault)] +pub struct Favicon(Vec); + +impl Favicon { + /// Crea un nuevo `Favicon` vacío. + /// + /// Equivalente a `Favicon::default()`. Se recomienda iniciar la secuencia de configuración + /// desde aquí. + pub fn new() -> Self { + Favicon::default() + } + + // Favicon BUILDER ***************************************************************************** + + /// Le añade un icono genérico apuntando a `image`. El tipo MIME se infiere automáticamente a + /// partir de la extensión. + pub fn with_icon(self, image: impl Into) -> Self { + self.add_icon_item("icon", image.into(), None, None) + } + + /// Le añade un icono genérico con atributo `sizes`, útil para indicar resoluciones específicas. + /// + /// El atributo `sizes` informa al navegador de las dimensiones de la imagen para que seleccione + /// el recurso más adecuado. Puede enumerar varias dimensiones separadas por espacios, p.ej. + /// `"16x16 32x32 48x48"` o usar `any` para iconos escalables (SVG). + /// + /// No es imprescindible, pero puede mejorar la selección del icono más adecuado. + pub fn with_icon_for_sizes(self, image: impl Into, sizes: impl Into) -> Self { + self.add_icon_item("icon", image.into(), Some(sizes.into()), None) + } + + /// Le añade un *Apple Touch Icon*, usado por dispositivos iOS para las pantallas de inicio. + /// + /// Se recomienda indicar también el tamaño, p.ej. `"256x256"`. + pub fn with_apple_touch_icon(self, image: impl Into, sizes: impl Into) -> Self { + self.add_icon_item("apple-touch-icon", image.into(), Some(sizes.into()), None) + } + + /// Le añade un icono para el navegador Safari, con un color dinámico. + /// + /// El atributo `color` lo usa Safari para colorear el trazado SVG cuando el icono se muestra en + /// modo *Pinned Tab*. Aunque Safari 12+ acepta *favicons normales*, este método garantiza + /// compatibilidad con versiones anteriores. + pub fn with_mask_icon(self, image: impl Into, color: impl Into) -> Self { + self.add_icon_item("mask-icon", image.into(), None, Some(color.into())) + } + + /// Define el color del tema (``). + /// + /// Lo usan algunos navegadores para colorear la barra de direcciones o interfaces. + pub fn with_theme_color(mut self, color: impl Into) -> Self { + self.0.push(html! { + meta name="theme-color" content=(color.into()); + }); + self + } + + /// Define el color del *tile* en Windows (``). + pub fn with_ms_tile_color(mut self, color: impl Into) -> Self { + self.0.push(html! { + meta name="msapplication-TileColor" content=(color.into()); + }); + self + } + + /// Define la imagen del *tile* en Windows (``). + pub fn with_ms_tile_image(mut self, image: impl Into) -> Self { + self.0.push(html! { + meta name="msapplication-TileImage" content=(image.into()); + }); + self + } + + // Función interna que centraliza la creación de las etiquetas ``. + // + // - `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`). + // + // También infiere automáticamente el tipo MIME (`type`) según la extensión del archivo. + fn add_icon_item( + mut self, + icon_rel: &str, + icon_source: String, + icon_sizes: Option, + icon_color: Option, + ) -> Self { + let icon_type = match icon_source.rfind('.') { + Some(i) => match icon_source[i..].to_owned().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); + }); + self + } +} + +impl Render for Favicon { + fn render(&self) -> Markup { + html! { + @for item in &self.0 { + (item) + } + } + } +} diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs new file mode 100644 index 0000000..fb0a1b6 --- /dev/null +++ b/src/html/assets/javascript.rs @@ -0,0 +1,178 @@ +use crate::html::assets::AssetsTrait; +use crate::html::{html, Markup, Render}; +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. +// +// - [`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 { + JavaScript { + source: Source::Inline(name.into(), script.into()), + ..Default::default() + } + } + + /// Crea un **script embebido** que se ejecuta automáticamente al terminar de cargarse el + /// documento HTML. + /// + /// 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 { + JavaScript { + source: Source::OnLoad(name.into(), script.into()), + ..Default::default() + } + } + + // JavaScript BUILDER ************************************************************************** + + /// 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) -> Self { + self.version = version.into(); + self + } + + /// 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. + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } +} + +impl AssetsTrait for JavaScript { + // Para *scripts* externos es la ruta; para *scripts* embebidos, un identificador. + fn name(&self) -> &str { + match &self.source { + Source::From(path) => path, + Source::Defer(path) => path, + Source::Async(path) => path, + Source::Inline(name, _) => name, + Source::OnLoad(name, _) => name, + } + } + + fn weight(&self) -> Weight { + self.weight + } +} + +impl Render for JavaScript { + fn render(&self) -> Markup { + match &self.source { + Source::From(path) => html! { + script src=(join_pair!(path, "?v=", self.version.as_str())) {}; + }, + Source::Defer(path) => html! { + script src=(join_pair!(path, "?v=", self.version.as_str())) defer {}; + }, + Source::Async(path) => html! { + script src=(join_pair!(path, "?v=", self.version.as_str())) async {}; + }, + Source::Inline(_, code) => html! { + script { (code) }; + }, + Source::OnLoad(_, code) => html! { (join!( + "document.addEventListener('DOMContentLoaded',function(){", code, "});" + )) }, + } + } +} diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs new file mode 100644 index 0000000..7f64d18 --- /dev/null +++ b/src/html/assets/stylesheet.rs @@ -0,0 +1,174 @@ +use crate::html::assets::AssetsTrait; +use crate::html::{html, Markup, PreEscaped, Render}; +use crate::{join_pair, AutoDefault, 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 ``. El parámetro `name` se usa como identificador interno del + /// recurso. + pub fn inline(name: impl Into, styles: impl Into) -> Self { + StyleSheet { + source: Source::Inline(name.into(), styles.into()), + ..Default::default() + } + } + + // StyleSheet BUILDER ************************************************************************** + + /// 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) -> Self { + self.version = version.into(); + self + } + + /// 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. + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + // StyleSheet EXTRAS *************************************************************************** + + /// Especifica el medio donde se aplican los estilos. + /// + /// 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. + pub fn for_media(mut self, media: TargetMedia) -> Self { + self.media = media; + self + } +} + +impl AssetsTrait for StyleSheet { + // Para hojas de estilos externas es la ruta; para las embebidas, un identificador. + fn name(&self) -> &str { + match &self.source { + Source::From(path) => path, + Source::Inline(name, _) => name, + } + } + + fn weight(&self) -> Weight { + self.weight + } +} + +impl Render for StyleSheet { + fn render(&self) -> Markup { + match &self.source { + Source::From(path) => html! { + link + rel="stylesheet" + href=(join_pair!(path, "?v=", self.version.as_str())) + media=[self.media.as_str_opt()]; + }, + Source::Inline(_, code) => html! { + style { (PreEscaped(code)) }; + }, + } + } +} diff --git a/src/html/context.rs b/src/html/context.rs new file mode 100644 index 0000000..23e2be2 --- /dev/null +++ b/src/html/context.rs @@ -0,0 +1,216 @@ +use crate::core::TypeInfo; +use crate::html::{html, Markup, Render}; +use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; +use crate::join; +use crate::locale::{LanguageIdentifier, DEFAULT_LANGID}; +use crate::service::HttpRequest; + +use std::collections::HashMap; +use std::error::Error; +use std::str::FromStr; + +use std::fmt; + +/// Errores de lectura o conversión de parámetros almacenados en el contexto. +#[derive(Debug)] +pub enum ErrorParam { + /// El parámetro solicitado no existe. + NotFound, + /// El valor del parámetro no pudo convertirse al tipo requerido. + ParseError(String), +} + +impl fmt::Display for ErrorParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ErrorParam::NotFound => write!(f, "Parameter not found"), + ErrorParam::ParseError(e) => write!(f, "Parse error: {e}"), + } + } +} + +impl Error for ErrorParam {} + +/// Representa el contexto asociado a un documento HTML. +/// +/// Esta estructura se crea internamente para recoger información relativa al documento asociado, +/// como la solicitud HTTP de origen, el idioma y los recursos *favicon* ([`Favicon`]), las hojas de +/// estilo [`StyleSheet`], los *scripts* [`JavaScript`], o parámetros de contexto definidos en +/// tiempo de ejecución. +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// fn configure_context(mut ctx: Context) { +/// // Establece el idioma del documento a español. +/// ctx.set_langid(LangMatch::langid_or_default("es-ES")); +/// +/// // Asigna un favicon. +/// ctx.set_favicon(Some(Favicon::new().with_icon("/icons/favicon.ico"))); +/// +/// // Añade una hoja de estilo externa. +/// ctx.add_stylesheet(StyleSheet::from("/css/style.css")); +/// +/// // Añade un script JavaScript. +/// ctx.add_javascript(JavaScript::defer("/js/main.js")); +/// +/// // Añade un parámetro dinámico al contexto. +/// ctx.set_param("usuario_id", 42); +/// +/// // Recupera el parámetro y lo convierte a su tipo original. +/// let id: i32 = ctx.get_param("usuario_id").unwrap(); +/// assert_eq!(id, 42); +/// +/// // Genera un identificador único para un componente de tipo `Menu`. +/// struct Menu; +/// let unique_id = ctx.required_id::(None); +/// assert_eq!(unique_id, "menu-1"); // Si es el primero generado. +/// } +/// ``` +#[rustfmt::skip] +pub struct Context { + request : HttpRequest, // Solicitud HTTP de origen. + langid : &'static LanguageIdentifier, // Identificador del idioma. + favicon : Option, // Favicon, si se ha definido. + stylesheets: Assets, // Hojas de estilo CSS. + javascripts: Assets, // Scripts JavaScript. + params : HashMap, // Parámetros definidos en tiempo de ejecución. + id_counter : usize, // Contador para generar identificadores únicos. +} + +impl Context { + // Crea un nuevo contexto asociado a una solicitud HTTP. + // + // El contexto inicializa el idioma por defecto, sin favicon ni recursos cargados. + #[rustfmt::skip] + pub(crate) fn new(request: HttpRequest) -> Self { + Context { + request, + langid : &DEFAULT_LANGID, + favicon : None, + stylesheets: Assets::::new(), + javascripts: Assets::::new(), + params : HashMap::::new(), + id_counter : 0, + } + } + + /// Modifica el identificador de idioma del documento. + pub fn set_langid(&mut self, langid: &'static LanguageIdentifier) -> &mut Self { + self.langid = langid; + self + } + + /// Define el *favicon* del documento. Sobrescribe cualquier valor anterior. + pub fn set_favicon(&mut self, favicon: Option) -> &mut Self { + self.favicon = favicon; + self + } + + /// Define el *favicon* solo si no se ha establecido previamente. + pub fn set_favicon_if_none(&mut self, favicon: Favicon) -> &mut Self { + if self.favicon.is_none() { + self.favicon = Some(favicon); + } + self + } + + /// Añade una hoja de estilos CSS al documento. + pub fn add_stylesheet(&mut self, css: StyleSheet) -> &mut Self { + self.stylesheets.add(css); + self + } + + /// Elimina una hoja de estilos por su ruta o identificador. + pub fn remove_stylesheet(&mut self, name: impl AsRef) -> &mut Self { + self.stylesheets.remove(name); + self + } + + /// Añade un *script* JavaScript al documento. + pub fn add_javascript(&mut self, js: JavaScript) -> &mut Self { + self.javascripts.add(js); + self + } + + /// Elimina un *script* por su ruta o identificador. + pub fn remove_javascript(&mut self, name: impl AsRef) -> &mut Self { + self.javascripts.remove(name); + self + } + + /// Añade o modifica un parámetro del contexto almacenando el valor como [`String`]. + pub fn set_param(&mut self, key: impl AsRef, value: T) -> &mut Self { + self.params + .insert(key.as_ref().to_string(), value.to_string()); + self + } + + // Context GETTERS ***************************************************************************** + + /// Devuelve la solicitud HTTP asociada al documento. + pub fn request(&self) -> &HttpRequest { + &self.request + } + + /// Devuelve el identificador del idioma asociado al documento. + pub fn langid(&self) -> &LanguageIdentifier { + self.langid + } + + /// Recupera un parámetro del contexto convertido al tipo especificado. + /// + /// Devuelve un error si el parámetro no existe ([`ErrorParam::NotFound`]) o la conversión falla + /// ([`ErrorParam::ParseError`]). + pub fn get_param(&self, key: impl AsRef) -> Result { + self.params + .get(key.as_ref()) + .ok_or(ErrorParam::NotFound) + .and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone()))) + } + + // Context EXTRAS ****************************************************************************** + + /// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó. + pub fn remove_param(&mut self, key: impl AsRef) -> bool { + self.params.remove(key.as_ref()).is_some() + } + + /// Genera un identificador único si no se proporciona uno explícito. + /// + /// Si no se proporciona un `id`, se genera un identificador único en la forma `-` + /// donde `` es el nombre corto del tipo en minúsculas (sin espacios) y `` es un + /// contador interno incremental. + pub fn required_id(&mut self, id: Option) -> String { + if let Some(id) = id { + id + } else { + let prefix = TypeInfo::ShortName + .of::() + .trim() + .replace(' ', "_") + .to_lowercase(); + let prefix = if prefix.is_empty() { + "prefix".to_owned() + } else { + prefix + }; + self.id_counter += 1; + join!(prefix, "-", self.id_counter.to_string()) + } + } +} + +impl Render for Context { + fn render(&self) -> Markup { + html! { + @if let Some(favicon) = &self.favicon { + (favicon) + } + (self.stylesheets) + (self.javascripts) + } + } +} diff --git a/src/html/maud.rs b/src/html/maud.rs index 39cb2c2..9bf179e 100644 --- a/src/html/maud.rs +++ b/src/html/maud.rs @@ -18,7 +18,7 @@ pub use pagetop_macros::html; mod escape; -/// An adapter that escapes HTML special characters. +/// Adaptador que escapa los caracteres especiales de HTML. /// /// The following characters are escaped: /// @@ -57,7 +57,7 @@ impl fmt::Write for Escaper<'_> { } } -/// Represents a type that can be rendered as HTML. +/// Representa un tipo que puede renderizarse como HTML. /// /// To implement this for your own type, override either the `.render()` /// or `.render_to()` methods; since each is defined in terms of the @@ -190,7 +190,7 @@ impl_render_with_itoa! { u8 u16 u32 u64 u128 usize } -/// Renders a value using its [`Display`] impl. +/// Renderiza un valor usando su implementación de [`Display`]. /// /// # Example /// @@ -219,7 +219,7 @@ pub fn display(value: impl Display) -> impl Render { DisplayWrapper(value) } -/// A wrapper that renders the inner value without escaping. +/// Contenedor que renderiza el valor interno sin escapar. #[derive(Debug, Clone, Copy)] pub struct PreEscaped(pub T); @@ -229,7 +229,7 @@ impl> Render for PreEscaped { } } -/// A block of markup is a string that does not need to be escaped. +/// Un bloque de marcado es una cadena que no necesita ser escapada. /// /// The `html!` macro expands to an expression of this type. pub type Markup = PreEscaped; @@ -259,7 +259,7 @@ impl Default for PreEscaped { } } -/// The literal string ``. +/// La cadena literal ``. /// /// # Example /// diff --git a/src/lib.rs b/src/lib.rs index 248ba6d..f312621 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,12 @@ impl Deref for StaticResources { } } +/// Representa el peso lógico de una instancia en una colección ordenada por pesos. +/// +/// Las instancias con pesos **más bajos**, incluyendo valores negativos (`-128..127`), se situarán +/// antes en la ordenación. +pub type Weight = i8; + // API ********************************************************************************************* // Funciones y macros útiles. diff --git a/tests/html.rs b/tests/html.rs new file mode 100644 index 0000000..315f74a --- /dev/null +++ b/tests/html.rs @@ -0,0 +1,17 @@ +use pagetop::prelude::*; + +#[pagetop::test] +async fn prepare_markup_is_empty() { + let _app = service::test::init_service(Application::new().test()).await; + + assert!(PrepareMarkup::None.is_empty()); + + assert!(PrepareMarkup::Text(String::from("")).is_empty()); + assert!(!PrepareMarkup::Text(String::from("x")).is_empty()); + + assert!(PrepareMarkup::Escaped(String::new()).is_empty()); + assert!(!PrepareMarkup::Escaped("a".into()).is_empty()); + + assert!(PrepareMarkup::With(html! {}).is_empty()); + assert!(!PrepareMarkup::With(html! { span { "!" } }).is_empty()); +}