Extiende normalización de cadenas ASCII

This commit is contained in:
Manuel Cillero 2026-01-04 19:14:51 +01:00
parent e9d326cd99
commit cf7aba2b53
3 changed files with 70 additions and 25 deletions

View file

@ -1,4 +1,4 @@
use crate::{builder_fn, trace, util, AutoDefault}; use crate::{builder_fn, util, AutoDefault};
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashSet; use std::collections::HashSet;
@ -74,16 +74,10 @@ 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 normalized = match util::normalize_ascii(classes.as_ref()) { let Some(normalized) =
Ok(c) => c, util::normalize_ascii_or_empty(classes.as_ref(), "Classes::with_classes")
Err(util::NormalizeAsciiError::NonAscii) => { else {
trace::debug!( return self;
classes = %classes.as_ref().escape_default(),
"Classes::with_classes: Ignoring classes due to non-ASCII chars"
);
return self;
}
_ => Cow::Borrowed(""),
}; };
match op { match op {
ClassesOp::Add => { ClassesOp::Add => {
@ -116,25 +110,24 @@ impl Classes {
self.0.retain(|c| !to_remove.contains(c.as_str())); self.0.retain(|c| !to_remove.contains(c.as_str()));
} }
ClassesOp::Replace(classes_to_replace) => { ClassesOp::Replace(classes_to_replace) => {
let mut pos = self.0.len(); let Some(classes_to_replace) = util::normalize_ascii_or_empty(
let classes_to_replace = match util::normalize_ascii(classes_to_replace.as_ref()) { classes_to_replace.as_ref(),
Ok(c) => c, "ClassesOp::Replace",
Err(util::NormalizeAsciiError::NonAscii) => { ) else {
trace::debug!( return self;
classes = %classes_to_replace.as_ref().escape_default(),
"Classes::with_classes: Invalid replace classes due to non-ASCII chars"
);
return self;
}
_ => Cow::Borrowed(""),
}; };
let mut pos = self.0.len();
let mut replaced = false;
for class in classes_to_replace.as_ref().split_ascii_whitespace() { for class in classes_to_replace.as_ref().split_ascii_whitespace() {
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);
pos = pos.min(replace_pos); pos = pos.min(replace_pos);
replaced = true;
} }
} }
self.add(normalized.as_ref().split_ascii_whitespace(), pos); if replaced {
self.add(normalized.as_ref().split_ascii_whitespace(), pos);
}
} }
ClassesOp::Toggle => { ClassesOp::Toggle => {
for class in normalized.as_ref().split_ascii_whitespace() { for class in normalized.as_ref().split_ascii_whitespace() {

View file

@ -128,6 +128,30 @@ pub fn normalize_ascii<'a>(input: &'a str) -> Result<Cow<'a, str>, NormalizeAsci
Ok(Cow::Owned(output)) Ok(Cow::Owned(output))
} }
/// Normaliza una cadena ASCII, opcionalmente vacía, con uno o varios tokens separados.
///
/// - Devuelve `Some(Cow)` si la entrada es válida ASCII (normalizada a minúsculas).
/// - Devuelve `Some(Cow::Borrowed(""))` si la entrada es `""` o queda vacía tras recortar.
/// - Devuelve `None` si la entrada contiene bytes non-ASCII; y emite un `trace::debug!` con el
/// campo `target`.
#[inline]
pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Option<Cow<'a, str>> {
match normalize_ascii(input) {
Ok(s) => Some(s),
Err(NormalizeAsciiError::NonAscii) => {
trace::debug!(
target = %target,
input = %input.escape_default(),
"Ignoring due to non-ASCII chars"
);
None
}
Err(NormalizeAsciiError::IsEmpty | NormalizeAsciiError::EmptyAfterTrimming) => {
Some(Cow::Borrowed(""))
}
}
}
/// Resuelve y valida la ruta de un directorio existente, devolviendo una ruta absoluta. /// Resuelve y valida la ruta de un directorio existente, devolviendo una ruta absoluta.
/// ///
/// - Si la ruta es relativa, se resuelve respecto al directorio del proyecto según la variable de /// - Si la ruta es relativa, se resuelve respecto al directorio del proyecto según la variable de

View file

@ -54,6 +54,12 @@ async fn classes_add_same_tokens() {
assert_classes(&c, Some("a b")); assert_classes(&c, Some("a b"));
} }
#[pagetop::test]
async fn classes_add_rejects_non_ascii_is_noop() {
let c = Classes::new("a b").with_classes(ClassesOp::Add, "c ñ d");
assert_classes(&c, Some("a b"));
}
#[pagetop::test] #[pagetop::test]
async fn classes_prepend_inserts_at_front_preserving_new_order() { async fn classes_prepend_inserts_at_front_preserving_new_order() {
let c = Classes::new("c d").with_classes(ClassesOp::Prepend, "A b"); let c = Classes::new("c d").with_classes(ClassesOp::Prepend, "A b");
@ -139,9 +145,9 @@ async fn classes_replace_removes_targets_and_inserts_new_at_min_position() {
} }
#[pagetop::test] #[pagetop::test]
async fn classes_replace_when_none_found_appends_at_end() { async fn classes_replace_when_none_found_does_nothing() {
let c = Classes::new("a b").with_classes(ClassesOp::Replace("x y".into()), "c d"); let c = Classes::new("a b").with_classes(ClassesOp::Replace("x y".into()), "c d");
assert_classes(&c, Some("a b c d")); assert_classes(&c, Some("a b"));
} }
#[pagetop::test] #[pagetop::test]
@ -169,6 +175,12 @@ async fn classes_replace_ignores_target_whitespace_and_repetition() {
assert_classes(&c, Some("a x y c")); assert_classes(&c, Some("a x y c"));
} }
#[pagetop::test]
async fn classes_replace_rejects_non_ascii_targets_is_noop() {
let c = Classes::new("a b c").with_classes(ClassesOp::Replace("b ñ".into()), "x");
assert_classes(&c, Some("a b c"));
}
// **< Queries (contains) >************************************************************************* // **< Queries (contains) >*************************************************************************
#[pagetop::test] #[pagetop::test]
@ -192,6 +204,22 @@ async fn classes_contains_all_and_any() {
assert!(!c.contains_any("missing other")); assert!(!c.contains_any("missing other"));
} }
#[pagetop::test]
async fn classes_contains_empty_and_whitespace_is_false() {
let c = Classes::new("a b");
assert!(!c.contains(""));
assert!(!c.contains(" \t"));
assert!(!c.contains_any(""));
assert!(!c.contains_any(" \n "));
}
#[pagetop::test]
async fn classes_contains_non_ascii_is_false() {
let c = Classes::new("a b");
assert!(!c.contains("ñ"));
assert!(!c.contains_any("a ñ"));
}
// **< Properties / regression (combined sequences, ordering) >************************************* // **< Properties / regression (combined sequences, ordering) >*************************************
#[pagetop::test] #[pagetop::test]