♻️ (util): Extrae non_blank() y normalize_token()

This commit is contained in:
Manuel Cillero 2026-07-28 01:00:14 +02:00
parent 747e64ce34
commit dc2ed27da4
8 changed files with 71 additions and 37 deletions

View file

@ -39,8 +39,7 @@ impl<C: Component> AfterRender<C> {
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
/// `C` con identificador `id`. /// `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self { pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); self.referer_id = util::normalize_token(id);
self.referer_id = if id.is_empty() { None } else { Some(id) };
self self
} }

View file

@ -39,8 +39,7 @@ impl<C: Component> BeforeRender<C> {
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
/// `C` con identificador `id`. /// `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self { pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); self.referer_id = util::normalize_token(id);
self.referer_id = if id.is_empty() { None } else { Some(id) };
self self
} }

View file

@ -39,8 +39,7 @@ impl<C: Component> TransformMarkup<C> {
/// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el /// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el
/// componente `C` con identificador `id`. /// componente `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self { pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); self.referer_id = util::normalize_token(id);
self.referer_id = if id.is_empty() { None } else { Some(id) };
self self
} }

View file

@ -1,5 +1,5 @@
use crate::locale::{L10n, LangId}; use crate::locale::{L10n, LangId};
use crate::{AutoDefault, builder_fn}; use crate::{AutoDefault, builder_fn, util};
/// Valor opcional para atributos HTML. /// Valor opcional para atributos HTML.
/// ///
@ -163,12 +163,10 @@ impl AttrName {
/// Establece un nombre nuevo normalizando el valor. /// Establece un nombre nuevo normalizando el valor.
#[builder_fn] #[builder_fn]
pub fn with_name(mut self, name: impl AsRef<str>) -> Self { pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
let name = name.as_ref().trim(); self.0 = match util::normalize_token(name) {
if name.is_empty() { Some(name) => Attr::some(name),
self.0 = Attr::default(); None => Attr::default(),
} else { };
self.0 = Attr::some(name.to_ascii_lowercase().replace(' ', "_"));
}
self self
} }
@ -228,12 +226,10 @@ impl AttrValue {
/// Establece una cadena nueva normalizando el valor. /// Establece una cadena nueva normalizando el valor.
#[builder_fn] #[builder_fn]
pub fn with_str(mut self, value: impl AsRef<str>) -> Self { pub fn with_str(mut self, value: impl AsRef<str>) -> Self {
let value = value.as_ref().trim(); self.0 = match util::non_blank(value.as_ref()) {
if value.is_empty() { Some(value) => Attr::some(value.to_string()),
self.0 = Attr::default(); None => Attr::default(),
} else { };
self.0 = Attr::some(value.to_string());
}
self self
} }

View file

@ -734,12 +734,7 @@ impl Props {
// **< Props PRIVATE >************************************************************************** // **< Props PRIVATE >**************************************************************************
fn apply_id(&mut self, id: &str) { fn apply_id(&mut self, id: &str) {
let id = id.trim(); self.id = util::normalize_token(id);
self.id = if id.is_empty() {
None
} else {
Some(id.to_ascii_lowercase().replace(' ', "_"))
};
} }
fn insert_classes<'a, I>(&mut self, classes: I, mut pos: usize) fn insert_classes<'a, I>(&mut self, classes: I, mut pos: usize)

View file

@ -1,4 +1,4 @@
use crate::AutoDefault; use crate::{AutoDefault, util};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
@ -201,10 +201,9 @@ impl FromStr for UnitValue {
type Err = String; type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> { fn from_str(input: &str) -> Result<Self, Self::Err> {
let s = input.trim(); let Some(s) = util::non_blank(input) else {
if s.is_empty() {
return Ok(UnitValue::None); return Ok(UnitValue::None);
} };
if s.eq_ignore_ascii_case("auto") { if s.eq_ignore_ascii_case("auto") {
return Ok(UnitValue::Auto); return Ok(UnitValue::Auto);
} }

View file

@ -1,4 +1,4 @@
use crate::{global, trace}; use crate::{global, trace, util};
use super::languages::LANGUAGES; use super::languages::LANGUAGES;
use super::{LanguageIdentifier, langid}; use super::{LanguageIdentifier, langid};
@ -92,12 +92,9 @@ impl Locale {
/// [`Locale::Resolved`]. /// [`Locale::Resolved`].
/// - En caso contrario, devuelve [`Locale::Unsupported`] con la cadena original. /// - En caso contrario, devuelve [`Locale::Unsupported`] con la cadena original.
pub fn resolve(language: impl AsRef<str>) -> Self { pub fn resolve(language: impl AsRef<str>) -> Self {
let language = language.as_ref().trim(); let Some(language) = util::non_blank(language.as_ref()) else {
// Rechaza cadenas vacías.
if language.is_empty() {
return Self::Unspecified; return Self::Unspecified;
} };
// Intenta aplicar coincidencia exacta con el código completo (p. ej. "es-MX"). // Intenta aplicar coincidencia exacta con el código completo (p. ej. "es-MX").
let lang = language.to_ascii_lowercase(); let lang = language.to_ascii_lowercase();
@ -149,8 +146,13 @@ impl Locale {
/// Debe llamarse durante la inicialización para indicar si el idioma por defecto procede de la /// Debe llamarse durante la inicialización para indicar si el idioma por defecto procede de la
/// configuración, de una configuración no válida o del idioma de respaldo. /// configuración, de una configuración no válida o del idioma de respaldo.
pub(crate) fn init() { pub(crate) fn init() {
match global::SETTINGS.app.language.as_deref() { match global::SETTINGS
Some(raw) if !raw.trim().is_empty() => { .app
.language
.as_deref()
.and_then(util::non_blank)
{
Some(raw) => {
if let Some(langid) = *CONFIG_LANGID { if let Some(langid) = *CONFIG_LANGID {
trace::debug!("Default language \"{langid}\" (from config: \"{raw}\")"); trace::debug!("Default language \"{langid}\" (from config: \"{raw}\")");
} else { } else {

View file

@ -50,6 +50,25 @@ pub enum NormalizeAsciiError {
NonAscii, NonAscii,
} }
/// Recorta espacios y convierte una cadena vacía en `None`.
///
/// Pensada para campos de formulario opcionales, de tal manera que si una cadena queda vacía tras
/// recortar, entonces se trata como "no proporcionado" en lugar de como un valor válido.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::util;
/// assert_eq!(util::non_blank(" hello "), Some("hello"));
/// assert_eq!(util::non_blank(" "), None);
/// assert_eq!(util::non_blank(""), None);
/// ```
#[inline]
pub fn non_blank(s: &str) -> Option<&str> {
let s = s.trim();
if s.is_empty() { None } else { Some(s) }
}
/// Normaliza una cadena ASCII con uno o varios tokens separados. /// Normaliza una cadena ASCII con uno o varios tokens separados.
/// ///
/// Los *separadores* son caracteres `is_ascii_whitespace()` como `' '`, `'\t'`, `'\n'` o `'\r'`. /// Los *separadores* son caracteres `is_ascii_whitespace()` como `' '`, `'\t'`, `'\n'` o `'\r'`.
@ -168,6 +187,32 @@ pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Opt
} }
} }
/// Recorta espacios, convierte una cadena vacía en `None` y normaliza el resto.
///
/// Convierte en un único token: en minúsculas y con cada espacio en blanco sustituido por `_`.
///
/// Al contrario que [`normalize_ascii()`], no colapsa las secuencias de varios espacios en blanco
/// seguidos, sino que cada uno se convierte en su propio `_` (p. ej. dos espacios seguidos generan
/// `__`).
///
/// Está pensada para identificadores internos (p. ej. el `id` HTML de un componente, o la clave
/// `referer_id` de una acción) construidos a partir de texto libre.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::util;
/// assert_eq!(util::normalize_token(" My Id "), Some("my_id".to_string()));
/// assert_eq!(util::normalize_token("AÑO\tNuevo"), Some("año_nuevo".to_string()));
/// assert_eq!(util::normalize_token("a b"), Some("a___b".to_string()));
/// assert_eq!(util::normalize_token(" "), None);
/// assert_eq!(util::normalize_token(""), None);
/// ```
#[inline]
pub fn normalize_token(s: impl AsRef<str>) -> Option<String> {
non_blank(s.as_ref()).map(|s| s.to_lowercase().replace(char::is_whitespace, "_"))
}
/// Indica si una URL **parece** externa por su prefijo. /// Indica si una URL **parece** externa por su prefijo.
/// ///
/// No es una validación de la URL; sólo mira el inicio del texto: `//` (relativa al protocolo), /// No es una validación de la URL; sólo mira el inicio del texto: `//` (relativa al protocolo),