diff --git a/src/html/props.rs b/src/html/props.rs
index d120282f..616b3cf7 100644
--- a/src/html/props.rs
+++ b/src/html/props.rs
@@ -99,11 +99,19 @@ pub enum PropsOp {
/// Añade la clase o clases que no existan al principio de la lista. La operación se ignora si
/// el valor contiene caracteres no ASCII.
PrependClasses(CowStr),
- /// Sustituye una o varias clases existentes (primer valor) por las clases indicadas (segundo
- /// valor), insertando las nuevas en la posición de la primera clase sustituida encontrada. Si
- /// ninguna de las clases a sustituir existe, la operación no tiene efecto. Se ignora si alguno
- /// de los dos valores contiene caracteres no ASCII.
+ /// Sustituye **una o más** clases del primer valor por las clases indicadas en el segundo
+ /// valor, insertando las nuevas en la posición de la primera clase a sustituir encontrada, con
+ /// independencia del orden en que aparecen en el primer valor. Las que no existan se ignoran.
+ /// Si **ninguna** de las clases a sustituir existe, la operación no tiene efecto y no se
+ /// inserta nada. Se ignora si alguno de los dos valores contiene caracteres no ASCII.
ReplaceClasses(CowStr, CowStr),
+ /// A diferencia de [`ReplaceClasses`](Self::ReplaceClasses), exige que **todas** las clases del
+ /// primer valor estén presentes, independientemente de su orden; si falta una sola, la
+ /// operación no tiene efecto: ninguna clase se elimina ni se inserta. Si todas están presentes,
+ /// las sustituye por las clases indicadas en el segundo valor, insertando las nuevas en la
+ /// posición de la primera clase a sustituir encontrada. Se ignora si alguno de los dos valores
+ /// contiene caracteres no ASCII.
+ ReplaceAllClasses(CowStr, CowStr),
/// Elimina la clase o clases indicadas de la lista. La operación se ignora si el valor contiene
/// caracteres no ASCII.
RemoveClasses(CowStr),
@@ -169,11 +177,34 @@ impl PropsOp {
/// let props = Props::classes("button primary")
/// .with_prop(PropsOp::replace_classes("button", "btn"));
/// assert_eq!(props.get_classes(), Some("btn primary".to_string()));
+ ///
+ /// // Basta con que exista alguna clase de `old` para aplicar el reemplazo.
+ /// let props = Props::classes("btn primary")
+ /// .with_prop(PropsOp::replace_classes("primary secondary", "danger"));
+ /// assert_eq!(props.get_classes(), Some("btn danger".to_string()));
/// ```
pub fn replace_classes(old: impl Into, new: impl Into) -> Self {
Self::ReplaceClasses(old.into(), new.into())
}
+ /// Crea la variante [`ReplaceAllClasses`](Self::ReplaceAllClasses) con las clases a sustituir
+ /// (`old`) y las nuevas clases (`new`).
+ ///
+ /// ```rust
+ /// # use pagetop::prelude::*;
+ /// let props = Props::classes("btn primary")
+ /// .with_prop(PropsOp::replace_all_classes("btn primary", "btn danger"));
+ /// assert_eq!(props.get_classes(), Some("btn danger".to_string()));
+ ///
+ /// // Si falta una sola clase de `old`, no hay reemplazo.
+ /// let props = Props::classes("btn primary")
+ /// .with_prop(PropsOp::replace_all_classes("primary secondary", "danger"));
+ /// assert_eq!(props.get_classes(), Some("btn primary".to_string()));
+ /// ```
+ pub fn replace_all_classes(old: impl Into, new: impl Into) -> Self {
+ Self::ReplaceAllClasses(old.into(), new.into())
+ }
+
/// Crea la variante [`RemoveClasses`](Self::RemoveClasses) con la clase o clases indicadas.
pub fn remove_classes(classes: impl Into) -> Self {
Self::RemoveClasses(classes.into())
@@ -462,6 +493,27 @@ impl Props {
self.insert_classes(new.as_ref().split_ascii_whitespace(), pos);
}
}
+ PropsOp::ReplaceAllClasses(old, new) => {
+ let Some(old) = util::normalize_ascii_or_empty(old.as_ref(), "Props::with_prop")
+ else {
+ return self;
+ };
+ let Some(new) = util::normalize_ascii_or_empty(new.as_ref(), "Props::with_prop")
+ else {
+ return self;
+ };
+ if !self.has_all_classes(old.as_ref()) {
+ return self;
+ }
+ let mut pos = self.classes.len();
+ for class in old.as_ref().split_ascii_whitespace() {
+ if let Some(replace_pos) = self.classes.iter().position(|c| c == class) {
+ self.classes.remove(replace_pos);
+ pos = pos.min(replace_pos);
+ }
+ }
+ self.insert_classes(new.as_ref().split_ascii_whitespace(), pos);
+ }
PropsOp::RemoveClasses(classes) => {
let Some(normalized) =
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
@@ -616,19 +668,8 @@ impl Props {
&& self.attrs.is_empty()
}
- /// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
- pub fn has_class(&self, classes: impl AsRef) -> bool {
- let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
- return false;
- };
- normalized
- .as_ref()
- .split_ascii_whitespace()
- .all(|class| self.classes.iter().any(|c| c == class))
- }
-
/// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente.
- pub fn has_any_class(&self, classes: impl AsRef) -> bool {
+ pub fn has_classes(&self, classes: impl AsRef) -> bool {
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
return false;
};
@@ -638,6 +679,17 @@ impl Props {
.any(|class| self.classes.iter().any(|c| c == class))
}
+ /// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
+ pub fn has_all_classes(&self, classes: impl AsRef) -> bool {
+ let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
+ return false;
+ };
+ normalized
+ .as_ref()
+ .split_ascii_whitespace()
+ .all(|class| self.classes.iter().any(|c| c == class))
+ }
+
/// Recupera una referencia tipada al valor extra asociado a la clave `key`.
///
/// Devuelve un [`Result`] que indica si la clave existe y si el tipo coincide:
diff --git a/tests/html_props_classes.rs b/tests/html_props_classes.rs
index ededb8b2..cf06484a 100644
--- a/tests/html_props_classes.rs
+++ b/tests/html_props_classes.rs
@@ -24,8 +24,8 @@ async fn classes_new_empty_and_whitespace_is_empty() {
async fn classes_new_normalizes_and_dedups_and_preserves_first_occurrence_order() {
let p = Props::classes("Btn btn BTN btn-primary BTN-PRIMARY");
assert_classes(&p, Some("btn btn-primary"));
- assert!(p.has_class("BTN"));
- assert!(p.has_class("btn-primary"));
+ assert!(p.has_all_classes("BTN"));
+ assert!(p.has_all_classes("btn-primary"));
}
// **< PropsOp::add_classes >***********************************************************************
@@ -119,6 +119,57 @@ async fn classes_replace_rejects_non_ascii_targets_is_noop() {
assert_classes(&p, Some("a b c"));
}
+// **< PropsOp::replace_all_classes >***************************************************************
+
+#[pagetop::test]
+async fn classes_replace_all_removes_targets_and_inserts_new_at_min_position() {
+ let p = Props::classes("a b c d").with_prop(PropsOp::replace_all_classes("c a", "x y"));
+ assert_classes(&p, Some("x y b d"));
+}
+
+#[pagetop::test]
+async fn classes_replace_all_when_missing_one_does_nothing_even_to_existing_one() {
+ let p = Props::classes("a b").with_prop(PropsOp::replace_all_classes("a x", "c d"));
+ assert_classes(&p, Some("a b"));
+}
+
+#[pagetop::test]
+async fn classes_replace_all_when_none_found_does_nothing() {
+ let p = Props::classes("a b").with_prop(PropsOp::replace_all_classes("x y", "c d"));
+ assert_classes(&p, Some("a b"));
+}
+
+#[pagetop::test]
+async fn classes_replace_all_is_case_insensitive_on_targets_and_new_values_are_normalized() {
+ let p = Props::classes("btn btn-primary active")
+ .with_prop(PropsOp::replace_all_classes("BTN ACTIVE", "Btn-Secondary"));
+ assert_classes(&p, Some("btn-secondary btn-primary"));
+}
+
+#[pagetop::test]
+async fn classes_replace_all_with_empty_new_removes_only() {
+ let p = Props::classes("a b c").with_prop(PropsOp::replace_all_classes("a b", " "));
+ assert_classes(&p, Some("c"));
+}
+
+#[pagetop::test]
+async fn classes_replace_all_dedups_against_existing_items() {
+ let p = Props::classes("a b c").with_prop(PropsOp::replace_all_classes("a b", "c d"));
+ assert_classes(&p, Some("d c"));
+}
+
+#[pagetop::test]
+async fn classes_replace_all_ignores_target_whitespace_and_repetition() {
+ let p = Props::classes("a b c").with_prop(PropsOp::replace_all_classes(" b b ", "x y"));
+ assert_classes(&p, Some("a x y c"));
+}
+
+#[pagetop::test]
+async fn classes_replace_all_rejects_non_ascii_targets_is_noop() {
+ let p = Props::classes("a b c").with_prop(PropsOp::replace_all_classes("b ñ", "x"));
+ assert_classes(&p, Some("a b c"));
+}
+
// **< PropsOp::set / remove ("class") >************************************************************
#[pagetop::test]
@@ -165,41 +216,41 @@ async fn classes_remove_with_extra_whitespace() {
assert_classes(&p, Some("a c"));
}
-// **< has_class / has_any_class >******************************************************************
+// **< has_classes / has_all_classes >**************************************************************
#[pagetop::test]
async fn classes_contains_single() {
let p = Props::classes("btn btn-primary");
- assert!(p.has_class("btn"));
- assert!(p.has_class("BTN"));
- assert!(!p.has_class("missing"));
+ assert!(p.has_all_classes("btn"));
+ assert!(p.has_all_classes("BTN"));
+ assert!(!p.has_all_classes("missing"));
}
#[pagetop::test]
async fn classes_contains_all_and_any() {
let p = Props::classes("btn btn-primary active");
- assert!(p.has_class("btn active"));
- assert!(p.has_class("BTN BTN-PRIMARY"));
- assert!(!p.has_class("btn missing"));
- assert!(p.has_any_class("missing active"));
- assert!(p.has_any_class("BTN-PRIMARY missing"));
- assert!(!p.has_any_class("missing other"));
+ assert!(p.has_classes("missing active"));
+ assert!(p.has_classes("BTN-PRIMARY missing"));
+ assert!(!p.has_classes("missing other"));
+ assert!(p.has_all_classes("btn active"));
+ assert!(p.has_all_classes("BTN BTN-PRIMARY"));
+ assert!(!p.has_all_classes("btn missing"));
}
#[pagetop::test]
async fn classes_contains_empty_and_whitespace_is_false() {
let p = Props::classes("a b");
- assert!(!p.has_class(""));
- assert!(!p.has_class(" \t"));
- assert!(!p.has_any_class(""));
- assert!(!p.has_any_class(" \n "));
+ assert!(!p.has_classes(""));
+ assert!(!p.has_classes(" \n "));
+ assert!(!p.has_all_classes(""));
+ assert!(!p.has_all_classes(" \t"));
}
#[pagetop::test]
async fn classes_contains_non_ascii_is_false() {
let p = Props::classes("a b");
- assert!(!p.has_class("ñ"));
- assert!(!p.has_any_class("a ñ"));
+ assert!(!p.has_classes("a ñ"));
+ assert!(!p.has_all_classes("ñ"));
}
// **< is_classes_empty >***************************************************************************