From 00d4de840ba19b4c1585ec972866960a89d420ed Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Wed, 31 Dec 2025 08:48:25 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Mejora=20operaciones=20con?= =?UTF-8?q?=20clases=20y=20documentaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/html/classes.rs | 119 ++++++++++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 49 deletions(-) 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)) } }