diff --git a/src/html/classes.rs b/src/html/classes.rs
index 0dec5fd8..0123d383 100644
--- a/src/html/classes.rs
+++ b/src/html/classes.rs
@@ -3,33 +3,44 @@ use crate::{builder_fn, AutoDefault};
use std::borrow::Cow;
/// 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 {
- /// Añade al final (si no existe).
+ /// Añade las clases que no existan al final.
Add,
- /// Añade al principio.
+ /// Añade las clases que no existan al principio.
Prepend,
- /// Elimina la(s) clase(s) indicada(s).
+ /// Elimina las clases indicadas que existan.
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>),
/// 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,
- /// Sustituye toda la lista.
+ /// Sustituye la lista completa por las clases indicadas.
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
/// normalizadas.
///
/// # Normalización
///
-/// - El [orden de las clases no es relevante](https://stackoverflow.com/a/1321712) en CSS, pero
-/// [`ClassesOp`] ofrece operaciones para controlar su orden de aparición.
-/// - Las clases se convierten a minúsculas.
-/// - No se permiten clases duplicadas.
+/// - 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 por legibilidad.
+/// - Las clases se almacenan en minúsculas.
+/// - 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.
+/// - Sin clases, [`get()`](Self::get) devuelve `None` (no `Some("")`).
///
/// # Ejemplo
///
@@ -59,53 +70,62 @@ impl Classes {
/// lista de clases actual.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self {
- let classes = classes.as_ref().to_ascii_lowercase();
- let classes: Vec<&str> = classes.split_ascii_whitespace().collect();
-
- if classes.is_empty() {
- return self;
- }
-
+ let classes = classes.as_ref();
match op {
ClassesOp::Add => {
- self.add(&classes, self.0.len());
+ self.add(classes, self.0.len());
}
ClassesOp::Prepend => {
- self.add(&classes, 0);
+ self.add(classes, 0);
}
ClassesOp::Remove => {
- for class in classes {
- self.0.retain(|c| c != class);
+ let mut classes_to_remove = classes.split_ascii_whitespace();
+
+ // 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) => {
let mut pos = self.0.len();
- let replace = classes_to_replace.to_ascii_lowercase();
- let replace: Vec<&str> = replace.split_ascii_whitespace().collect();
- for class in replace {
- if let Some(replace_pos) = self.0.iter().position(|c| c == class) {
+ for class in classes_to_replace.split_ascii_whitespace() {
+ let class = class.to_ascii_lowercase();
+ if let Some(replace_pos) = self.0.iter().position(|c| c == &class) {
self.0.remove(replace_pos);
- if pos > replace_pos {
- pos = replace_pos;
- }
+ pos = pos.min(replace_pos);
}
}
- self.add(&classes, pos);
+ self.add(classes, pos);
}
ClassesOp::Toggle => {
- for class in classes {
- if !class.is_empty() {
- if let Some(pos) = self.0.iter().position(|c| c.eq(class)) {
- self.0.remove(pos);
- } else {
- self.0.push(class.to_string());
- }
+ for class in classes.split_ascii_whitespace() {
+ let class = class.to_ascii_lowercase();
+ if let Some(pos) = self.0.iter().position(|c| c == &class) {
+ self.0.remove(pos);
+ } else {
+ self.0.push(class);
}
}
}
ClassesOp::Set => {
self.0.clear();
- self.add(&classes, 0);
+ self.add(classes, 0);
}
}
@@ -113,10 +133,16 @@ impl Classes {
}
#[inline]
- fn add(&mut self, classes: &[&str], mut pos: usize) {
- for &class in classes {
- if !class.is_empty() && !self.0.iter().any(|c| c == class) {
- self.0.insert(pos, class.to_string());
+ fn add(&mut self, classes: &str, mut pos: usize) {
+ for class in classes.split_ascii_whitespace() {
+ let class = class.to_ascii_lowercase();
+ // 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;
}
}
@@ -135,8 +161,8 @@ impl Classes {
/// Devuelve `true` si **una única clase** está presente.
///
- /// Si necesitas comprobar varias clases separadas por espacios, usa [`contains_all`] o
- /// [`contains_any`].
+ /// Si necesitas comprobar varias clases, usa [`contains_all()`](Self::contains_all) o
+ /// [`contains_any()`](Self::contains_any).
pub fn contains(&self, class: impl AsRef) -> bool {
self.contains_class(class.as_ref())
}
@@ -159,11 +185,6 @@ impl Classes {
#[inline]
fn contains_class(&self, class: &str) -> bool {
- if class.bytes().any(|b| b.is_ascii_uppercase()) {
- let class = class.to_ascii_lowercase();
- self.0.iter().any(|c| c == &class)
- } else {
- self.0.iter().any(|c| c == class)
- }
+ self.0.iter().any(|c| c.eq_ignore_ascii_case(class))
}
}