Compare commits

..

2 commits

Author SHA1 Message Date
f4e142a242 📝 Retoques en la documentación 2025-07-20 14:28:09 +02:00
d042467f50 Añade soporte para recursos en documentos HTML
- Incluye los recursos favicon, hojas de estilo y scripts JavaScript.
- Se introduce una estructura de contexto que, además de gestionar el
  idioma y el uso de parámetros contextuales, permite administrar estos
  recursos en documentos HTML.
2025-07-20 14:24:19 +02:00
15 changed files with 923 additions and 28 deletions

View file

@ -201,5 +201,5 @@ pub trait AnyCast: AnyInfo {
/// Implementación automática para cualquier tipo que ya cumpla [`AnyInfo`]. /// Implementación automática para cualquier tipo que ya cumpla [`AnyInfo`].
impl<T: ?Sized + AnyInfo> AnyCast for T {} impl<T: ?Sized + AnyInfo> AnyCast for T {}
// Infraestructura para ampliar funcionalidades mediante extensiones. // API para añadir nuevas funcionalidades usando extensiones.
pub mod extension; pub mod extension;

View file

@ -1,4 +1,4 @@
//! Infraestructura para ampliar funcionalidades mediante extensiones. //! API para añadir nuevas funcionalidades usando extensiones.
//! //!
//! Cada funcionalidad adicional que quiera incorporarse a una aplicación `PageTop` se debe modelar //! Cada funcionalidad adicional que quiera incorporarse a una aplicación `PageTop` se debe modelar
//! como una **extensión**. Todas comparten la misma interfaz declarada en [`ExtensionTrait`]. //! como una **extensión**. Todas comparten la misma interfaz declarada en [`ExtensionTrait`].

View file

@ -10,8 +10,8 @@ pub type ExtensionRef = &'static dyn ExtensionTrait;
/// Interfaz común que debe implementar cualquier extensión de `PageTop`. /// Interfaz común que debe implementar cualquier extensión de `PageTop`.
/// ///
/// Este *trait* es fácil de implementar, basta con declarar la estructura de la extensión y /// Este *trait* es fácil de implementar, basta con declarar una estructura de tamaño cero para la
/// sobreescribir los métodos que sea necesario. /// extensión y sobreescribir los métodos que sea necesario.
/// ///
/// ```rust /// ```rust
/// use pagetop::prelude::*; /// use pagetop::prelude::*;

View file

@ -2,3 +2,77 @@
mod maud; mod maud;
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE}; 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 <b>mundo</b>"));
/// assert_eq!(fragment.render().into_string(), "Hola &lt;b&gt;mundo&lt;/b&gt;");
///
/// let raw_html = PrepareMarkup::Escaped(String::from("<b>negrita</b>"));
/// assert_eq!(raw_html.render().into_string(), "<b>negrita</b>");
///
/// 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(),
/// "<h2>Título de ejemplo</h2><p>Este es un párrafo con contenido dinámico.</p>"
/// );
/// ```
#[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) },
}
}
}

63
src/html/assets.rs Normal file
View file

@ -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<T>(Vec<T>);
impl<T: AssetsTrait> Assets<T> {
pub fn new() -> Self {
Assets::<T>(Vec::<T>::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<str>) -> bool {
if let Some(index) = self.0.iter().position(|x| x.name() == name.as_ref()) {
self.0.remove(index);
true
} else {
false
}
}
}
impl<T: AssetsTrait> Render for Assets<T> {
fn render(&self) -> Markup {
let mut assets = self.0.iter().collect::<Vec<_>>();
assets.sort_by_key(|a| a.weight());
html! {
@for a in assets {
(a.render())
}
}
}
}

165
src/html/assets/favicon.rs Normal file
View file

@ -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<Markup>);
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<String>) -> 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<String>, sizes: impl Into<String>) -> Self {
self.add_icon_item("icon", image.into(), Some(sizes.into()), None)
}
/// Le añade un *Apple Touch Icon*, usado por dispositivos iOS para las pantallas de inicio.
///
/// Se recomienda indicar también el tamaño, p.ej. `"256x256"`.
pub fn with_apple_touch_icon(self, image: impl Into<String>, sizes: impl Into<String>) -> Self {
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<String>, color: impl Into<String>) -> Self {
self.add_icon_item("mask-icon", image.into(), None, Some(color.into()))
}
/// Define el color del tema (`<meta name="theme-color">`).
///
/// Lo usan algunos navegadores para colorear la barra de direcciones o interfaces.
pub fn with_theme_color(mut self, color: impl Into<String>) -> Self {
self.0.push(html! {
meta name="theme-color" content=(color.into());
});
self
}
/// Define el color del *tile* en Windows (`<meta name="msapplication-TileColor">`).
pub fn with_ms_tile_color(mut self, color: impl Into<String>) -> Self {
self.0.push(html! {
meta name="msapplication-TileColor" content=(color.into());
});
self
}
/// Define la imagen del *tile* en Windows (`<meta name="msapplication-TileImage">`).
pub fn with_ms_tile_image(mut self, image: impl Into<String>) -> Self {
self.0.push(html! {
meta name="msapplication-TileImage" content=(image.into());
});
self
}
// Función interna que centraliza la creación de las etiquetas `<link>`.
//
// - `icon_rel`: indica el tipo de recurso (`"icon"`, `"apple-touch-icon"`, etc.).
// - `icon_source`: URL del recurso.
// - `icon_sizes`: tamaños opcionales.
// - `icon_color`: color opcional (solo relevante para `mask-icon`).
//
// También infiere automáticamente el tipo MIME (`type`) según la extensión del archivo.
fn add_icon_item(
mut self,
icon_rel: &str,
icon_source: String,
icon_sizes: Option<String>,
icon_color: Option<String>,
) -> 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)
}
}
}
}

View file

@ -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 `<script src="...">`.
// - [`Defer`] Igual que [`From`], pero con el atributo `defer`.
// - [`Async`] Igual que [`From`], pero con el atributo `async`.
// - [`Inline`] Inserta el código directamente en la etiqueta `<script>`.
// - [`OnLoad`] Inserta el código JavaScript y lo ejecuta tras el evento `DOMContentLoaded`.
#[derive(AutoDefault)]
enum Source {
#[default]
From(String),
Defer(String),
Async(String),
Inline(String, String),
OnLoad(String, String),
}
/// Define un recurso **JavaScript** para incluir en un documento HTML.
///
/// 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
/// en el documento.
///
/// > **Nota**
/// > Los archivos de los *scripts* 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::*;
///
/// // Script externo con carga diferida, versión para control de caché y prioriza el renderizado.
/// let script = JavaScript::defer("/assets/js/app.js")
/// .with_version("1.2.3")
/// .with_weight(-10);
///
/// // Script embebido que se ejecuta tras la carga del documento.
/// let script = JavaScript::on_load("init_tooltips", r#"
/// const tooltips = document.querySelectorAll('[data-tooltip]');
/// for (const el of tooltips) {
/// el.addEventListener('mouseenter', showTooltip);
/// }
/// "#);
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct JavaScript {
source : Source, // Fuente y modo de carga del script.
version: String, // Versión del recurso para la caché del navegador.
weight : Weight, // Peso que determina el orden.
}
impl JavaScript {
/// Crea un **script externo** que se carga y ejecuta de forma inmediata, en orden con el resto
/// del documento HTML.
///
/// Equivale a `<script src="...">`.
pub fn from(path: impl Into<String>) -> Self {
JavaScript {
source: Source::From(path.into()),
..Default::default()
}
}
/// Crea un **script externo** con el atributo `defer`, que se carga en segundo plano y se
/// ejecuta tras analizar completamente el documento HTML.
///
/// Equivale a `<script src="..." defer>`. Útil para mantener el orden de ejecución y evitar
/// bloquear el análisis del documento HTML.
pub fn defer(path: impl Into<String>) -> Self {
JavaScript {
source: Source::Defer(path.into()),
..Default::default()
}
}
/// Crea un **script externo** con el atributo `async`, que se carga y ejecuta de forma
/// asíncrona tan pronto como esté disponible.
///
/// 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 {
JavaScript {
source: Source::Async(path.into()),
..Default::default()
}
}
/// Crea un **script embebido** directamente en el documento HTML.
///
/// Equivale a `<script>…</script>`. El parámetro `name` se usa como identificador interno del
/// *script*.
pub fn inline(name: impl Into<String>, script: impl Into<String>) -> 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<String>, script: impl Into<String>) -> 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<String>) -> 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, "});"
)) },
}
}
}

View file

@ -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 `<style>`.
//
// - [`From`] Carga la hoja de estilos desde un archivo externo, insertándola mediante una
// etiqueta `<link>` con `rel="stylesheet"`.
// - [`Inline`] Inserta directamente el contenido CSS dentro de una etiqueta `<style>`.
#[derive(AutoDefault)]
enum Source {
#[default]
From(String),
Inline(String, String),
}
/// Define el medio objetivo para la hoja de estilos.
///
/// Permite especificar en qué contexto se aplica el CSS, adaptándose a diferentes dispositivos o
/// situaciones de impresión.
#[derive(AutoDefault)]
pub enum TargetMedia {
/// Se aplica en todos los casos (el atributo `media` se omite).
#[default]
Default,
/// Se aplica cuando el documento se imprime.
Print,
/// Se aplica en pantallas.
Screen,
/// Se aplica en dispositivos que convierten el texto a voz.
Speech,
}
/// Devuelve el texto asociado al punto de interrupción usado por Bootstrap.
#[rustfmt::skip]
impl TargetMedia {
fn as_str_opt(&self) -> Option<&str> {
match self {
TargetMedia::Default => None,
TargetMedia::Print => Some("print"),
TargetMedia::Screen => Some("screen"),
TargetMedia::Speech => Some("speech"),
}
}
}
/// Define un recurso **StyleSheet** para incluir en un documento HTML.
///
/// Este tipo permite incluir hojas de estilo CSS externas o embebidas, con soporte para medios
/// específicos (`screen`, `print`, etc.) y [pesos](crate::Weight) que determinan el orden de
/// inserción en el documento.
///
/// > **Nota**
/// > Las hojas de estilo CSS 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::*;
///
/// // Crea una hoja de estilos externa con control de versión y medio específico (`screen`).
/// let stylesheet = StyleSheet::from("/assets/css/main.css")
/// .with_version("2.0.1")
/// .for_media(TargetMedia::Screen)
/// .with_weight(-10);
///
/// // Crea una hoja de estilos embebida en el documento HTML.
/// let embedded = StyleSheet::inline("custom_theme", r#"
/// body {
/// background-color: #f5f5f5;
/// font-family: 'Segoe UI', sans-serif;
/// }
/// "#);
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct StyleSheet {
source : Source, // Fuente y modo de inclusión del CSS.
version: String, // Versión del recurso para la caché del navegador.
media : TargetMedia, // Medio objetivo para los estilos (`print`, `screen`, …).
weight : Weight, // Peso que determina el orden.
}
impl StyleSheet {
/// Crea una hoja de estilos externa.
///
/// Equivale a `<link rel="stylesheet" href="...">`.
pub fn from(path: impl Into<String>) -> Self {
StyleSheet {
source: Source::From(path.into()),
..Default::default()
}
}
/// Crea una hoja de estilos embebida directamente en el documento HTML.
///
/// Equivale a `<style>…</style>`. El parámetro `name` se usa como identificador interno del
/// recurso.
pub fn inline(name: impl Into<String>, styles: impl Into<String>) -> 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<String>) -> 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)) };
},
}
}
}

216
src/html/context.rs Normal file
View file

@ -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::<Menu>(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>, // Favicon, si se ha definido.
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
javascripts: Assets<JavaScript>, // Scripts JavaScript.
params : HashMap<String, String>, // 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::<StyleSheet>::new(),
javascripts: Assets::<JavaScript>::new(),
params : HashMap::<String, String>::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<Favicon>) -> &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<str>) -> &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<str>) -> &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<T: ToString>(&mut self, key: impl AsRef<str>, 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<T: FromStr>(&self, key: impl AsRef<str>) -> Result<T, ErrorParam> {
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<str>) -> 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 `<tipo>-<número>`
/// donde `<tipo>` es el nombre corto del tipo en minúsculas (sin espacios) y `<número>` es un
/// contador interno incremental.
pub fn required_id<T>(&mut self, id: Option<String>) -> String {
if let Some(id) = id {
id
} else {
let prefix = TypeInfo::ShortName
.of::<T>()
.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)
}
}
}

View file

@ -18,7 +18,7 @@ pub use pagetop_macros::html;
mod escape; mod escape;
/// An adapter that escapes HTML special characters. /// Adaptador que escapa los caracteres especiales de HTML.
/// ///
/// The following characters are escaped: /// 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()` /// To implement this for your own type, override either the `.render()`
/// or `.render_to()` methods; since each is defined in terms of the /// 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 u8 u16 u32 u64 u128 usize
} }
/// Renders a value using its [`Display`] impl. /// Renderiza un valor usando su implementación de [`Display`].
/// ///
/// # Example /// # Example
/// ///
@ -219,7 +219,7 @@ pub fn display(value: impl Display) -> impl Render {
DisplayWrapper(value) DisplayWrapper(value)
} }
/// A wrapper that renders the inner value without escaping. /// Contenedor que renderiza el valor interno sin escapar.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct PreEscaped<T>(pub T); pub struct PreEscaped<T>(pub T);
@ -229,7 +229,7 @@ impl<T: AsRef<str>> Render for PreEscaped<T> {
} }
} }
/// 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. /// The `html!` macro expands to an expression of this type.
pub type Markup = PreEscaped<String>; pub type Markup = PreEscaped<String>;
@ -259,7 +259,7 @@ impl<T: Default> Default for PreEscaped<T> {
} }
} }
/// The literal string `<!DOCTYPE html>`. /// La cadena literal `<!DOCTYPE html>`.
/// ///
/// # Example /// # Example
/// ///

View file

@ -29,7 +29,6 @@
//! ``` //! ```
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#![doc( #![doc(
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
)] )]
@ -66,6 +65,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 ********************************************************************************************* // API *********************************************************************************************
// Funciones y macros útiles. // Funciones y macros útiles.

View file

@ -107,8 +107,8 @@ use std::sync::LazyLock;
use std::fmt; use std::fmt;
// Asocia cada código de idioma (como "en-US") con su respectivo [`LanguageIdentifier`] y la clave // Asocia cada identificador de idioma (como "en-US") con su respectivo [`LanguageIdentifier`] y la
// en *locale/.../languages.ftl* para obtener el nombre del idioma según la localización. // clave en *locale/.../languages.ftl* para obtener el nombre del idioma según la localización.
static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> = LazyLock::new(|| { static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
hm![ hm![
"en" => ( langid!("en-US"), "english" ), "en" => ( langid!("en-US"), "english" ),
@ -121,20 +121,20 @@ static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> = LazyLock
// Identificador del idioma de **respaldo** (predefinido a `en-US`). // Identificador del idioma de **respaldo** (predefinido a `en-US`).
// //
// Se usa cuando el valor del código de idioma en las traducciones no corresponde con ningún idioma // Se usa cuando el valor del identificador de idioma en las traducciones no corresponde con ningún
// soportado por la aplicación. // idioma soportado por la aplicación.
static FALLBACK_LANGID: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US")); static FALLBACK_LANGID: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US"));
// Identificador del idioma **por defecto** para la aplicación. // Identificador del idioma **por defecto** para la aplicación.
// //
// Se resuelve a partir de [`global::SETTINGS.app.language`](global::SETTINGS). Si el código de // Se resuelve a partir de [`global::SETTINGS.app.language`](global::SETTINGS). Si el identificador
// idioma configurado no es válido o no está disponible entonces resuelve como [`FALLBACK_LANGID`]. // de idioma no es válido o no está disponible entonces resuelve como [`FALLBACK_LANGID`].
pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> = pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
LazyLock::new(|| LangMatch::langid_or_fallback(&global::SETTINGS.app.language)); LazyLock::new(|| LangMatch::langid_or_fallback(&global::SETTINGS.app.language));
/// Operaciones con los idiomas soportados por `PageTop`. /// Operaciones con los idiomas soportados por `PageTop`.
/// ///
/// Utiliza [`LangMatch`] para transformar un código de idioma en un [`LanguageIdentifier`] /// Utiliza [`LangMatch`] para transformar un identificador de idioma en un [`LanguageIdentifier`]
/// soportado por `PageTop`. /// soportado por `PageTop`.
/// ///
/// # Ejemplos /// # Ejemplos
@ -156,10 +156,10 @@ pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
/// ///
/// // 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")));
/// ``` /// ```
/// ///
/// Las siguientes instrucciones devuelven siempre un [`LanguageIdentifier`] válido, ya sea porque /// Las siguientes líneas devuelven siempre un [`LanguageIdentifier`] válido, ya sea porque
/// resuelven un idioma soportado o porque aplican el idioma por defecto o de respaldo: /// resuelven un idioma soportado o porque aplican el idioma por defecto o de respaldo:
/// ///
/// ```rust /// ```rust
@ -177,13 +177,13 @@ pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
/// ``` /// ```
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum LangMatch { pub enum LangMatch {
/// Cuando el código del idioma es una cadena vacía. /// Cuando el identificador del idioma es una cadena vacía.
Unspecified, Unspecified,
/// Si encuentra un [`LanguageIdentifier`] en la lista de idiomas soportados por `PageTop` que /// Si encuentra un [`LanguageIdentifier`] en la lista de idiomas soportados por `PageTop` que
/// coincide exactamente con el código del idioma (p.ej. "es-ES"), o con el código del idioma /// coincide exactamente con el identificador del idioma (p.ej. "es-ES"), o con el identificador
/// base (p.ej. "es"). /// del idioma base (p.ej. "es").
Found(&'static LanguageIdentifier), Found(&'static LanguageIdentifier),
/// Si el código del idioma no está entre los soportados por `PageTop`. /// Si el identificador del idioma no está entre los soportados por `PageTop`.
Unsupported(String), Unsupported(String),
} }
@ -210,7 +210,7 @@ impl LangMatch {
} }
// En otro caso indica que el idioma no está soportado. // En otro caso indica que el idioma no está soportado.
Self::Unsupported(language.to_string()) Self::Unsupported(String::from(language))
} }
/// Devuelve el idioma de la variante de la instancia, o el idioma por defecto si no está /// Devuelve el idioma de la variante de la instancia, o el idioma por defecto si no está
@ -319,7 +319,7 @@ enum L10nOp {
/// También para traducciones a idiomas concretos. /// También para traducciones a idiomas concretos.
/// ///
/// ```rust,ignore /// ```rust,ignore
/// // Traducción con clave, conjunto de traducciones y código de idioma a usar. /// // Traducción con clave, conjunto de traducciones e identificador de idioma a usar.
/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(LangMatch::langid_or_default("it")); /// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(LangMatch::langid_or_default("it"));
/// ``` /// ```
#[derive(AutoDefault)] #[derive(AutoDefault)]

View file

@ -4,7 +4,7 @@
pub use crate::{builder_fn, html, main, test}; pub use crate::{builder_fn, html, main, test};
pub use crate::{AutoDefault, StaticResources}; pub use crate::{AutoDefault, StaticResources, Weight};
// MACROS. // MACROS.
@ -16,6 +16,8 @@ pub use crate::include_config;
pub use crate::include_locales; pub use crate::include_locales;
// crate::service // crate::service
pub use crate::{include_files, include_files_service}; pub use crate::{include_files, include_files_service};
// crate::core::action
//pub use crate::actions;
// API. // API.
@ -35,6 +37,7 @@ pub use crate::service;
pub use crate::core::{AnyCast, AnyInfo, TypeInfo}; pub use crate::core::{AnyCast, AnyInfo, TypeInfo};
//pub use crate::core::action::*;
pub use crate::core::extension::*; pub use crate::core::extension::*;
pub use crate::app::Application; pub use crate::app::Application;

View file

@ -6,7 +6,7 @@ pub use actix_web::dev::ServiceFactory as Factory;
pub use actix_web::dev::ServiceRequest as Request; pub use actix_web::dev::ServiceRequest as Request;
pub use actix_web::dev::ServiceResponse as Response; pub use actix_web::dev::ServiceResponse as Response;
pub use actix_web::{http, rt, web}; pub use actix_web::{http, rt, web};
pub use actix_web::{App, Error, HttpServer}; pub use actix_web::{App, Error, HttpRequest, HttpServer};
#[doc(hidden)] #[doc(hidden)]
pub use actix_web::test; pub use actix_web::test;

17
tests/html.rs Normal file
View file

@ -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());
}