✨ Extiende normalización de cadenas ASCII
This commit is contained in:
parent
e9d326cd99
commit
cf7aba2b53
3 changed files with 70 additions and 25 deletions
|
|
@ -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() {
|
||||||
|
|
|
||||||
24
src/util.rs
24
src/util.rs
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue