♻️ (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:
Manuel Cillero 2026-09-12 20:44:17 +02:00
parent 0b8f3f3000
commit 64113aff09
21 changed files with 764 additions and 497 deletions

View file

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

View file

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

View file

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

View file

@ -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

View file

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