✨ 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.
This commit is contained in:
parent
3ed94457fa
commit
d042467f50
11 changed files with 904 additions and 8 deletions
165
src/html/assets/favicon.rs
Normal file
165
src/html/assets/favicon.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
178
src/html/assets/javascript.rs
Normal file
178
src/html/assets/javascript.rs
Normal 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, "});"
|
||||
)) },
|
||||
}
|
||||
}
|
||||
}
|
174
src/html/assets/stylesheet.rs
Normal file
174
src/html/assets/stylesheet.rs
Normal 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)) };
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue