✨ (props): Añade ReplaceAllClasses
Sustituye clases sólo si están todas las indicadas en el primer valor; a diferencia de `ReplaceClasses`, no actúa parcialmente si falta alguna. Renombra `Props::has_class()`/`has_any_class()` a `has_all_classes()`/ `has_classes()`, que la nueva variante usa internamente para comprobar la condición.
This commit is contained in:
parent
946d335664
commit
ed30b2ae17
2 changed files with 137 additions and 34 deletions
|
|
@ -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
|
/// 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.
|
/// el valor contiene caracteres no ASCII.
|
||||||
PrependClasses(CowStr),
|
PrependClasses(CowStr),
|
||||||
/// Sustituye una o varias clases existentes (primer valor) por las clases indicadas (segundo
|
/// 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 sustituida encontrada. Si
|
/// valor, insertando las nuevas en la posición de la primera clase a sustituir encontrada, con
|
||||||
/// ninguna de las clases a sustituir existe, la operación no tiene efecto. Se ignora si alguno
|
/// independencia del orden en que aparecen en el primer valor. Las que no existan se ignoran.
|
||||||
/// de los dos valores contiene caracteres no ASCII.
|
/// 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),
|
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
|
/// Elimina la clase o clases indicadas de la lista. La operación se ignora si el valor contiene
|
||||||
/// caracteres no ASCII.
|
/// caracteres no ASCII.
|
||||||
RemoveClasses(CowStr),
|
RemoveClasses(CowStr),
|
||||||
|
|
@ -169,11 +177,34 @@ impl PropsOp {
|
||||||
/// let props = Props::classes("button primary")
|
/// let props = Props::classes("button primary")
|
||||||
/// .with_prop(PropsOp::replace_classes("button", "btn"));
|
/// .with_prop(PropsOp::replace_classes("button", "btn"));
|
||||||
/// assert_eq!(props.get_classes(), Some("btn primary".to_string()));
|
/// 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<CowStr>, new: impl Into<CowStr>) -> Self {
|
pub fn replace_classes(old: impl Into<CowStr>, new: impl Into<CowStr>) -> Self {
|
||||||
Self::ReplaceClasses(old.into(), new.into())
|
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<CowStr>, new: impl Into<CowStr>) -> Self {
|
||||||
|
Self::ReplaceAllClasses(old.into(), new.into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Crea la variante [`RemoveClasses`](Self::RemoveClasses) con la clase o clases indicadas.
|
/// Crea la variante [`RemoveClasses`](Self::RemoveClasses) con la clase o clases indicadas.
|
||||||
pub fn remove_classes(classes: impl Into<CowStr>) -> Self {
|
pub fn remove_classes(classes: impl Into<CowStr>) -> Self {
|
||||||
Self::RemoveClasses(classes.into())
|
Self::RemoveClasses(classes.into())
|
||||||
|
|
@ -462,6 +493,27 @@ impl Props {
|
||||||
self.insert_classes(new.as_ref().split_ascii_whitespace(), pos);
|
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) => {
|
PropsOp::RemoveClasses(classes) => {
|
||||||
let Some(normalized) =
|
let Some(normalized) =
|
||||||
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
|
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
|
||||||
|
|
@ -616,19 +668,8 @@ impl Props {
|
||||||
&& self.attrs.is_empty()
|
&& 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<str>) -> 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.
|
/// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente.
|
||||||
pub fn has_any_class(&self, classes: impl AsRef<str>) -> bool {
|
pub fn has_classes(&self, classes: impl AsRef<str>) -> bool {
|
||||||
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
@ -638,6 +679,17 @@ impl Props {
|
||||||
.any(|class| self.classes.iter().any(|c| c == class))
|
.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<str>) -> 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`.
|
/// 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:
|
/// Devuelve un [`Result`] que indica si la clave existe y si el tipo coincide:
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
async fn classes_new_normalizes_and_dedups_and_preserves_first_occurrence_order() {
|
||||||
let p = Props::classes("Btn btn BTN btn-primary BTN-PRIMARY");
|
let p = Props::classes("Btn btn BTN btn-primary BTN-PRIMARY");
|
||||||
assert_classes(&p, Some("btn btn-primary"));
|
assert_classes(&p, Some("btn btn-primary"));
|
||||||
assert!(p.has_class("BTN"));
|
assert!(p.has_all_classes("BTN"));
|
||||||
assert!(p.has_class("btn-primary"));
|
assert!(p.has_all_classes("btn-primary"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< PropsOp::add_classes >***********************************************************************
|
// **< PropsOp::add_classes >***********************************************************************
|
||||||
|
|
@ -119,6 +119,57 @@ async fn classes_replace_rejects_non_ascii_targets_is_noop() {
|
||||||
assert_classes(&p, Some("a b c"));
|
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") >************************************************************
|
// **< PropsOp::set / remove ("class") >************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -165,41 +216,41 @@ async fn classes_remove_with_extra_whitespace() {
|
||||||
assert_classes(&p, Some("a c"));
|
assert_classes(&p, Some("a c"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< has_class / has_any_class >******************************************************************
|
// **< has_classes / has_all_classes >**************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_contains_single() {
|
async fn classes_contains_single() {
|
||||||
let p = Props::classes("btn btn-primary");
|
let p = Props::classes("btn btn-primary");
|
||||||
assert!(p.has_class("btn"));
|
assert!(p.has_all_classes("btn"));
|
||||||
assert!(p.has_class("BTN"));
|
assert!(p.has_all_classes("BTN"));
|
||||||
assert!(!p.has_class("missing"));
|
assert!(!p.has_all_classes("missing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_contains_all_and_any() {
|
async fn classes_contains_all_and_any() {
|
||||||
let p = Props::classes("btn btn-primary active");
|
let p = Props::classes("btn btn-primary active");
|
||||||
assert!(p.has_class("btn active"));
|
assert!(p.has_classes("missing active"));
|
||||||
assert!(p.has_class("BTN BTN-PRIMARY"));
|
assert!(p.has_classes("BTN-PRIMARY missing"));
|
||||||
assert!(!p.has_class("btn missing"));
|
assert!(!p.has_classes("missing other"));
|
||||||
assert!(p.has_any_class("missing active"));
|
assert!(p.has_all_classes("btn active"));
|
||||||
assert!(p.has_any_class("BTN-PRIMARY missing"));
|
assert!(p.has_all_classes("BTN BTN-PRIMARY"));
|
||||||
assert!(!p.has_any_class("missing other"));
|
assert!(!p.has_all_classes("btn missing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_contains_empty_and_whitespace_is_false() {
|
async fn classes_contains_empty_and_whitespace_is_false() {
|
||||||
let p = Props::classes("a b");
|
let p = Props::classes("a b");
|
||||||
assert!(!p.has_class(""));
|
assert!(!p.has_classes(""));
|
||||||
assert!(!p.has_class(" \t"));
|
assert!(!p.has_classes(" \n "));
|
||||||
assert!(!p.has_any_class(""));
|
assert!(!p.has_all_classes(""));
|
||||||
assert!(!p.has_any_class(" \n "));
|
assert!(!p.has_all_classes(" \t"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_contains_non_ascii_is_false() {
|
async fn classes_contains_non_ascii_is_false() {
|
||||||
let p = Props::classes("a b");
|
let p = Props::classes("a b");
|
||||||
assert!(!p.has_class("ñ"));
|
assert!(!p.has_classes("a ñ"));
|
||||||
assert!(!p.has_any_class("a ñ"));
|
assert!(!p.has_all_classes("ñ"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< is_classes_empty >***************************************************************************
|
// **< is_classes_empty >***************************************************************************
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue