♻️ (pagetop): Sustituye StyleSheet::inline
- `StyleSheet::inline` se resuelve ahora con `ResponsiveStyles`. - `StyleSheet` se simplifica a sólo hojas de estilo externas. - `ResponsiveStyles` añade `add_styles()` para declarar varias propiedades en una sola llamada. - `AssetsOp` gana un constructor por variante y conversiones "From<Favicon/Preload/StyleSheet/JavaScript>" para pasarlos directamente a `with_assets()`.
This commit is contained in:
parent
0b8f3f3000
commit
64113aff09
21 changed files with 764 additions and 497 deletions
|
|
@ -17,7 +17,8 @@ impl Extension for IntroFlex {
|
|||
|
||||
async fn intro_flex(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_assets(AssetsOp::AddStyleSheet(demo_styles()))
|
||||
.with_assets(demo_box_styles())
|
||||
.with_assets(demo_row_styles())
|
||||
.with_child(
|
||||
Intro::default()
|
||||
.with_opening(IntroOpening::Custom)
|
||||
|
|
@ -478,32 +479,37 @@ fn other_block() -> Block {
|
|||
|
||||
// **< HELPERS >************************************************************************************
|
||||
|
||||
// Aspecto fijo de las cajas y filas de muestra.
|
||||
fn demo_styles() -> StyleSheet {
|
||||
StyleSheet::inline("intro-flex", |_| {
|
||||
util::indoc!(
|
||||
r#"
|
||||
.flex-demo-box {
|
||||
background-color: #0d6efd;
|
||||
color: #fff;
|
||||
min-width: 3rem;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
border-radius: 0.375rem;
|
||||
text-align: center;
|
||||
}
|
||||
.flex-demo-row {
|
||||
background-color: #f1f3f5;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0 0 1.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
"#
|
||||
)
|
||||
.to_string()
|
||||
})
|
||||
// Aspecto fijo de las cajas de muestra.
|
||||
fn demo_box_styles() -> AssetsOp {
|
||||
AssetsOp::add_responsive_styles(
|
||||
None,
|
||||
"flex-demo-box",
|
||||
[
|
||||
("background-color", "#0d6efd"),
|
||||
("color", "#fff"),
|
||||
("min-width", "3rem"),
|
||||
("width", "auto"),
|
||||
("max-width", "none"),
|
||||
("margin", "0"),
|
||||
("border-radius", "0.375rem"),
|
||||
("text-align", "center"),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// Aspecto fijo de las filas de muestra.
|
||||
fn demo_row_styles() -> AssetsOp {
|
||||
AssetsOp::add_responsive_styles(
|
||||
None,
|
||||
"flex-demo-row",
|
||||
[
|
||||
("background-color", "#f1f3f5"),
|
||||
("width", "100%"),
|
||||
("max-width", "none"),
|
||||
("margin", "0 0 1.5rem"),
|
||||
("padding", "0.75rem"),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// Caja con fondo azul y relleno vertical configurable, para mostrar diferencias de altura.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ impl Extension for IntroResponsive {
|
|||
|
||||
async fn intro_responsive(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_assets(AssetsOp::AddStyleSheet(demo_styles()))
|
||||
.with_assets(demo_box_styles())
|
||||
.with_assets(demo_row_styles())
|
||||
.with_assets(demo_code_styles())
|
||||
.with_child(
|
||||
Intro::default()
|
||||
.with_opening(IntroOpening::Custom)
|
||||
|
|
@ -189,35 +191,42 @@ fn gap_grow_block() -> Block {
|
|||
|
||||
// **< HELPERS >************************************************************************************
|
||||
|
||||
// Aspecto fijo de las cajas y filas de muestra.
|
||||
fn demo_styles() -> StyleSheet {
|
||||
StyleSheet::inline("intro-responsive", |_| {
|
||||
util::indoc!(
|
||||
r#"
|
||||
.flex-demo-box {
|
||||
background-color: #0d6efd;
|
||||
color: #fff;
|
||||
min-width: 3rem;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
border-radius: 0.375rem;
|
||||
text-align: center;
|
||||
}
|
||||
.flex-demo-row {
|
||||
background-color: #f1f3f5;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0 0 1.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
code {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
"#
|
||||
)
|
||||
.to_string()
|
||||
})
|
||||
// Aspecto fijo de las cajas de muestra.
|
||||
fn demo_box_styles() -> AssetsOp {
|
||||
AssetsOp::add_responsive_styles(
|
||||
None,
|
||||
"flex-demo-box",
|
||||
[
|
||||
("background-color", "#0d6efd"),
|
||||
("color", "#fff"),
|
||||
("min-width", "3rem"),
|
||||
("width", "auto"),
|
||||
("max-width", "none"),
|
||||
("margin", "0"),
|
||||
("border-radius", "0.375rem"),
|
||||
("text-align", "center"),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// Aspecto fijo de las filas de muestra.
|
||||
fn demo_row_styles() -> AssetsOp {
|
||||
AssetsOp::add_responsive_styles(
|
||||
None,
|
||||
"flex-demo-row",
|
||||
[
|
||||
("background-color", "#f1f3f5"),
|
||||
("width", "100%"),
|
||||
("max-width", "none"),
|
||||
("margin", "0 0 1.5rem"),
|
||||
("padding", "0.75rem"),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// Evita que los fragmentos de código largos desborden su contenedor.
|
||||
fn demo_code_styles() -> AssetsOp {
|
||||
AssetsOp::add_responsive_styles(None, "flex-demo-code", [("overflow-wrap", "anywhere")])
|
||||
}
|
||||
|
||||
// Caja con fondo azul y relleno vertical configurable, para mostrar diferencias de altura.
|
||||
|
|
@ -250,7 +259,7 @@ fn caption(title: Lc, code: Lc) -> Html {
|
|||
Html::with(move |cx| {
|
||||
html! {
|
||||
h3 { (title.using(cx)) }
|
||||
p { code { (code.using(cx)) } }
|
||||
p { code class="flex-demo-code" { (code.using(cx)) } }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,21 +113,21 @@ impl Extension for Aliner {
|
|||
#[async_trait]
|
||||
impl Theme for Aliner {
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||
page.alter_assets(
|
||||
StyleSheet::from("/pagetop/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
)
|
||||
.alter_assets(
|
||||
StyleSheet::from("/pagetop/css/basic.css")
|
||||
.with_version(PAGETOP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
)
|
||||
.alter_assets(
|
||||
StyleSheet::from("/aliner/css/styles.css")
|
||||
.with_version(env!("CARGO_PKG_VERSION"))
|
||||
.with_weight(-99),
|
||||
))
|
||||
)
|
||||
.alter_child_in(
|
||||
&CoreRegions::Footer,
|
||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||
|
|
|
|||
|
|
@ -178,42 +178,40 @@ impl Theme for Bootsier {
|
|||
|
||||
// Las URLs de las fuentes deben coincidir exactamente con las declaradas en @font-face de
|
||||
// _bootsier-custom.scss; cualquier discrepancia hace que el navegador descargue dos veces.
|
||||
page.alter_assets(AssetsOp::AddPreload(
|
||||
Preload::font("/bootsier/fonts/bootsier.font.woff2").with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddPreload(
|
||||
Preload::font("/bootsier/fonts/bootsier.font.italic.woff2").with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/bootsier/css/bootsier.min.css")
|
||||
.with_version(ADMINLTE_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootsier.bundle.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootsier.extended.min.js")
|
||||
.with_version(ADMINLTE_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootsier.dialog.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootsier.confirm.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_body_props(PropsOp::set("data-confirm-ok", confirm_ok))
|
||||
.alter_body_props(PropsOp::set("data-confirm-cancel", confirm_cancel))
|
||||
.alter_child_in(
|
||||
&CoreRegions::Footer,
|
||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||
);
|
||||
page.alter_assets(Preload::font("/bootsier/fonts/bootsier.font.woff2").with_weight(-99))
|
||||
.alter_assets(
|
||||
Preload::font("/bootsier/fonts/bootsier.font.italic.woff2").with_weight(-99),
|
||||
)
|
||||
.alter_assets(
|
||||
StyleSheet::from("/bootsier/css/bootsier.min.css")
|
||||
.with_version(ADMINLTE_VERSION)
|
||||
.with_weight(-99),
|
||||
)
|
||||
.alter_assets(
|
||||
JavaScript::defer("/bootsier/js/bootsier.bundle.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-99),
|
||||
)
|
||||
.alter_assets(
|
||||
JavaScript::defer("/bootsier/js/bootsier.extended.min.js")
|
||||
.with_version(ADMINLTE_VERSION)
|
||||
.with_weight(-99),
|
||||
)
|
||||
.alter_assets(
|
||||
JavaScript::defer("/bootsier/js/bootsier.dialog.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-99),
|
||||
)
|
||||
.alter_assets(
|
||||
JavaScript::defer("/bootsier/js/bootsier.confirm.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-99),
|
||||
)
|
||||
.alter_body_props(PropsOp::set("data-confirm-ok", confirm_ok))
|
||||
.alter_body_props(PropsOp::set("data-confirm-cancel", confirm_cancel))
|
||||
.alter_child_in(
|
||||
&CoreRegions::Footer,
|
||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ async fn render_admin(cx: &mut Context) -> Markup {
|
|||
cx.alter_body_props(PropsOp::add_classes(
|
||||
"layout-fixed sidebar-expand-lg bg-body-tertiary",
|
||||
));
|
||||
cx.alter_assets(AssetsOp::AddJavaScript(
|
||||
cx.alter_assets(
|
||||
JavaScript::defer("/bootsier/js/bootsier.shell.min.js")
|
||||
.with_version(ADMINLTE_VERSION)
|
||||
.with_weight(-88),
|
||||
));
|
||||
);
|
||||
// `CoreRegions::Aside` es una región neutra del core: la usa `pagetop-admin` para su menú de
|
||||
// secciones sin que este tema tenga que depender de él. `BootsierRegions::Sidebar` sigue
|
||||
// disponible para que cualquier extensión añada elementos propios a mano.
|
||||
|
|
|
|||
|
|
@ -154,7 +154,5 @@ impl Extension for Htmx {
|
|||
}
|
||||
|
||||
fn add_htmx_script(page: &mut Page) {
|
||||
page.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/htmx/js/htmx.min.js").with_version("2.0.10"),
|
||||
));
|
||||
page.alter_assets(JavaScript::defer("/htmx/js/htmx.min.js").with_version("2.0.10"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,11 +114,9 @@ impl Component for Intro {
|
|||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
cx.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION),
|
||||
));
|
||||
cx.alter_assets(StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION));
|
||||
if *self.opening() == IntroOpening::PageTop {
|
||||
cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
||||
cx.alter_assets(JavaScript::on_load_async("intro-js", |cx|
|
||||
util::indoc!(r#"
|
||||
try {
|
||||
const resp = await fetch("https://crates.io/api/v1/crates/pagetop");
|
||||
|
|
@ -134,7 +132,7 @@ impl Component for Intro {
|
|||
"#)
|
||||
.replace("LANGID", cx.langid().to_string().as_str())
|
||||
.replace("LABEL", Lc::l("intro_release_label").using(cx).as_str())
|
||||
)));
|
||||
));
|
||||
}
|
||||
|
||||
Ok(html! {
|
||||
|
|
|
|||
|
|
@ -14,28 +14,26 @@ impl Extension for Basic {
|
|||
#[async_trait]
|
||||
impl Theme for Basic {
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||
page.alter_assets(
|
||||
StyleSheet::from("/pagetop/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
)
|
||||
.alter_assets(
|
||||
StyleSheet::from("/pagetop/css/basic.min.css")
|
||||
.with_version(PAGETOP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/pagetop/js/basic.menu.min.js").with_version("4.4.0"),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
)
|
||||
.alter_assets(JavaScript::defer("/pagetop/js/basic.menu.min.js").with_version("4.4.0"))
|
||||
.alter_assets(
|
||||
JavaScript::defer("/pagetop/js/basic.dropdown.min.js").with_version(PAGETOP_VERSION),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
)
|
||||
.alter_assets(
|
||||
JavaScript::defer("/pagetop/js/basic.navbar.init.js").with_version(PAGETOP_VERSION),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
)
|
||||
.alter_assets(
|
||||
JavaScript::defer("/pagetop/js/basic.dialog.min.js").with_version(PAGETOP_VERSION),
|
||||
))
|
||||
)
|
||||
.alter_child_in(
|
||||
&CoreRegions::Footer,
|
||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||
|
|
|
|||
|
|
@ -2,246 +2,28 @@ use crate::auth::CurrentUser;
|
|||
use crate::core::TypeInfo;
|
||||
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
|
||||
use crate::core::theme::all::DEFAULT_THEME;
|
||||
use crate::core::theme::{Breakpoint, ChildrenInRegions, CoreRegions, CoreTemplates};
|
||||
use crate::core::theme::{ChildrenInRegions, CoreRegions, CoreTemplates};
|
||||
use crate::core::theme::{RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::html::{Assets, Favicon, JavaScript, Preload, ResponsiveStyles, StyleSheet};
|
||||
use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
||||
use crate::locale::Lc;
|
||||
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||
use crate::web::HttpRequest;
|
||||
use crate::{CowStr, builder_impl, util};
|
||||
use crate::{builder_impl, util};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use thiserror::Error;
|
||||
|
||||
use std::any::{Any, TypeId};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Operaciones para modificar recursos asociados al [`Context`] de un documento.
|
||||
pub enum AssetsOp {
|
||||
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
|
||||
SetFavicon(Option<Favicon>),
|
||||
/// Define el *favicon* solo si no se ha establecido previamente.
|
||||
SetFaviconIfNone(Favicon),
|
||||
mod assets_op;
|
||||
pub use assets_op::AssetsOp;
|
||||
|
||||
/// Añade un recurso para precarga al documento.
|
||||
AddPreload(Preload),
|
||||
/// Elimina un recurso para precarga por su ruta.
|
||||
RemovePreload(&'static str),
|
||||
mod error;
|
||||
pub use error::ContextError;
|
||||
|
||||
/// Añade una hoja de estilos CSS al documento.
|
||||
AddStyleSheet(StyleSheet),
|
||||
/// Elimina una hoja de estilos por su ruta o identificador.
|
||||
RemoveStyleSheet(&'static str),
|
||||
|
||||
/// Añade un script JavaScript al documento.
|
||||
AddJavaScript(JavaScript),
|
||||
/// Elimina un script por su ruta o identificador.
|
||||
RemoveJavaScript(&'static str),
|
||||
|
||||
/// Añade una declaración de estilo responsive (`property: value`) para las clases indicadas,
|
||||
/// dentro del punto de corte dado (`None` para una regla siempre activa). Ver
|
||||
/// [`ResponsiveStyles::add_style()`].
|
||||
AddResponsiveStyle(Option<Breakpoint>, CowStr, CowStr, CowStr),
|
||||
}
|
||||
|
||||
/// Errores de acceso a parámetros dinámicos del contexto.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ContextError {
|
||||
/// La clave no existe.
|
||||
#[error("parameter not found")]
|
||||
ParamNotFound,
|
||||
/// La clave existe, pero el valor guardado no coincide con el tipo solicitado. Incluye
|
||||
/// nombre de la clave (`key`), tipo esperado (`expected`) y tipo realmente guardado (`saved`)
|
||||
/// para facilitar el diagnóstico.
|
||||
#[error("type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\"")]
|
||||
ParamTypeMismatch {
|
||||
key: &'static str,
|
||||
expected: &'static str,
|
||||
saved: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
|
||||
///
|
||||
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
|
||||
///
|
||||
/// - Almacenar la **petición HTTP** de origen.
|
||||
/// - Seleccionar la **plantilla** y el **tema** de renderizado.
|
||||
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
|
||||
/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`].
|
||||
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
|
||||
///
|
||||
/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como
|
||||
/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::Page).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_aliner::Aliner;
|
||||
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
||||
/// cx.with_langid(&Locale::resolve("es-ES"))
|
||||
/// .with_template(&CoreTemplates::Standard)
|
||||
/// .with_theme(&Aliner)
|
||||
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
||||
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
||||
/// .with_param("user_id", 42_i32)
|
||||
/// }
|
||||
/// ```
|
||||
#[builder_impl]
|
||||
pub trait Contextual: LangId {
|
||||
// **< Contextual BUILDER >*********************************************************************
|
||||
|
||||
/// Establece el idioma del documento.
|
||||
fn with_langid(self, language: &impl LangId) -> Self;
|
||||
|
||||
/// Almacena la petición HTTP de origen en el contexto.
|
||||
///
|
||||
/// También recalcula el idioma ([`RequestLocale::from_request()`]) y
|
||||
/// [`current_user()`](Self::current_user) a partir de la petición indicada, descartando
|
||||
/// cualquier idioma forzado antes con [`with_langid()`](Self::with_langid) o el usuario ya
|
||||
/// resuelto. Si necesitas forzar el idioma o el usuario, llama a `with_request()` primero en
|
||||
/// la cadena de construcción, nunca después.
|
||||
fn with_request(self, request: Option<HttpRequest>) -> Self;
|
||||
|
||||
/// Especifica la plantilla para renderizar el documento.
|
||||
fn with_template(self, template: TemplateRef) -> Self;
|
||||
|
||||
/// Especifica el tema para renderizar el documento.
|
||||
fn with_theme(self, theme: ThemeRef) -> Self;
|
||||
|
||||
/// Añade o modifica un parámetro dinámico del contexto.
|
||||
///
|
||||
/// El valor se almacena junto con el nombre de su tipo, lo que permite generar mensajes de
|
||||
/// error precisos al recuperarlo con [`param`](Contextual::param) si el tipo solicitado no
|
||||
/// coincide.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let cx = Context::default()
|
||||
/// .with_param("user_id", 42_i32)
|
||||
/// .with_param("title", "Hello".to_string())
|
||||
/// .with_param("flags", vec!["a", "b"]);
|
||||
/// ```
|
||||
fn with_param<T: Send + Sync + 'static>(self, key: &'static str, value: T) -> Self;
|
||||
|
||||
/// Define los recursos del contexto usando [`AssetsOp`].
|
||||
fn with_assets(self, op: AssetsOp) -> Self;
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del elemento `<body>`.
|
||||
fn with_body_props(self, op: PropsOp) -> Self;
|
||||
|
||||
/// Añade un componente o aplica una operación [`ChildOp`] en la región por defecto del
|
||||
/// documento.
|
||||
fn with_child(self, op: impl Into<ChildOp>) -> Self;
|
||||
|
||||
/// Añade un componente o aplica una operación [`ChildOp`] en una región específica del
|
||||
/// documento.
|
||||
fn with_child_in(self, region: RegionRef, op: impl Into<ChildOp>) -> Self;
|
||||
|
||||
// **< Contextual GETTERS >*********************************************************************
|
||||
|
||||
/// Devuelve una referencia a la petición HTTP asociada, si existe.
|
||||
fn request(&self) -> Option<&HttpRequest>;
|
||||
|
||||
/// Devuelve la identidad del usuario actual.
|
||||
///
|
||||
/// Si ninguna extensión de autenticación ha inyectado un
|
||||
/// [`CurrentUser`](crate::auth::CurrentUser) en las extensiones de la petición HTTP, devuelve
|
||||
/// `&CurrentUser::Anonymous`.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// async fn greet(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
/// let mut page = Page::new(request);
|
||||
/// if page.current_user().is_authenticated() {
|
||||
/// // Personalizar la página para el usuario autenticado.
|
||||
/// }
|
||||
/// page.render().await
|
||||
/// }
|
||||
/// ```
|
||||
fn current_user(&self) -> &CurrentUser;
|
||||
|
||||
/// Devuelve la plantilla configurada para renderizar el documento.
|
||||
fn template(&self) -> TemplateRef;
|
||||
|
||||
/// Devuelve el tema que se usará para renderizar el documento.
|
||||
fn theme(&self) -> ThemeRef;
|
||||
|
||||
/// Recupera una *referencia tipada* al parámetro solicitado.
|
||||
///
|
||||
/// Devuelve:
|
||||
///
|
||||
/// - `Ok(&T)` si la clave existe y el tipo coincide.
|
||||
/// - `Err(ContextError::ParamNotFound)` si la clave no existe.
|
||||
/// - `Err(ContextError::ParamTypeMismatch)` si la clave existe pero el tipo no coincide.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let cx = Context::default()
|
||||
/// .with_param("user_id", 42_i32)
|
||||
/// .with_param("title", "Hello".to_string());
|
||||
///
|
||||
/// let id: i32 = *cx.param("user_id").unwrap();
|
||||
/// let title: &String = cx.param("title").unwrap();
|
||||
///
|
||||
/// // Error de tipo:
|
||||
/// assert!(cx.param::<String>("user_id").is_err());
|
||||
/// ```
|
||||
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError>;
|
||||
|
||||
/// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
|
||||
fn param_or_default<T: Clone + Default + 'static>(&self, key: &'static str) -> T {
|
||||
self.param::<T>(key).ok().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Devuelve el parámetro clonado o un **valor por defecto** si no existe.
|
||||
fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
|
||||
self.param::<T>(key).ok().cloned().unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Devuelve el parámetro clonado o el **valor evaluado** por la función `f` si no existe.
|
||||
fn param_or_else<T: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T {
|
||||
self.param::<T>(key).ok().cloned().unwrap_or_else(f)
|
||||
}
|
||||
|
||||
/// Devuelve el Favicon de los recursos del contexto.
|
||||
fn favicon(&self) -> Option<&Favicon>;
|
||||
|
||||
/// Devuelve las hojas de estilo de los recursos del contexto.
|
||||
fn stylesheets(&self) -> &Assets<StyleSheet>;
|
||||
|
||||
/// Devuelve los scripts JavaScript de los recursos del contexto.
|
||||
fn javascripts(&self) -> &Assets<JavaScript>;
|
||||
|
||||
/// Devuelve los estilos *responsive* acumulados en el contexto.
|
||||
fn responsive_styles(&self) -> &ResponsiveStyles;
|
||||
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento `<body>`.
|
||||
fn body_props(&self) -> &Props;
|
||||
|
||||
// **< Contextual HELPERS >*********************************************************************
|
||||
|
||||
/// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let mut cx = Context::default().with_param("temp", 1u8);
|
||||
/// assert!(cx.remove_param("temp"));
|
||||
/// assert!(!cx.remove_param("temp")); // ya no existe
|
||||
/// ```
|
||||
fn remove_param(&mut self, key: &'static str) -> bool;
|
||||
}
|
||||
mod contextual;
|
||||
pub use contextual::Contextual;
|
||||
|
||||
/// Implementa un **contexto de renderizado** para un documento HTML.
|
||||
///
|
||||
|
|
@ -280,11 +62,11 @@ pub trait Contextual: LangId {
|
|||
/// // Establece el tema para renderizar.
|
||||
/// .with_theme(&Aliner)
|
||||
/// // Asigna un favicon.
|
||||
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||
/// .with_assets(Favicon::new().with_icon("/favicon.ico"))
|
||||
/// // Añade una hoja de estilo externa.
|
||||
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/style.css")))
|
||||
/// .with_assets(StyleSheet::from("/css/style.css"))
|
||||
/// // Añade un script JavaScript.
|
||||
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js")))
|
||||
/// .with_assets(JavaScript::defer("/js/main.js"))
|
||||
/// // Añade un parámetro dinámico al contexto.
|
||||
/// .with_param("user_id", 42);
|
||||
/// # cx }
|
||||
|
|
@ -586,8 +368,8 @@ impl Contextual for Context {
|
|||
self
|
||||
}
|
||||
|
||||
fn with_assets(mut self, op: AssetsOp) -> Self {
|
||||
match op {
|
||||
fn with_assets(mut self, op: impl Into<AssetsOp>) -> Self {
|
||||
match op.into() {
|
||||
// Favicon.
|
||||
AssetsOp::SetFavicon(favicon) => {
|
||||
self.favicon = favicon;
|
||||
|
|
@ -623,6 +405,9 @@ impl Contextual for Context {
|
|||
self.responsives
|
||||
.add_style(breakpoint, classes, property, value);
|
||||
}
|
||||
AssetsOp::AddResponsiveStyles(breakpoint, classes, styles) => {
|
||||
self.responsives.add_styles(breakpoint, classes, styles);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
|
|||
169
src/core/component/context/assets_op.rs
Normal file
169
src/core/component/context/assets_op.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
use crate::CowStr;
|
||||
use crate::core::theme::Breakpoint;
|
||||
use crate::html::{Favicon, JavaScript, Preload, StyleSheet};
|
||||
|
||||
/// Operaciones para modificar recursos asociados al [`Context`](super::Context) de un documento.
|
||||
///
|
||||
/// [`Favicon`], [`Preload`], [`StyleSheet`] y [`JavaScript`] se convierten implícitamente en la
|
||||
/// operación de añadir correspondiente (ver sus `impl From<...>` más abajo), por lo que no
|
||||
/// necesitarían ningún constructor. Para el resto de operaciones, el método recomendado es recurrir
|
||||
/// a los constructores asociados como [`remove_stylesheet()`], [`add_responsive_style()`], etc.
|
||||
///
|
||||
/// [`remove_stylesheet()`]: Self::remove_stylesheet
|
||||
/// [`add_responsive_style()`]: Self::add_responsive_style
|
||||
pub enum AssetsOp {
|
||||
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
|
||||
SetFavicon(Option<Favicon>),
|
||||
/// Define el *favicon* sólo si no se ha establecido previamente.
|
||||
SetFaviconIfNone(Favicon),
|
||||
|
||||
/// Añade un recurso para precarga al documento.
|
||||
AddPreload(Preload),
|
||||
/// Elimina un recurso para precarga por su ruta.
|
||||
RemovePreload(&'static str),
|
||||
|
||||
/// Añade una hoja de estilos CSS al documento.
|
||||
AddStyleSheet(StyleSheet),
|
||||
/// Elimina una hoja de estilos por su ruta.
|
||||
RemoveStyleSheet(&'static str),
|
||||
|
||||
/// Añade un script JavaScript al documento.
|
||||
AddJavaScript(JavaScript),
|
||||
/// Elimina un script por su ruta o identificador.
|
||||
RemoveJavaScript(&'static str),
|
||||
|
||||
/// Añade una declaración de estilo *responsive* (`property: value`) para las clases indicadas,
|
||||
/// para un punto de corte dado (`None` para una regla siempre activa). Ver
|
||||
/// [`ResponsiveStyles::add_style()`](crate::html::ResponsiveStyles::add_style).
|
||||
AddResponsiveStyle(Option<Breakpoint>, CowStr, CowStr, CowStr),
|
||||
/// Añade varias declaraciones de estilo *responsive* (`property: value`) para las clases
|
||||
/// indicadas, para un punto de corte dado, en una única llamada. Ver
|
||||
/// [`ResponsiveStyles::add_styles()`](crate::html::ResponsiveStyles::add_styles).
|
||||
AddResponsiveStyles(Option<Breakpoint>, CowStr, Vec<(CowStr, CowStr)>),
|
||||
}
|
||||
|
||||
impl AssetsOp {
|
||||
/// Crea la variante [`SetFavicon`](Self::SetFavicon) con el favicon indicado, o `None` para
|
||||
/// eliminar cualquier favicon ya establecido.
|
||||
pub fn set_favicon(favicon: impl Into<Option<Favicon>>) -> Self {
|
||||
Self::SetFavicon(favicon.into())
|
||||
}
|
||||
|
||||
/// Crea la variante [`SetFaviconIfNone`](Self::SetFaviconIfNone) con el favicon indicado.
|
||||
pub fn set_favicon_if_none(favicon: Favicon) -> Self {
|
||||
Self::SetFaviconIfNone(favicon)
|
||||
}
|
||||
|
||||
/// Crea la variante [`AddPreload`](Self::AddPreload) con el recurso indicado.
|
||||
pub fn add_preload(preload: Preload) -> Self {
|
||||
Self::AddPreload(preload)
|
||||
}
|
||||
|
||||
/// Crea la variante [`RemovePreload`](Self::RemovePreload) para la ruta indicada.
|
||||
pub fn remove_preload(path: &'static str) -> Self {
|
||||
Self::RemovePreload(path)
|
||||
}
|
||||
|
||||
/// Crea la variante [`AddStyleSheet`](Self::AddStyleSheet) con la hoja de estilos indicada.
|
||||
pub fn add_stylesheet(stylesheet: StyleSheet) -> Self {
|
||||
Self::AddStyleSheet(stylesheet)
|
||||
}
|
||||
|
||||
/// Crea la variante [`RemoveStyleSheet`](Self::RemoveStyleSheet) para la ruta indicada.
|
||||
pub fn remove_stylesheet(path: &'static str) -> Self {
|
||||
Self::RemoveStyleSheet(path)
|
||||
}
|
||||
|
||||
/// Crea la variante [`AddJavaScript`](Self::AddJavaScript) con el script indicado.
|
||||
pub fn add_javascript(js: JavaScript) -> Self {
|
||||
Self::AddJavaScript(js)
|
||||
}
|
||||
|
||||
/// Crea la variante [`RemoveJavaScript`](Self::RemoveJavaScript) para la ruta o identificador
|
||||
/// indicado.
|
||||
pub fn remove_javascript(path: &'static str) -> Self {
|
||||
Self::RemoveJavaScript(path)
|
||||
}
|
||||
|
||||
/// Crea la variante [`AddResponsiveStyle`](Self::AddResponsiveStyle) con la declaración de
|
||||
/// estilo (`property: value`) indicada, para las clases y el punto de corte dados.
|
||||
pub fn add_responsive_style(
|
||||
breakpoint: impl Into<Option<Breakpoint>>,
|
||||
classes: impl Into<CowStr>,
|
||||
property: impl Into<CowStr>,
|
||||
value: impl Into<CowStr>,
|
||||
) -> Self {
|
||||
Self::AddResponsiveStyle(
|
||||
breakpoint.into(),
|
||||
classes.into(),
|
||||
property.into(),
|
||||
value.into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Crea la variante [`AddResponsiveStyles`](Self::AddResponsiveStyles) con las declaraciones
|
||||
/// de estilo (`property: value`) indicadas, para las clases y el punto de corte dados.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let op = AssetsOp::add_responsive_styles(
|
||||
/// None,
|
||||
/// "flex-demo-box",
|
||||
/// [("background-color", "#0d6efd"), ("color", "#fff")],
|
||||
/// );
|
||||
/// ```
|
||||
pub fn add_responsive_styles(
|
||||
breakpoint: impl Into<Option<Breakpoint>>,
|
||||
classes: impl Into<CowStr>,
|
||||
styles: impl IntoIterator<Item = (impl Into<CowStr>, impl Into<CowStr>)>,
|
||||
) -> Self {
|
||||
Self::AddResponsiveStyles(
|
||||
breakpoint.into(),
|
||||
classes.into(),
|
||||
styles
|
||||
.into_iter()
|
||||
.map(|(p, v)| (p.into(), v.into()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Favicon> for AssetsOp {
|
||||
/// Convierte un favicon en [`AssetsOp::SetFavicon`] (lo sobrescribe siempre), permitiendo
|
||||
/// pasarlo directamente a métodos como [`Contextual::with_assets`] sin envolverlo
|
||||
/// explícitamente. Para establecerlo sólo si no hay uno ya definido, usar
|
||||
/// [`AssetsOp::set_favicon_if_none()`] explícitamente.
|
||||
///
|
||||
/// [`Contextual::with_assets`]: crate::core::component::Contextual::with_assets
|
||||
#[inline]
|
||||
fn from(favicon: Favicon) -> Self {
|
||||
Self::SetFavicon(Some(favicon))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Preload> for AssetsOp {
|
||||
/// Convierte un recurso de precarga en [`AssetsOp::AddPreload`]. Ver la conversión
|
||||
/// equivalente para [`Favicon`].
|
||||
#[inline]
|
||||
fn from(preload: Preload) -> Self {
|
||||
Self::AddPreload(preload)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StyleSheet> for AssetsOp {
|
||||
/// Convierte una hoja de estilos en [`AssetsOp::AddStyleSheet`]. Ver la conversión
|
||||
/// equivalente para [`Favicon`].
|
||||
#[inline]
|
||||
fn from(stylesheet: StyleSheet) -> Self {
|
||||
Self::AddStyleSheet(stylesheet)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JavaScript> for AssetsOp {
|
||||
/// Convierte un script en [`AssetsOp::AddJavaScript`]. Ver la conversión equivalente para
|
||||
/// [`Favicon`].
|
||||
#[inline]
|
||||
fn from(js: JavaScript) -> Self {
|
||||
Self::AddJavaScript(js)
|
||||
}
|
||||
}
|
||||
195
src/core/component/context/contextual.rs
Normal file
195
src/core/component/context/contextual.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
use super::{AssetsOp, ContextError};
|
||||
|
||||
use crate::auth::CurrentUser;
|
||||
use crate::builder_impl;
|
||||
use crate::core::component::ChildOp;
|
||||
use crate::core::theme::{RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::html::{Assets, Favicon, JavaScript, Props, PropsOp, ResponsiveStyles, StyleSheet};
|
||||
use crate::locale::LangId;
|
||||
use crate::web::HttpRequest;
|
||||
|
||||
/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
|
||||
///
|
||||
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
|
||||
///
|
||||
/// - Almacenar la **petición HTTP** de origen.
|
||||
/// - Seleccionar la **plantilla** y el **tema** de renderizado.
|
||||
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
|
||||
/// [`StyleSheet`] o los scripts [`JavaScript`], directamente o mediante una operación
|
||||
/// [`AssetsOp`].
|
||||
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
|
||||
///
|
||||
/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como
|
||||
/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::Page).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_aliner::Aliner;
|
||||
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
||||
/// cx.with_langid(&Locale::resolve("es-ES"))
|
||||
/// .with_template(&CoreTemplates::Standard)
|
||||
/// .with_theme(&Aliner)
|
||||
/// .with_assets(Favicon::new().with_icon("/favicon.ico"))
|
||||
/// .with_assets(StyleSheet::from("/css/app.css"))
|
||||
/// .with_assets(JavaScript::defer("/js/app.js"))
|
||||
/// .with_param("user_id", 42_i32)
|
||||
/// }
|
||||
/// ```
|
||||
#[builder_impl]
|
||||
pub trait Contextual: LangId {
|
||||
// **< Contextual BUILDER >*********************************************************************
|
||||
|
||||
/// Establece el idioma del documento.
|
||||
fn with_langid(self, language: &impl LangId) -> Self;
|
||||
|
||||
/// Almacena la petición HTTP de origen en el contexto.
|
||||
///
|
||||
/// También recalcula el idioma ([`RequestLocale::from_request()`]) y
|
||||
/// [`current_user()`](Self::current_user) a partir de la petición indicada, descartando
|
||||
/// cualquier idioma forzado antes con [`with_langid()`](Self::with_langid) o el usuario ya
|
||||
/// resuelto. Si necesitas forzar el idioma o el usuario, llama a `with_request()` primero en
|
||||
/// la cadena de construcción, nunca después.
|
||||
///
|
||||
/// [`RequestLocale::from_request()`]: crate::locale::RequestLocale::from_request
|
||||
fn with_request(self, request: Option<HttpRequest>) -> Self;
|
||||
|
||||
/// Especifica la plantilla para renderizar el documento.
|
||||
fn with_template(self, template: TemplateRef) -> Self;
|
||||
|
||||
/// Especifica el tema para renderizar el documento.
|
||||
fn with_theme(self, theme: ThemeRef) -> Self;
|
||||
|
||||
/// Añade o modifica un parámetro dinámico del contexto.
|
||||
///
|
||||
/// El valor se almacena junto con el nombre de su tipo, lo que permite generar mensajes de
|
||||
/// error precisos al recuperarlo con [`param`](Contextual::param) si el tipo solicitado no
|
||||
/// coincide.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let cx = Context::default()
|
||||
/// .with_param("user_id", 42_i32)
|
||||
/// .with_param("title", "Hello".to_string())
|
||||
/// .with_param("flags", vec!["a", "b"]);
|
||||
/// ```
|
||||
fn with_param<T: Send + Sync + 'static>(self, key: &'static str, value: T) -> Self;
|
||||
|
||||
/// Añade un recurso ([`Favicon`], [`StyleSheet`], [`JavaScript`] o
|
||||
/// [`Preload`](crate::html::Preload)) directamente, o aplica una operación [`AssetsOp`] sobre
|
||||
/// los recursos del contexto.
|
||||
fn with_assets(self, op: impl Into<AssetsOp>) -> Self;
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del elemento `<body>`.
|
||||
fn with_body_props(self, op: PropsOp) -> Self;
|
||||
|
||||
/// Añade un componente o aplica una operación [`ChildOp`] en la región por defecto del
|
||||
/// documento.
|
||||
fn with_child(self, op: impl Into<ChildOp>) -> Self;
|
||||
|
||||
/// Añade un componente o aplica una operación [`ChildOp`] en una región específica del
|
||||
/// documento.
|
||||
fn with_child_in(self, region: RegionRef, op: impl Into<ChildOp>) -> Self;
|
||||
|
||||
// **< Contextual GETTERS >*********************************************************************
|
||||
|
||||
/// Devuelve una referencia a la petición HTTP asociada, si existe.
|
||||
fn request(&self) -> Option<&HttpRequest>;
|
||||
|
||||
/// Devuelve la identidad del usuario actual.
|
||||
///
|
||||
/// Si ninguna extensión de autenticación ha inyectado un
|
||||
/// [`CurrentUser`](crate::auth::CurrentUser) en las extensiones de la petición HTTP, devuelve
|
||||
/// `&CurrentUser::Anonymous`.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// async fn greet(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
/// let mut page = Page::new(request);
|
||||
/// if page.current_user().is_authenticated() {
|
||||
/// // Personalizar la página para el usuario autenticado.
|
||||
/// }
|
||||
/// page.render().await
|
||||
/// }
|
||||
/// ```
|
||||
fn current_user(&self) -> &CurrentUser;
|
||||
|
||||
/// Devuelve la plantilla configurada para renderizar el documento.
|
||||
fn template(&self) -> TemplateRef;
|
||||
|
||||
/// Devuelve el tema que se usará para renderizar el documento.
|
||||
fn theme(&self) -> ThemeRef;
|
||||
|
||||
/// Recupera una *referencia tipada* al parámetro solicitado.
|
||||
///
|
||||
/// Devuelve:
|
||||
///
|
||||
/// - `Ok(&T)` si la clave existe y el tipo coincide.
|
||||
/// - `Err(ContextError::ParamNotFound)` si la clave no existe.
|
||||
/// - `Err(ContextError::ParamTypeMismatch)` si la clave existe pero el tipo no coincide.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let cx = Context::default()
|
||||
/// .with_param("user_id", 42_i32)
|
||||
/// .with_param("title", "Hello".to_string());
|
||||
///
|
||||
/// let id: i32 = *cx.param("user_id").unwrap();
|
||||
/// let title: &String = cx.param("title").unwrap();
|
||||
///
|
||||
/// // Error de tipo:
|
||||
/// assert!(cx.param::<String>("user_id").is_err());
|
||||
/// ```
|
||||
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError>;
|
||||
|
||||
/// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
|
||||
fn param_or_default<T: Clone + Default + 'static>(&self, key: &'static str) -> T {
|
||||
self.param::<T>(key).ok().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Devuelve el parámetro clonado o un **valor por defecto** si no existe.
|
||||
fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
|
||||
self.param::<T>(key).ok().cloned().unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Devuelve el parámetro clonado o el **valor evaluado** por la función `f` si no existe.
|
||||
fn param_or_else<T: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T {
|
||||
self.param::<T>(key).ok().cloned().unwrap_or_else(f)
|
||||
}
|
||||
|
||||
/// Devuelve el Favicon de los recursos del contexto.
|
||||
fn favicon(&self) -> Option<&Favicon>;
|
||||
|
||||
/// Devuelve las hojas de estilo de los recursos del contexto.
|
||||
fn stylesheets(&self) -> &Assets<StyleSheet>;
|
||||
|
||||
/// Devuelve los scripts JavaScript de los recursos del contexto.
|
||||
fn javascripts(&self) -> &Assets<JavaScript>;
|
||||
|
||||
/// Devuelve los estilos *responsive* acumulados en el contexto.
|
||||
fn responsive_styles(&self) -> &ResponsiveStyles;
|
||||
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento `<body>`.
|
||||
fn body_props(&self) -> &Props;
|
||||
|
||||
// **< Contextual HELPERS >*********************************************************************
|
||||
|
||||
/// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let mut cx = Context::default().with_param("temp", 1u8);
|
||||
/// assert!(cx.remove_param("temp"));
|
||||
/// assert!(!cx.remove_param("temp")); // ya no existe
|
||||
/// ```
|
||||
fn remove_param(&mut self, key: &'static str) -> bool;
|
||||
}
|
||||
18
src/core/component/context/error.rs
Normal file
18
src/core/component/context/error.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use thiserror::Error;
|
||||
|
||||
/// Errores de acceso a parámetros dinámicos del contexto.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ContextError {
|
||||
/// La clave no existe.
|
||||
#[error("parameter not found")]
|
||||
ParamNotFound,
|
||||
/// La clave existe, pero el valor guardado no coincide con el tipo solicitado. Incluye
|
||||
/// nombre de la clave (`key`), tipo esperado (`expected`) y tipo realmente guardado (`saved`)
|
||||
/// para facilitar el diagnóstico.
|
||||
#[error("type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\"")]
|
||||
ParamTypeMismatch {
|
||||
key: &'static str,
|
||||
expected: &'static str,
|
||||
saved: &'static str,
|
||||
},
|
||||
}
|
||||
|
|
@ -19,9 +19,9 @@ type Entry = (Option<Breakpoint>, CowStr, Vec<(CowStr, CowStr)>);
|
|||
/// El punto de corte es opcional, donde `None` declara una regla siempre activa, sin pasar por el
|
||||
/// tema ni depender de que resuelva algún [`Breakpoint`]. No ordena ni combina las clases de dos
|
||||
/// llamadas que las declaren en distinto orden (por ejemplo, `"foo bar"` y `"bar foo"` generan dos
|
||||
/// entradas distintas). La llamada es a través de [`AssetsOp::AddResponsiveStyle`].
|
||||
/// entradas distintas). La llamada es a través de [`AssetsOp::add_responsive_style()`].
|
||||
///
|
||||
/// [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle
|
||||
/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub struct ResponsiveStyles(Vec<Entry>);
|
||||
|
||||
|
|
@ -50,50 +50,94 @@ impl ResponsiveStyles {
|
|||
value: impl AsRef<str>,
|
||||
) {
|
||||
let breakpoint = breakpoint.into();
|
||||
|
||||
let Some(classes) = util::normalize_ascii(classes.as_ref()) else {
|
||||
let Some(classes) = Self::normalize_classes(classes.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
if classes.is_empty() {
|
||||
return;
|
||||
}
|
||||
let classes: CowStr = classes.into_owned().into();
|
||||
|
||||
let property_norm = property.as_ref().trim().to_ascii_lowercase();
|
||||
if property_norm.is_empty() {
|
||||
return;
|
||||
}
|
||||
let property: CowStr = property_norm.into();
|
||||
|
||||
match self
|
||||
.0
|
||||
.iter_mut()
|
||||
.find(|(bp, cls, _)| *bp == breakpoint && *cls == classes)
|
||||
{
|
||||
Some((_, _, styles)) => {
|
||||
// Ya declarada: se descarta sin normalizar `value`, el camino habitual (y que debe
|
||||
// ser barato) cuando muchos componentes comparten la misma clase utilitaria.
|
||||
if styles.iter().any(|(k, _)| *k == property) {
|
||||
return;
|
||||
}
|
||||
let Some(value) = util::non_blank(value.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
styles.push((property, value.to_string().into()));
|
||||
}
|
||||
Some((_, _, styles)) => Self::insert_style(styles, property.as_ref(), value.as_ref()),
|
||||
None => {
|
||||
let Some(value) = util::non_blank(value.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
self.0.push((
|
||||
breakpoint,
|
||||
classes,
|
||||
vec![(property, value.to_string().into())],
|
||||
));
|
||||
let mut styles = Vec::new();
|
||||
Self::insert_style(&mut styles, property.as_ref(), value.as_ref());
|
||||
if !styles.is_empty() {
|
||||
self.0.push((breakpoint, classes, styles));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Añade varias declaraciones de estilo (`property: value`) para las clases indicadas, dentro
|
||||
/// del punto de corte dado, en una única llamada.
|
||||
///
|
||||
/// Equivale a invocar [`add_style()`](Self::add_style) una vez por cada par `(property,
|
||||
/// value)` de `styles`, con las mismas reglas de normalización y de descarte silencioso, pero
|
||||
/// normalizando `classes` y localizando la entrada una sola vez para todo el lote.
|
||||
pub fn add_styles(
|
||||
&mut self,
|
||||
breakpoint: impl Into<Option<Breakpoint>>,
|
||||
classes: impl AsRef<str>,
|
||||
styles: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>,
|
||||
) {
|
||||
let breakpoint = breakpoint.into();
|
||||
let Some(classes) = Self::normalize_classes(classes.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match self
|
||||
.0
|
||||
.iter_mut()
|
||||
.find(|(bp, cls, _)| *bp == breakpoint && *cls == classes)
|
||||
{
|
||||
Some((_, _, existing)) => {
|
||||
for (property, value) in styles {
|
||||
Self::insert_style(existing, property.as_ref(), value.as_ref());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let mut new_styles = Vec::new();
|
||||
for (property, value) in styles {
|
||||
Self::insert_style(&mut new_styles, property.as_ref(), value.as_ref());
|
||||
}
|
||||
if !new_styles.is_empty() {
|
||||
self.0.push((breakpoint, classes, new_styles));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normaliza `classes` para usarla como clave de entrada. Devuelve `None` si contiene caracteres
|
||||
// no ASCII o si el resultado queda vacío tras recortar espacios. Compartida por `add_style()`,
|
||||
// `add_styles()` y `entry()`.
|
||||
fn normalize_classes(classes: &str) -> Option<CowStr> {
|
||||
let classes = util::normalize_ascii(classes)?;
|
||||
if classes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(classes.into_owned().into())
|
||||
}
|
||||
|
||||
// Inserta una declaración (`property: value`) en la lista de destino, ya localizada por el
|
||||
// llamador. Aplica las mismas reglas que `add_style()`: normaliza `property`, descarta si ya
|
||||
// existe una declaración para esa propiedad (sin normalizar `value`, el camino barato cuando
|
||||
// muchos componentes comparten la misma clase utilitaria) y descarta si `property` o `value`
|
||||
// quedan vacíos tras recortar espacios.
|
||||
fn insert_style(styles: &mut Vec<(CowStr, CowStr)>, property: &str, value: &str) {
|
||||
let Some(property) = util::normalize_property(property) else {
|
||||
return;
|
||||
};
|
||||
if styles.iter().any(|(k, _)| k.as_ref() == property) {
|
||||
return;
|
||||
}
|
||||
let Some(value) = util::non_blank(value) else {
|
||||
return;
|
||||
};
|
||||
styles.push((property.into(), value.to_string().into()));
|
||||
}
|
||||
|
||||
// **< ResponsiveStyles GETTERS >***************************************************************
|
||||
|
||||
/// Devuelve el valor de la propiedad indicada para el punto de corte y las clases dados, si
|
||||
|
|
@ -105,7 +149,7 @@ impl ResponsiveStyles {
|
|||
property: impl AsRef<str>,
|
||||
) -> Option<String> {
|
||||
let styles = self.entry(breakpoint.into(), classes.as_ref())?;
|
||||
let property = property.as_ref().trim().to_ascii_lowercase();
|
||||
let property = util::normalize_property(property)?;
|
||||
styles
|
||||
.iter()
|
||||
.find(|(k, _)| k.as_ref() == property)
|
||||
|
|
@ -197,20 +241,17 @@ impl ResponsiveStyles {
|
|||
rules
|
||||
}
|
||||
|
||||
// Normaliza `classes` igual que `add_style()` y busca las declaraciones de la entrada
|
||||
// correspondiente al punto de corte y las clases dados.
|
||||
// Normaliza `classes` y busca las declaraciones de la entrada correspondiente al punto de corte
|
||||
// y las clases dados.
|
||||
fn entry(
|
||||
&self,
|
||||
breakpoint: Option<Breakpoint>,
|
||||
classes: &str,
|
||||
) -> Option<&Vec<(CowStr, CowStr)>> {
|
||||
let classes = util::normalize_ascii(classes)?;
|
||||
if classes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let classes = Self::normalize_classes(classes)?;
|
||||
self.0
|
||||
.iter()
|
||||
.find(|(bp, cls, _)| *bp == breakpoint && cls.as_ref() == classes.as_ref())
|
||||
.find(|(bp, cls, _)| *bp == breakpoint && *cls == classes)
|
||||
.map(|(_, _, styles)| styles)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,8 @@
|
|||
use crate::core::component::Context;
|
||||
use crate::html::assets::Asset;
|
||||
use crate::html::{Markup, PreEscaped, html};
|
||||
use crate::html::{Markup, html};
|
||||
use crate::{AutoDefault, CowStr, Weight, util};
|
||||
|
||||
/// 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(CowStr),
|
||||
/// `name`, `closure(&mut Context) -> String`.
|
||||
Inline(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||
}
|
||||
|
||||
/// Define el medio objetivo para una hoja de estilos.
|
||||
///
|
||||
/// Permite especificar en qué contexto se aplica el CSS, adaptándose a diferentes dispositivos o
|
||||
|
|
@ -50,9 +34,15 @@ impl TargetMedia {
|
|||
|
||||
/// 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.
|
||||
/// Este tipo permite incluir hojas de estilo CSS externas, con soporte para medios específicos
|
||||
/// (`screen`, `print`, etc.) y [pesos](crate::Weight) que determinan el orden de inserción en el
|
||||
/// documento.
|
||||
///
|
||||
/// Para declarar estilos embebidos en el documento (sin un archivo CSS externo), usar
|
||||
/// [`AssetsOp::add_responsive_style()`](crate::core::component::AssetsOp::add_responsive_style) o
|
||||
/// [`AssetsOp::add_responsive_styles()`](crate::core::component::AssetsOp::add_responsive_styles),
|
||||
/// que asocian declaraciones de estilo `propiedad: valor` a una o varias clases CSS y las agrupan
|
||||
/// en un único `<style>` en el `<head>` del documento.
|
||||
///
|
||||
/// > **Nota**
|
||||
/// > Las hojas de estilo CSS deben estar disponibles en el servidor web de la aplicación. Pueden
|
||||
|
|
@ -67,18 +57,10 @@ impl TargetMedia {
|
|||
/// .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;
|
||||
/// }
|
||||
/// "#.to_string());
|
||||
/// ```
|
||||
#[derive(AutoDefault)]
|
||||
pub struct StyleSheet {
|
||||
source: Source, // Fuente y modo de inclusión del CSS.
|
||||
path: CowStr, // Ruta del recurso CSS externo.
|
||||
version: CowStr, // 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.
|
||||
|
|
@ -90,23 +72,7 @@ impl StyleSheet {
|
|||
/// Equivale a `<link rel="stylesheet" href="...">`.
|
||||
pub fn from(path: impl Into<CowStr>) -> Self {
|
||||
Self {
|
||||
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.
|
||||
///
|
||||
/// Un closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
source: Source::Inline(name.into(), Box::new(f)),
|
||||
path: path.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -147,14 +113,9 @@ impl StyleSheet {
|
|||
}
|
||||
|
||||
impl Asset for StyleSheet {
|
||||
/// Devuelve el nombre del recurso, utilizado como clave única.
|
||||
///
|
||||
/// Para hojas de estilos externas es la ruta del recurso; para las embebidas, un identificador.
|
||||
/// Devuelve la ruta del recurso, utilizada como clave única.
|
||||
fn name(&self) -> &str {
|
||||
match &self.source {
|
||||
Source::From(path) => path,
|
||||
Source::Inline(name, _) => name,
|
||||
}
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn weight(&self) -> Weight {
|
||||
|
|
@ -163,17 +124,12 @@ impl Asset for StyleSheet {
|
|||
|
||||
// **< StyleSheet RENDER >**********************************************************************
|
||||
|
||||
fn render(&self, cx: &mut Context) -> Markup {
|
||||
match &self.source {
|
||||
Source::From(path) => html! {
|
||||
link
|
||||
rel="stylesheet"
|
||||
href=(util::join_pair!(path, "?v=", &self.version))
|
||||
media=[self.media.as_str()];
|
||||
},
|
||||
Source::Inline(_, f) => html! {
|
||||
style { (PreEscaped((f)(cx))) };
|
||||
},
|
||||
fn render(&self, _cx: &mut Context) -> Markup {
|
||||
html! {
|
||||
link
|
||||
rel="stylesheet"
|
||||
href=(util::join_pair!(&self.path, "?v=", &self.version))
|
||||
media=[self.media.as_str()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@
|
|||
//! manera independiente a cualquier tema o framework CSS. El nombre interno de cada clase se deriva
|
||||
//! de la propiedad y el valor que representa, así que dos elementos con la misma configuración
|
||||
//! comparten la misma regla en vez de duplicarla. Las declaraciones correspondientes se registran
|
||||
//! vía [`AssetsOp::AddResponsiveStyle`] y se renderizan como reglas en el `<head>` del documento.
|
||||
//! Funciona igual conviva con quien conviva en la misma página, sin necesidad de coordinar nombres
|
||||
//! de clase ni orden alguno en la carga de hojas de estilo.
|
||||
//! vía [`AssetsOp::add_responsive_style()`] y se renderizan como reglas en el `<head>` del
|
||||
//! documento. Funciona igual conviva con quien conviva en la misma página, sin necesidad de
|
||||
//! coordinar nombres de clase ni orden alguno en la carga de hojas de estilo.
|
||||
//!
|
||||
//! [Flexbox]: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout
|
||||
//! [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle
|
||||
//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
|
||||
//! [`PropsOp::flex_item()`]: crate::html::props::PropsOp::flex_item
|
||||
//! [`Container`]: crate::base::component::Container
|
||||
//! [`Navbar`]: crate::base::component::Navbar
|
||||
|
|
@ -72,10 +72,10 @@ fn styles(
|
|||
}
|
||||
classes.push_str(&class);
|
||||
|
||||
cx.alter_assets(AssetsOp::AddResponsiveStyle(
|
||||
cx.alter_assets(AssetsOp::add_responsive_style(
|
||||
entry.map(|e| e.breakpoint),
|
||||
class,
|
||||
property.into(),
|
||||
property,
|
||||
value,
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ enum DisplayFlex {
|
|||
///
|
||||
/// Se resuelve como clases CSS generadas dinámicamente (`display`, `flex-direction`, `flex-wrap`,
|
||||
/// `justify-content`, `align-items`, `align-content`, `gap`), registradas vía
|
||||
/// [`AssetsOp::AddResponsiveStyle`] en [`ResponsiveStyles`] y renderizadas como reglas en el
|
||||
/// [`AssetsOp::add_responsive_style()`] en [`ResponsiveStyles`] y renderizadas como reglas en el
|
||||
/// `<head>` del documento. Son propiedades nativas que no requieren interpretación por parte de los
|
||||
/// temas, siempre funcionan igual, sin una sola línea de CSS ni de código específico.
|
||||
///
|
||||
|
|
@ -32,7 +32,7 @@ enum DisplayFlex {
|
|||
/// regla generada en vez de duplicarla, y el nombre generado no coincide por accidente con clases
|
||||
/// de terceros.
|
||||
///
|
||||
/// [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle
|
||||
/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
|
||||
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
|
||||
///
|
||||
/// # Ejemplo
|
||||
|
|
|
|||
|
|
@ -378,7 +378,7 @@ impl Props {
|
|||
|
||||
/// Devuelve el valor de la propiedad de estilo indicada, si existe.
|
||||
pub fn get_style(&self, property: impl AsRef<str>) -> Option<String> {
|
||||
let property = property.as_ref().trim().to_ascii_lowercase();
|
||||
let property = util::normalize_property(property)?;
|
||||
self.styles
|
||||
.iter()
|
||||
.find(|(k, _)| k.as_ref() == property)
|
||||
|
|
@ -639,10 +639,9 @@ impl Props {
|
|||
// la documentación de `PropsOp::AddStyle` sobre por qué los valores de estilo no se restringen
|
||||
// a ASCII.
|
||||
fn set_style(&mut self, property: &str, value: &str) {
|
||||
let Some(property) = util::non_blank(property) else {
|
||||
let Some(property) = util::normalize_property(property) else {
|
||||
return;
|
||||
};
|
||||
let property = property.to_ascii_lowercase();
|
||||
let Some(value) = util::non_blank(value) else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -703,8 +702,9 @@ impl Props {
|
|||
|
||||
// Elimina la propiedad de estilo indicada, si existe.
|
||||
fn remove_style(&mut self, property: &str) {
|
||||
let property = property.trim().to_ascii_lowercase();
|
||||
self.styles.retain(|(k, _)| k.as_ref() != property);
|
||||
if let Some(property) = util::normalize_property(property) {
|
||||
self.styles.retain(|(k, _)| k.as_ref() != property);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
14
src/lib.rs
14
src/lib.rs
|
|
@ -109,15 +109,15 @@ use std::ops::Deref;
|
|||
/// impl Theme for MyTheme {
|
||||
/// fn before_render_page_body(&self, page: &mut Page) {
|
||||
/// page
|
||||
/// .alter_assets(AssetsOp::AddStyleSheet(
|
||||
/// StyleSheet::from("/pagetop/css/normalize.css").with_version("8.0.1"),
|
||||
/// ))
|
||||
/// .alter_assets(AssetsOp::AddStyleSheet(
|
||||
/// .alter_assets(
|
||||
/// StyleSheet::from("/pagetop/css/normalize.css").with_version("8.0.1")
|
||||
/// )
|
||||
/// .alter_assets(
|
||||
/// StyleSheet::from("/pagetop/css/basic.css").with_version(PAGETOP_VERSION),
|
||||
/// ))
|
||||
/// .alter_assets(AssetsOp::AddStyleSheet(
|
||||
/// )
|
||||
/// .alter_assets(
|
||||
/// StyleSheet::from("/mytheme/styles.css").with_version(env!("CARGO_PKG_VERSION")),
|
||||
/// ));
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ impl Contextual for Page {
|
|||
self
|
||||
}
|
||||
|
||||
fn with_assets(mut self, op: AssetsOp) -> Self {
|
||||
fn with_assets(mut self, op: impl Into<AssetsOp>) -> Self {
|
||||
self.context.alter_assets(op);
|
||||
self
|
||||
}
|
||||
|
|
|
|||
14
src/util.rs
14
src/util.rs
|
|
@ -212,6 +212,20 @@ pub fn normalize_ascii(input: &str) -> Option<Cow<'_, str>> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Recorta espacios y pasa a minúsculas el nombre de una propiedad, tratando el resultado vacío
|
||||
/// como ausencia.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::util;
|
||||
/// assert_eq!(util::normalize_property(" Flex-Basis "), Some("flex-basis".to_string()));
|
||||
/// assert_eq!(util::normalize_property(" "), None);
|
||||
/// ```
|
||||
pub fn normalize_property(property: impl AsRef<str>) -> Option<String> {
|
||||
non_blank(property.as_ref()).map(str::to_ascii_lowercase)
|
||||
}
|
||||
|
||||
/// Recorta espacios, convierte una cadena vacía en `None` y normaliza el resto.
|
||||
///
|
||||
/// Convierte en un único token: en minúsculas y con cada espacio en blanco sustituido por `_`.
|
||||
|
|
|
|||
|
|
@ -105,6 +105,75 @@ async fn add_style_ignores_non_ascii_classes() {
|
|||
assert!(r.is_empty());
|
||||
}
|
||||
|
||||
// **< ResponsiveStyles::add_styles >***************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn add_styles_adds_multiple_declarations_in_order() {
|
||||
let mut r = ResponsiveStyles::new();
|
||||
r.add_styles(
|
||||
Breakpoint::Md,
|
||||
"col",
|
||||
[("flex-basis", "50%"), ("margin-inline-start", "0")],
|
||||
);
|
||||
assert_eq!(
|
||||
r.get_styles(Breakpoint::Md, "col"),
|
||||
Some("flex-basis: 50%; margin-inline-start: 0".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn add_styles_merges_into_an_entry_already_created_by_add_style() {
|
||||
// The batch must reuse the entry created by a prior add_style() call, not duplicate it.
|
||||
let mut r = ResponsiveStyles::new();
|
||||
r.add_style(Breakpoint::Md, "col", "flex-basis", "50%");
|
||||
r.add_styles(Breakpoint::Md, "col", [("margin-inline-start", "0")]);
|
||||
assert_eq!(
|
||||
r.get_styles(Breakpoint::Md, "col"),
|
||||
Some("flex-basis: 50%; margin-inline-start: 0".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn add_styles_keeps_first_value_when_property_repeats_within_the_batch() {
|
||||
// Same first-write-wins rule as add_style(), applied within a single batch.
|
||||
let mut r = ResponsiveStyles::new();
|
||||
r.add_styles(
|
||||
Breakpoint::Md,
|
||||
"col",
|
||||
[("flex-basis", "50%"), ("flex-basis", "33%")],
|
||||
);
|
||||
assert_eq!(
|
||||
r.get_styles(Breakpoint::Md, "col"),
|
||||
Some("flex-basis: 50%".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn add_styles_empty_batch_leaves_no_trace() {
|
||||
// A batch that yields no declarations must not create an empty entry.
|
||||
let none: Vec<(&str, &str)> = Vec::new();
|
||||
let mut r = ResponsiveStyles::new();
|
||||
r.add_styles(Breakpoint::Md, "col", none);
|
||||
assert!(r.is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn add_styles_ignores_all_invalid_declarations_in_the_batch() {
|
||||
// Same silent-discard rules as add_style(), so a batch with only invalid declarations must
|
||||
// also leave no trace (no empty entry left behind).
|
||||
let mut r = ResponsiveStyles::new();
|
||||
r.add_styles(Breakpoint::Md, "col", [("", "50%"), ("flex-basis", "")]);
|
||||
assert!(r.is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn add_styles_ignores_empty_or_non_ascii_classes() {
|
||||
let mut r = ResponsiveStyles::new();
|
||||
r.add_styles(Breakpoint::Md, "", [("flex-basis", "50%")]);
|
||||
r.add_styles(Breakpoint::Md, "cañón", [("flex-basis", "50%")]);
|
||||
assert!(r.is_empty());
|
||||
}
|
||||
|
||||
// **< Class normalization >************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
|
|
@ -355,11 +424,11 @@ async fn render_has_no_line_breaks() {
|
|||
|
||||
#[pagetop::test]
|
||||
async fn context_add_responsive_style_feeds_responsives() {
|
||||
let cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
|
||||
let cx = Context::default().with_assets(AssetsOp::add_responsive_style(
|
||||
Some(Breakpoint::Md),
|
||||
"col".into(),
|
||||
"flex-basis".into(),
|
||||
"50%".into(),
|
||||
"col",
|
||||
"flex-basis",
|
||||
"50%",
|
||||
));
|
||||
assert_eq!(
|
||||
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
||||
|
|
@ -370,17 +439,17 @@ async fn context_add_responsive_style_feeds_responsives() {
|
|||
#[pagetop::test]
|
||||
async fn context_add_responsive_style_accumulates_across_calls() {
|
||||
let cx = Context::default()
|
||||
.with_assets(AssetsOp::AddResponsiveStyle(
|
||||
.with_assets(AssetsOp::add_responsive_style(
|
||||
Some(Breakpoint::Md),
|
||||
"col".into(),
|
||||
"flex-basis".into(),
|
||||
"50%".into(),
|
||||
"col",
|
||||
"flex-basis",
|
||||
"50%",
|
||||
))
|
||||
.with_assets(AssetsOp::AddResponsiveStyle(
|
||||
.with_assets(AssetsOp::add_responsive_style(
|
||||
Some(Breakpoint::Md),
|
||||
"col".into(),
|
||||
"margin-inline-start".into(),
|
||||
"0".into(),
|
||||
"col",
|
||||
"margin-inline-start",
|
||||
"0",
|
||||
));
|
||||
assert_eq!(
|
||||
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
||||
|
|
@ -388,6 +457,19 @@ async fn context_add_responsive_style_accumulates_across_calls() {
|
|||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn context_add_responsive_styles_feeds_responsives() {
|
||||
let cx = Context::default().with_assets(AssetsOp::add_responsive_styles(
|
||||
Some(Breakpoint::Md),
|
||||
"col",
|
||||
[("flex-basis", "50%"), ("margin-inline-start", "0")],
|
||||
));
|
||||
assert_eq!(
|
||||
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
||||
Some("flex-basis: 50%; margin-inline-start: 0".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn context_default_has_no_responsive_styles() {
|
||||
assert!(Context::default().responsive_styles().is_empty());
|
||||
|
|
@ -397,11 +479,11 @@ async fn context_default_has_no_responsive_styles() {
|
|||
|
||||
#[pagetop::test]
|
||||
async fn render_assets_includes_style_tag_with_responsive_styles() {
|
||||
let mut cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
|
||||
let mut cx = Context::default().with_assets(AssetsOp::add_responsive_style(
|
||||
Some(Breakpoint::Xs),
|
||||
"col".into(),
|
||||
"flex-basis".into(),
|
||||
"100%".into(),
|
||||
"col",
|
||||
"flex-basis",
|
||||
"100%",
|
||||
));
|
||||
assert_eq!(
|
||||
cx.render_assets().into_string(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue