Compare commits
2 commits
2a4d6a7890
...
e3ca6079ff
Author | SHA1 | Date | |
---|---|---|---|
e3ca6079ff | |||
ddf78c2de8 |
21 changed files with 234 additions and 175 deletions
|
@ -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_owned();
|
let mut app = app_name.substring(0, maxlen).to_string();
|
||||||
if app_name.len() > maxlen {
|
if app_name.len() > maxlen {
|
||||||
app = format!("{app}...");
|
app = format!("{app}...");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(String::from("visitor"));
|
/// let user = cx.param::<String>("username").cloned().unwrap_or("visitor".to_string());
|
||||||
/// html! {
|
/// html! {
|
||||||
/// h1 { "Hello, " (user) }
|
/// h1 { "Hello, " (user) }
|
||||||
/// }
|
/// }
|
||||||
|
|
|
@ -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, Render};
|
use crate::html::{html, Context, Markup, PrepareMarkup};
|
||||||
|
|
||||||
/// Define la función de renderizado para todos los componentes.
|
/// Define la función de renderizado para todos los componentes.
|
||||||
///
|
///
|
||||||
|
|
|
@ -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 (como animaciones,
|
//! tipografías, espaciados y cualquier otro detalle visual o de comportamiento (como animaciones,
|
||||||
//! *scripts* de interfaz, etc.).
|
//! scripts de interfaz, etc.).
|
||||||
//!
|
//!
|
||||||
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension); por
|
//! 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;
|
||||||
|
|
|
@ -36,7 +36,7 @@ impl Default for Region {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
key: REGION_CONTENT,
|
key: REGION_CONTENT,
|
||||||
name: String::from(REGION_CONTENT),
|
name: REGION_CONTENT.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
src/html.rs
10
src/html.rs
|
@ -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, Render, DOCTYPE};
|
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, 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(String::from("Hola <b>mundo</b>"));
|
/// let fragment = PrepareMarkup::Escaped("Hola <b>mundo</b>".to_string());
|
||||||
/// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>");
|
/// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>");
|
||||||
///
|
///
|
||||||
/// // HTML literal, se inserta directamente, sin escapado adicional.
|
/// // HTML literal, se inserta directamente, sin escapado adicional.
|
||||||
/// let raw_html = PrepareMarkup::Raw(String::from("<b>negrita</b>"));
|
/// let raw_html = PrepareMarkup::Raw("<b>negrita</b>".to_string());
|
||||||
/// 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,11 +119,9 @@ 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!`].
|
||||||
fn render(&self) -> Markup {
|
pub fn render(&self) -> Markup {
|
||||||
match self {
|
match self {
|
||||||
PrepareMarkup::None => html! {},
|
PrepareMarkup::None => html! {},
|
||||||
PrepareMarkup::Escaped(text) => html! { (text) },
|
PrepareMarkup::Escaped(text) => html! { (text) },
|
||||||
|
|
|
@ -2,10 +2,10 @@ pub mod favicon;
|
||||||
pub mod javascript;
|
pub mod javascript;
|
||||||
pub mod stylesheet;
|
pub mod stylesheet;
|
||||||
|
|
||||||
use crate::html::{html, Markup, Render};
|
use crate::html::{html, Context, Markup};
|
||||||
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,12 +13,15 @@ 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: Render {
|
pub trait Asset {
|
||||||
/// 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
|
||||||
|
@ -77,16 +80,13 @@ impl<T: Asset> Assets<T> {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Asset> Render for Assets<T> {
|
pub fn render(&self, cx: &mut Context) -> Markup {
|
||||||
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)
|
(a.render(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::html::{html, Markup, Render};
|
use crate::html::{html, Context, Markup};
|
||||||
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_owned().to_lowercase().as_str() {
|
Some(i) => match icon_source[i..].to_string().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,10 +151,12 @@ impl Favicon {
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Favicon {
|
/// Renderiza el **Favicon** completo con todas las etiquetas declaradas.
|
||||||
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)
|
||||||
|
|
|
@ -1,35 +1,45 @@
|
||||||
use crate::html::assets::Asset;
|
use crate::html::assets::Asset;
|
||||||
use crate::html::{html, Markup, Render};
|
use crate::html::{html, Context, Markup, PreEscaped};
|
||||||
use crate::{join, join_pair, AutoDefault, Weight};
|
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.
|
// script en relación con el análisis del documento HTML y la ejecución del resto de scripts.
|
||||||
//
|
//
|
||||||
// - [`From`] – Carga el script de forma estándar con la etiqueta `<script src="...">`.
|
// - [`From`] – Carga estándar con la etiqueta `<script src="...">`.
|
||||||
// - [`Defer`] – Igual que [`From`], pero con el atributo `defer`.
|
// - [`Defer`] – Igual que [`From`], pero con el atributo `defer`, descarga en paralelo y se
|
||||||
// - [`Async`] – Igual que [`From`], pero con el atributo `async`.
|
// ejecuta tras el análisis del documento HTML, respetando el orden de
|
||||||
|
// aparición.
|
||||||
|
// - [`Async`] – Igual que [`From`], pero con el atributo `async`, descarga en paralelo y se
|
||||||
|
// ejecuta en cuanto esté listo, **sin garantizar** el orden relativo respecto a
|
||||||
|
// otros scripts.
|
||||||
// - [`Inline`] – Inserta el código directamente en la etiqueta `<script>`.
|
// - [`Inline`] – Inserta el código directamente en la etiqueta `<script>`.
|
||||||
// - [`OnLoad`] – Inserta el código JavaScript y lo ejecuta tras el evento `DOMContentLoaded`.
|
// - [`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),
|
||||||
Inline(String, String),
|
// `name`, `closure(Context) -> String`.
|
||||||
OnLoad(String, String),
|
Inline(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||||
|
// `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
|
||||||
|
@ -37,23 +47,37 @@ enum Source {
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// // Script externo con carga diferida, versión para control de caché y prioriza el renderizado.
|
/// // Script externo con carga diferida, versión de caché y prioridad en 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 modo de carga del script.
|
source : Source, // Fuente y estrategia 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.
|
||||||
}
|
}
|
||||||
|
@ -70,11 +94,11 @@ impl JavaScript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un **script externo** con el atributo `defer`, que se carga en segundo plano y se
|
/// Crea un **script externo** con el atributo `defer`, que se descarga en paralelo y se ejecuta
|
||||||
/// ejecuta tras analizar completamente el documento HTML.
|
/// tras analizar completamente el documento HTML, **respetando el orden** de inserción.
|
||||||
///
|
///
|
||||||
/// Equivale a `<script src="..." defer>`. Útil para mantener el orden de ejecución y evitar
|
/// Equivale a `<script src="..." defer>`. Suele ser la opción recomendada para scripts no
|
||||||
/// bloquear el análisis del documento HTML.
|
/// críticos.
|
||||||
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()),
|
||||||
|
@ -82,11 +106,10 @@ impl JavaScript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un **script externo** con el atributo `async`, que se carga y ejecuta de forma
|
/// Crea un **script externo** con el atributo `async`, que se descarga en paralelo y se ejecuta
|
||||||
/// asíncrona tan pronto como esté disponible.
|
/// tan pronto como esté disponible.
|
||||||
///
|
///
|
||||||
/// Equivale a `<script src="..." async>`. La ejecución puede producirse fuera de orden respecto
|
/// Equivale a `<script src="..." async>`. **No garantiza** el orden relativo con otros scripts.
|
||||||
/// 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()),
|
||||||
|
@ -97,37 +120,68 @@ 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(), script.into()),
|
source: Source::Inline(name.into(), Box::new(f)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un **script embebido** que se ejecuta automáticamente al terminar de cargarse el
|
/// Crea un **script embebido** que se ejecuta cuando **el DOM está listo**.
|
||||||
/// documento HTML.
|
|
||||||
///
|
///
|
||||||
/// El código se envuelve automáticamente en un `addEventListener('DOMContentLoaded', ...)`. El
|
/// El código se envuelve en un `addEventListener('DOMContentLoaded',function(){...})` que lo
|
||||||
/// parámetro `name` se usa como identificador interno del *script*.
|
/// ejecuta tras analizar el documento HTML, **no** espera imágenes ni otros recursos externos.
|
||||||
pub fn on_load(name: impl Into<String>, script: impl Into<String>) -> Self {
|
/// Útil para inicializaciones que no dependen de `await`. El parámetro `name` se usa como
|
||||||
|
/// identificador interno del script.
|
||||||
|
///
|
||||||
|
/// Los scripts con `defer` se ejecutan antes de `DOMContentLoaded`.
|
||||||
|
///
|
||||||
|
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||||
|
pub fn on_load<F>(name: impl Into<String>, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
{
|
||||||
JavaScript {
|
JavaScript {
|
||||||
source: Source::OnLoad(name.into(), script.into()),
|
source: Source::OnLoad(name.into(), Box::new(f)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un **script embebido** con un **manejador asíncrono**.
|
||||||
|
///
|
||||||
|
/// El código se envuelve en un `addEventListener('DOMContentLoaded',async()=>{...})`, que
|
||||||
|
/// emplea una función `async` para que el cuerpo devuelto por la función *closure* pueda usar
|
||||||
|
/// `await`. Ideal para hidratar la interfaz, cargar módulos dinámicos o realizar lecturas
|
||||||
|
/// iniciales.
|
||||||
|
///
|
||||||
|
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||||
|
pub fn on_load_async<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.
|
||||||
|
@ -140,7 +194,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,
|
||||||
|
@ -148,16 +202,15 @@ 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
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for JavaScript {
|
fn render(&self, cx: &mut Context) -> Markup {
|
||||||
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())) {};
|
||||||
|
@ -168,12 +221,15 @@ impl Render 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(_, code) => html! {
|
Source::Inline(_, f) => html! {
|
||||||
script { (code) };
|
script { (PreEscaped((f)(cx))) };
|
||||||
},
|
},
|
||||||
Source::OnLoad(_, code) => html! { (join!(
|
Source::OnLoad(_, f) => html! { script { (PreEscaped(join!(
|
||||||
"document.addEventListener('DOMContentLoaded',function(){", code, "});"
|
"document.addEventListener(\"DOMContentLoaded\",function(){", (f)(cx), "});"
|
||||||
)) },
|
))) } },
|
||||||
|
Source::OnLoadAsync(_, f) => html! { script { (PreEscaped(join!(
|
||||||
|
"document.addEventListener(\"DOMContentLoaded\",async()=>{", (f)(cx), "});"
|
||||||
|
))) } },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::html::assets::Asset;
|
use crate::html::assets::Asset;
|
||||||
use crate::html::{html, Markup, PreEscaped, Render};
|
use crate::html::{html, Context, Markup, PreEscaped};
|
||||||
use crate::{join_pair, AutoDefault, Weight};
|
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,7 +14,8 @@ use crate::{join_pair, AutoDefault, Weight};
|
||||||
enum Source {
|
enum Source {
|
||||||
#[default]
|
#[default]
|
||||||
From(String),
|
From(String),
|
||||||
Inline(String, String),
|
// `name`, `closure(Context) -> 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.
|
||||||
|
@ -34,7 +35,7 @@ pub enum TargetMedia {
|
||||||
Speech,
|
Speech,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve el texto asociado al punto de interrupción usado por Bootstrap.
|
/// Devuelve el valor para el atributo `media` (`Some(...)`) o `None` para `Default`.
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
impl TargetMedia {
|
impl TargetMedia {
|
||||||
fn as_str_opt(&self) -> Option<&str> {
|
fn as_str_opt(&self) -> Option<&str> {
|
||||||
|
@ -69,12 +70,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)]
|
||||||
|
@ -100,9 +101,14 @@ 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(), styles.into()),
|
source: Source::Inline(name.into(), Box::new(f)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,9 +139,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 aplican cuando el documento se imprime.
|
/// - `TargetMedia::Print` - Se aplica cuando el documento se imprime.
|
||||||
/// - `TargetMedia::Screen` - Se aplican en pantallas.
|
/// - `TargetMedia::Screen` - Se aplica en pantallas.
|
||||||
/// - `TargetMedia::Speech` - Se aplican en dispositivos que convierten el texto a voz.
|
/// - `TargetMedia::Speech` - Se aplica 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
|
||||||
|
@ -156,10 +162,8 @@ impl Asset for StyleSheet {
|
||||||
fn weight(&self) -> Weight {
|
fn weight(&self) -> Weight {
|
||||||
self.weight
|
self.weight
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for StyleSheet {
|
fn render(&self, cx: &mut Context) -> Markup {
|
||||||
fn render(&self) -> Markup {
|
|
||||||
match &self.source {
|
match &self.source {
|
||||||
Source::From(path) => html! {
|
Source::From(path) => html! {
|
||||||
link
|
link
|
||||||
|
@ -167,8 +171,8 @@ impl Render 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(_, code) => html! {
|
Source::Inline(_, f) => html! {
|
||||||
style { (PreEscaped(code)) };
|
style { (PreEscaped((f)(cx))) };
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(String::from("btn active")));
|
/// assert_eq!(classes.get(), Some("btn active".to_string()));
|
||||||
/// assert!(classes.contains("active"));
|
/// assert!(classes.contains("active"));
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
|
|
|
@ -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(String::from("¡Hola mundo!"))
|
/// Some("¡Hola mundo!".to_string())
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
/// // 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(String::from("Hello world!"))
|
/// Some("Hello world!".to_string())
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
/// // Uso típico en un atributo:
|
/// // Uso típico en un atributo:
|
||||||
|
|
|
@ -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_owned())
|
Some(value.to_string())
|
||||||
};
|
};
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,14 +258,29 @@ impl Context {
|
||||||
// Context RENDER ******************************************************************************
|
// Context RENDER ******************************************************************************
|
||||||
|
|
||||||
/// Renderiza los recursos del contexto.
|
/// Renderiza los recursos del contexto.
|
||||||
pub fn render_assets(&self) -> Markup {
|
pub fn render_assets(&mut self) -> Markup {
|
||||||
html! {
|
use std::mem::take as mem_take;
|
||||||
@if let Some(favicon) = &self.favicon {
|
|
||||||
(favicon)
|
// Extrae temporalmente los recursos.
|
||||||
}
|
let favicon = mem_take(&mut self.favicon); // Deja valor por defecto (None) en self.
|
||||||
(self.stylesheets)
|
let stylesheets = mem_take(&mut self.stylesheets); // Assets<StyleSheet>::default() en self.
|
||||||
(self.javascripts)
|
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))
|
||||||
|
(javascripts.render(self))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restaura los campos tal y como estaban.
|
||||||
|
self.favicon = favicon;
|
||||||
|
self.stylesheets = stylesheets;
|
||||||
|
self.javascripts = javascripts;
|
||||||
|
|
||||||
|
markup
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context PARAMS ******************************************************************************
|
// Context PARAMS ******************************************************************************
|
||||||
|
@ -285,7 +300,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", String::from("Hola"));
|
/// .with_param("titulo", "Hola".to_string());
|
||||||
///
|
///
|
||||||
/// 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();
|
||||||
|
@ -318,7 +333,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", String::from("Hola"));
|
/// .with_param("titulo", "Hola".to_string());
|
||||||
///
|
///
|
||||||
/// 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á
|
||||||
|
@ -416,7 +431,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", String::from("Hola"))
|
/// .with_param("titulo", "Hola".to_string())
|
||||||
/// .with_param("flags", vec!["a", "b"]);
|
/// .with_param("flags", vec!["a", "b"]);
|
||||||
/// ```
|
/// ```
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
@ -484,7 +499,7 @@ impl Contextual for Context {
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// let cx = Context::new(None).with_param("username", String::from("Alice"));
|
/// let cx = Context::new(None).with_param("username", "Alice".to_string());
|
||||||
///
|
///
|
||||||
/// // Devuelve Some(&String) si existe y coincide el tipo.
|
/// // 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"));
|
||||||
|
@ -533,7 +548,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_owned()
|
"prefix".to_string()
|
||||||
} else {
|
} else {
|
||||||
prefix
|
prefix
|
||||||
};
|
};
|
||||||
|
|
|
@ -69,23 +69,6 @@ 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 {
|
||||||
|
@ -238,6 +221,10 @@ 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> {
|
||||||
|
|
|
@ -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(String::from("ja-JP")));
|
/// assert_eq!(lang, LangMatch::Unsupported("ja-JP".to_string()));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Con la siguiente instrucción siempre se obtiene un [`LanguageIdentifier`] válido, ya sea porque
|
/// 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(String::from(language))
|
Self::Unsupported(language.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido.
|
/// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido.
|
||||||
|
|
|
@ -202,7 +202,7 @@ impl Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renderiza los recursos de la página.
|
/// Renderiza los recursos de la página.
|
||||||
pub fn render_assets(&self) -> Markup {
|
pub fn render_assets(&mut self) -> Markup {
|
||||||
self.context.render_assets()
|
self.context.render_assets()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
22
src/util.rs
22
src/util.rs
|
@ -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, String::from("Hello World"));
|
/// assert_eq!(result, "Hello World".to_string());
|
||||||
///
|
///
|
||||||
/// // 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, String::from("HelloWorld"));
|
/// assert_eq!(result_with_empty, "HelloWorld".to_string());
|
||||||
///
|
///
|
||||||
/// // 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, String::from("Hello"));
|
/// assert_eq!(single_result, "Hello".to_string());
|
||||||
/// ```
|
/// ```
|
||||||
#[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(String::from("Hello World")));
|
/// assert_eq!(result_with_separator, Some("Hello World".to_string()));
|
||||||
///
|
///
|
||||||
/// // 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(String::from("HelloWorld")));
|
/// assert_eq!(result_without_separator, Some("HelloWorld".to_string()));
|
||||||
///
|
///
|
||||||
/// // 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, String::from("Hello-World"));
|
/// assert_eq!(result, "Hello-World".to_string());
|
||||||
///
|
///
|
||||||
/// // 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, String::from("World"));
|
/// assert_eq!(result_empty_first, "World".to_string());
|
||||||
///
|
///
|
||||||
/// // 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, String::from("Hello"));
|
/// assert_eq!(result_empty_second, "Hello".to_string());
|
||||||
///
|
///
|
||||||
/// // 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, String::from(""));
|
/// assert_eq!(result_both_empty, "".to_string());
|
||||||
/// ```
|
/// ```
|
||||||
#[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(String::from("HelloWorld")));
|
/// assert_eq!(result, Some("HelloWorld".to_string()));
|
||||||
///
|
///
|
||||||
/// // 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(String::from("Hello World")));
|
/// assert_eq!(result_with_separator, Some("Hello World".to_string()));
|
||||||
///
|
///
|
||||||
/// // 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"]);
|
||||||
|
|
|
@ -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", String::from("Alice"));
|
let mut cx = Context::new(None).with_param("username", "Alice".to_string());
|
||||||
|
|
||||||
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();
|
||||||
|
|
|
@ -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.contains("poweredby__pagetop"));
|
assert!(html.as_str().contains("poweredby__pagetop"));
|
||||||
|
|
||||||
// Y NO debe mostrar el bloque de copyright.
|
// Y NO debe mostrar el bloque de copyright.
|
||||||
assert!(!html.contains("poweredby__copyright"));
|
assert!(!html.as_str().contains("poweredby__copyright"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
@ -22,17 +22,20 @@ 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!(html.contains(&year), "HTML should include the current year");
|
assert!(
|
||||||
|
html.as_str().contains(&year),
|
||||||
|
"HTML should include the current year"
|
||||||
|
);
|
||||||
|
|
||||||
// El nombre de la app proviene de `global::SETTINGS.app.name`.
|
// 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.contains(app_name),
|
html.as_str().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.contains("poweredby__copyright"));
|
assert!(html.as_str().contains("poweredby__copyright"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
@ -43,8 +46,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.contains(custom));
|
assert!(html.as_str().contains(custom));
|
||||||
assert!(html.contains("poweredby__copyright"));
|
assert!(html.as_str().contains("poweredby__copyright"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
@ -54,9 +57,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.contains("poweredby__copyright"));
|
assert!(!html.as_str().contains("poweredby__copyright"));
|
||||||
// El reconocimiento a PageTop siempre debe aparecer.
|
// El reconocimiento a PageTop siempre debe aparecer.
|
||||||
assert!(html.contains("poweredby__pagetop"));
|
assert!(html.as_str().contains("poweredby__pagetop"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
@ -67,7 +70,7 @@ async fn poweredby_link_points_to_crates_io() {
|
||||||
let html = render_component(&p);
|
let html = render_component(&p);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
html.contains("https://pagetop.cillero.es"),
|
html.as_str().contains("https://pagetop.cillero.es"),
|
||||||
"Link should point to pagetop.cillero.es"
|
"Link should point to pagetop.cillero.es"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -89,12 +92,8 @@ async fn poweredby_getter_reflects_internal_state() {
|
||||||
|
|
||||||
// HELPERS *****************************************************************************************
|
// HELPERS *****************************************************************************************
|
||||||
|
|
||||||
fn render(x: &impl Render) -> String {
|
fn render_component<C: Component>(c: &C) -> Markup {
|
||||||
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);
|
||||||
render(&pm)
|
pm.render()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!(render(&PrepareMarkup::None), "");
|
assert_eq!(PrepareMarkup::None.render().as_str(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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(String::from("<b>& \" ' </b>"));
|
let pm = PrepareMarkup::Escaped("<b>& \" ' </b>".to_string());
|
||||||
assert_eq!(render(&pm), "<b>& " ' </b>");
|
assert_eq!(pm.render().as_str(), "<b>& " ' </b>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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(String::from("<b>bold</b><script>1<2</script>"));
|
let pm = PrepareMarkup::Raw("<b>bold</b><script>1<2</script>".to_string());
|
||||||
assert_eq!(render(&pm), "<b>bold</b><script>1<2</script>");
|
assert_eq!(pm.render().as_str(), "<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!(
|
||||||
render(&pm),
|
pm.render().as_str(),
|
||||||
"<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) } };
|
let wrapped_escaped = html! { div { (escaped.render()) } };
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
wrapped_escaped.into_string(),
|
wrapped_escaped.into_string(),
|
||||||
"<div><i>x</i></div>"
|
"<div><i>x</i></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) } };
|
let wrapped_raw = html! { div { (raw.render()) } };
|
||||||
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) } };
|
let wrapped_with = html! { div { (with.render()) } };
|
||||||
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,11 +57,14 @@ 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!(render(&esc), "Hello, tomorrow coffee ☕ & donuts!");
|
assert_eq!(
|
||||||
|
esc.render().as_str(),
|
||||||
|
"Hello, tomorrow coffee ☕ & 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!(render(&raw), "Title — section © 2025");
|
assert_eq!(raw.render().as_str(), "Title — section © 2025");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
@ -69,11 +72,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(String::from("")).is_empty());
|
assert!(PrepareMarkup::Escaped("".to_string()).is_empty());
|
||||||
assert!(!PrepareMarkup::Escaped(String::from("x")).is_empty());
|
assert!(!PrepareMarkup::Escaped("x".to_string()).is_empty());
|
||||||
|
|
||||||
assert!(PrepareMarkup::Raw(String::new()).is_empty());
|
assert!(PrepareMarkup::Raw(String::new()).is_empty());
|
||||||
assert!(PrepareMarkup::Raw(String::from("")).is_empty());
|
assert!(PrepareMarkup::Raw("".to_string()).is_empty());
|
||||||
assert!(!PrepareMarkup::Raw("a".into()).is_empty());
|
assert!(!PrepareMarkup::Raw("a".into()).is_empty());
|
||||||
|
|
||||||
assert!(PrepareMarkup::With(html! {}).is_empty());
|
assert!(PrepareMarkup::With(html! {}).is_empty());
|
||||||
|
@ -94,17 +97,12 @@ async fn prepare_markup_equivalence_between_render_and_inline_in_html_macro() {
|
||||||
];
|
];
|
||||||
|
|
||||||
for pm in cases {
|
for pm in cases {
|
||||||
let rendered = render(&pm);
|
let rendered = pm.render();
|
||||||
let in_macro = html! { (pm) }.into_string();
|
let in_macro = html! { (rendered) }.into_string();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rendered, in_macro,
|
rendered.as_str(),
|
||||||
|
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()
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue