♻️ (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
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue