️ Mejora operaciones con clases y documentación

This commit is contained in:
Manuel Cillero 2025-12-31 08:48:25 +01:00
parent e9565bf70b
commit 00d4de840b

View file

@ -3,33 +3,44 @@ use crate::{builder_fn, AutoDefault};
use std::borrow::Cow; use std::borrow::Cow;
/// Operaciones disponibles sobre la lista de clases en [`Classes`]. /// Operaciones disponibles sobre la lista de clases en [`Classes`].
///
/// Cada variante opera sobre **una o más clases** proporcionadas como una cadena separada por
/// espacios (p. ej. `"btn active"`), que se normalizan internamente a minúsculas en
/// [`Classes::with_classes()`].
pub enum ClassesOp { pub enum ClassesOp {
/// Añade al final (si no existe). /// Añade las clases que no existan al final.
Add, Add,
/// Añade al principio. /// Añade las clases que no existan al principio.
Prepend, Prepend,
/// Elimina la(s) clase(s) indicada(s). /// Elimina las clases indicadas que existan.
Remove, Remove,
/// Sustituye una o varias clases por otras nuevas (`Replace("old other".into())`). /// Sustituye una o varias clases existentes (indicadas en la variante) por las clases
/// proporcionadas.
Replace(Cow<'static, str>), Replace(Cow<'static, str>),
/// Alterna presencia/ausencia de una o más clases. /// Alterna presencia/ausencia de una o más clases.
///
/// Si en una misma llamada se repite una clase (p. ej. `"a a"`) que ya existe, el resultado
/// mantiene la pertenencia pero puede cambiar el orden (primero se elimina y luego se añade al
/// final).
Toggle, Toggle,
/// Sustituye toda la lista. /// Sustituye la lista completa por las clases indicadas.
Set, Set,
} }
/// Cadena de clases CSS normalizadas para el atributo `class` de HTML. /// Lista de clases CSS normalizadas para el atributo `class` de HTML.
/// ///
/// Permite construir y modificar dinámicamente con [`ClassesOp`] una lista de clases CSS /// Permite construir y modificar dinámicamente con [`ClassesOp`] una lista de clases CSS
/// normalizadas. /// normalizadas.
/// ///
/// # Normalización /// # Normalización
/// ///
/// - El [orden de las clases no es relevante](https://stackoverflow.com/a/1321712) en CSS, pero /// - Aunque el orden de las clases en el atributo `class` no afecta al resultado en CSS,
/// [`ClassesOp`] ofrece operaciones para controlar su orden de aparición. /// [`ClassesOp`] ofrece operaciones para controlar su orden de aparición por legibilidad.
/// - Las clases se convierten a minúsculas. /// - Las clases se almacenan en minúsculas.
/// - No se permiten clases duplicadas. /// - No se permiten clases duplicadas tras la normalización (por ejemplo, `Btn` y `btn` se
/// consideran la misma clase).
/// - Las clases vacías se ignoran. /// - Las clases vacías se ignoran.
/// - Sin clases, [`get()`](Self::get) devuelve `None` (no `Some("")`).
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
@ -59,53 +70,62 @@ impl Classes {
/// lista de clases actual. /// lista de clases actual.
#[builder_fn] #[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self { pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
let classes = classes.as_ref().to_ascii_lowercase(); let classes = classes.as_ref();
let classes: Vec<&str> = classes.split_ascii_whitespace().collect();
if classes.is_empty() {
return self;
}
match op { match op {
ClassesOp::Add => { ClassesOp::Add => {
self.add(&classes, self.0.len()); self.add(classes, self.0.len());
} }
ClassesOp::Prepend => { ClassesOp::Prepend => {
self.add(&classes, 0); self.add(classes, 0);
} }
ClassesOp::Remove => { ClassesOp::Remove => {
for class in classes { let mut classes_to_remove = classes.split_ascii_whitespace();
self.0.retain(|c| c != class);
// 0 clases: no se hace nada.
let Some(first) = classes_to_remove.next() else {
return self;
};
// 1 clase: un único *retain*, cero reservas extra.
let first = first.to_ascii_lowercase();
let Some(second) = classes_to_remove.next() else {
self.0.retain(|c| c != &first);
return self;
};
// 2+ clases: se construye lista para borrar y un único *retain*.
let mut to_remove = Vec::new();
to_remove.push(first);
to_remove.push(second.to_ascii_lowercase());
for class in classes_to_remove {
to_remove.push(class.to_ascii_lowercase());
} }
self.0.retain(|c| !to_remove.iter().any(|r| r == c));
} }
ClassesOp::Replace(classes_to_replace) => { ClassesOp::Replace(classes_to_replace) => {
let mut pos = self.0.len(); let mut pos = self.0.len();
let replace = classes_to_replace.to_ascii_lowercase(); for class in classes_to_replace.split_ascii_whitespace() {
let replace: Vec<&str> = replace.split_ascii_whitespace().collect(); let class = class.to_ascii_lowercase();
for class in replace { if let Some(replace_pos) = self.0.iter().position(|c| c == &class) {
if let Some(replace_pos) = self.0.iter().position(|c| c == class) {
self.0.remove(replace_pos); self.0.remove(replace_pos);
if pos > replace_pos { pos = pos.min(replace_pos);
pos = replace_pos;
}
} }
} }
self.add(&classes, pos); self.add(classes, pos);
} }
ClassesOp::Toggle => { ClassesOp::Toggle => {
for class in classes { for class in classes.split_ascii_whitespace() {
if !class.is_empty() { let class = class.to_ascii_lowercase();
if let Some(pos) = self.0.iter().position(|c| c.eq(class)) { if let Some(pos) = self.0.iter().position(|c| c == &class) {
self.0.remove(pos); self.0.remove(pos);
} else { } else {
self.0.push(class.to_string()); self.0.push(class);
}
} }
} }
} }
ClassesOp::Set => { ClassesOp::Set => {
self.0.clear(); self.0.clear();
self.add(&classes, 0); self.add(classes, 0);
} }
} }
@ -113,10 +133,16 @@ impl Classes {
} }
#[inline] #[inline]
fn add(&mut self, classes: &[&str], mut pos: usize) { fn add(&mut self, classes: &str, mut pos: usize) {
for &class in classes { for class in classes.split_ascii_whitespace() {
if !class.is_empty() && !self.0.iter().any(|c| c == class) { let class = class.to_ascii_lowercase();
self.0.insert(pos, class.to_string()); // Inserción segura descartando duplicados.
if !self.0.iter().any(|c| c == &class) {
if pos >= self.0.len() {
self.0.push(class);
} else {
self.0.insert(pos, class);
}
pos += 1; pos += 1;
} }
} }
@ -135,8 +161,8 @@ impl Classes {
/// Devuelve `true` si **una única clase** está presente. /// Devuelve `true` si **una única clase** está presente.
/// ///
/// Si necesitas comprobar varias clases separadas por espacios, usa [`contains_all`] o /// Si necesitas comprobar varias clases, usa [`contains_all()`](Self::contains_all) o
/// [`contains_any`]. /// [`contains_any()`](Self::contains_any).
pub fn contains(&self, class: impl AsRef<str>) -> bool { pub fn contains(&self, class: impl AsRef<str>) -> bool {
self.contains_class(class.as_ref()) self.contains_class(class.as_ref())
} }
@ -159,11 +185,6 @@ impl Classes {
#[inline] #[inline]
fn contains_class(&self, class: &str) -> bool { fn contains_class(&self, class: &str) -> bool {
if class.bytes().any(|b| b.is_ascii_uppercase()) { self.0.iter().any(|c| c.eq_ignore_ascii_case(class))
let class = class.to_ascii_lowercase();
self.0.iter().any(|c| c == &class)
} else {
self.0.iter().any(|c| c == class)
}
} }
} }