diff --git a/src/base/component/form/props.rs b/src/base/component/form/props.rs index 5211b33d..31067f44 100644 --- a/src/base/component/form/props.rs +++ b/src/base/component/form/props.rs @@ -96,7 +96,7 @@ impl Autocomplete { /// El prefijo `section-*` sirve para distinguir entre varios grupos del mismo tipo en una misma /// página (p. ej. una dirección de envío y otra de facturación). pub fn section(name: impl AsRef, field: AutofillField) -> Self { - match util::normalize_ascii_non_blank(name.as_ref()) { + match util::normalize_ascii(name.as_ref()) { Ok(n) if !n.as_ref().contains(' ') => { Self::custom(util::join!("section-", n.as_ref(), " ", field.as_str())) } @@ -196,7 +196,7 @@ impl Autocomplete { let raw = value.as_ref(); // Normaliza la entrada. - let Some(normalized) = util::normalize_ascii(raw) else { + let Some(normalized) = util::normalize_ascii_or_empty(raw, "Autocomplete::custom") else { return Self::On; }; let autocomplete = normalized.as_ref(); diff --git a/src/core/component/context.rs b/src/core/component/context.rs index c23308e1..68d28d9d 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -2,9 +2,9 @@ use crate::auth::CurrentUser; use crate::core::TypeInfo; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; -use crate::core::theme::{Breakpoint, ChildrenInRegions, CoreRegions, CoreTemplates}; +use crate::core::theme::{ChildrenInRegions, CoreRegions, CoreTemplates}; use crate::core::theme::{RegionRef, TemplateRef, ThemeRef}; -use crate::html::{Assets, Favicon, JavaScript, Preload, ResponsiveStyles, StyleSheet}; +use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet}; use crate::html::{Markup, Props, PropsOp, RoutePath, html}; use crate::locale::Lc; use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; @@ -38,11 +38,6 @@ pub enum AssetsOp { AddJavaScript(JavaScript), /// Elimina un script por su ruta o identificador. RemoveJavaScript(&'static str), - - /// Añade una declaración de estilo responsive (`property: value`) para las clases indicadas, - /// dentro del punto de corte dado (`None` para una regla siempre activa). Ver - /// [`ResponsiveStyles::add_style()`]. - AddResponsiveStyle(Option, &'static str, &'static str, &'static str), } /// Errores de acceso a parámetros dinámicos del contexto. @@ -222,9 +217,6 @@ pub trait Contextual: LangId { /// Devuelve los scripts JavaScript de los recursos del contexto. fn javascripts(&self) -> &Assets; - /// Devuelve los estilos *responsive* acumulados en el contexto. - fn responsive_styles(&self) -> &ResponsiveStyles; - /// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento ``. fn body_props(&self) -> &Props; @@ -321,7 +313,6 @@ pub struct Context { preloads : Assets, // Recursos para precarga. stylesheets : Assets, // Hojas de estilo CSS. javascripts : Assets, // Scripts JavaScript. - responsives : ResponsiveStyles, // Estilos *responsive*. body_props : Props, // Id, clases CSS y atributos del . regions : ChildrenInRegions, // Regiones de componentes para renderizar. params : HashMap<&'static str, (Box, &'static str)>, // Parámetros. @@ -354,7 +345,6 @@ impl Context { preloads : Assets::::new(), stylesheets: Assets::::new(), javascripts: Assets::::new(), - responsives: ResponsiveStyles::new(), body_props : Props::default(), regions : ChildrenInRegions::default(), params : HashMap::default(), @@ -411,10 +401,6 @@ impl Context { // Primero los recursos para precarga para iniciar las descargas inmediatamente. (preloads.render(self)) (stylesheets.render(self)) - // Después los estilos *responsive*, para poder sobrescribir sus clases. - @if !self.responsives.is_empty() { - style { (self.responsives.render(self)) } - } (javascripts.render(self)) }; @@ -618,11 +604,6 @@ impl Contextual for Context { AssetsOp::RemoveJavaScript(path) => { self.javascripts.remove(path); } - // Estilos responsive. - AssetsOp::AddResponsiveStyle(breakpoint, classes, property, value) => { - self.responsives - .add_style(breakpoint, classes, property, value); - } } self } @@ -683,10 +664,6 @@ impl Contextual for Context { &self.javascripts } - fn responsive_styles(&self) -> &ResponsiveStyles { - &self.responsives - } - fn body_props(&self) -> &Props { &self.body_props } diff --git a/src/core/theme.rs b/src/core/theme.rs index c6a10048..1e8cba33 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -50,8 +50,8 @@ //! devuelva `Some(&Self)`. Basta con un `impl Theme for MyTheme {}` vacío, ya que todos los //! métodos de [`Theme`] tienen implementación por defecto. //! -//! Un tema puede personalizarse en seis pasos, cada uno necesario sólo si lo que ofrece PageTop -//! por defecto no basta o no aplica: +//! Un tema puede personalizarse en cinco pasos, cada uno necesario sólo si lo que ofrece PageTop +//! por defecto no basta: //! //! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegions`] (`Header`, `Aside`, //! `Content`, `Footer`) como regiones de plantilla siempre disponibles, y [`ReservedRegions`] @@ -77,22 +77,14 @@ //! ejemplo, [`CoreTemplates`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente //! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias //! variantes de plantilla. -//! 4. **Definir los anchos mínimos *mobile-first* para los puntos de corte** sobrescribiendo -//! [`Theme::breakpoint_min_width()`]. Por defecto, [`Breakpoint`] resuelve el ancho mínimo de -//! cada variante (`Sm`, `Md`, etc.) como una cadena CSS ya formateada (p. ej. `"768px"`) que -//! cada tema puede adaptar. Cuando se genera CSS *responsive* a partir de un [`Breakpoint`], se -//! consulta el punto de corte a través de [`Breakpoint::min_width()`], listo para interpolar en -//! un `@media (min-width: ...)` sin ningún cálculo adicional. Un tema sin diseño *responsive* -//! puede traducir todas las variantes a `""` porque al ser *mobile-first*, un punto de corte sin -//! ancho real se aplicará siempre. -//! 5. **Traducir [`Intent`] a la paleta de colores propia del tema** sobrescribiendo +//! 4. **Traducir [`Intent`] a la paleta de colores propia del tema** sobrescribiendo //! [`Theme::intent_color()`]. Por defecto, este método devuelve el vocabulario semántico de //! [`Intent`] (`"primary"`, `"severe"`, etc.); un tema con su propio catálogo de colores (por //! ejemplo, uno basado en Bootstrap) debe traducir cada variante al nombre que le corresponda en //! su paleta. Los componentes que generan clases CSS a partir de una [`Intent`] (`Button`, //! `Badge`, `Dropdown`, etc.) consultan este método a través de [`Intent::color()`], así que la //! clase resultante ya nace en la paleta del tema activo. -//! 6. **Reexportar, extender o añadir componentes**. Un tema puede reexportar tal cual los +//! 5. **Reexportar, extender o añadir componentes**. Un tema puede reexportar tal cual los //! componentes propios de PageTop que no requieran adaptación, extenderlos con un trait propio //! para añadir métodos exclusivos (guardando su estado en valores extra con //! [`PropsOp::set_extra()`](crate::html::PropsOp::set_extra) para consumirlos en el @@ -145,9 +137,6 @@ mod intent; pub use intent::Intent; -mod breakpoint; -pub use breakpoint::Breakpoint; - mod layout; pub use layout::{CoreRegions, RegionName, RegionRef}; pub use layout::{CoreTemplates, TemplateName, TemplateRef}; diff --git a/src/core/theme/breakpoint.rs b/src/core/theme/breakpoint.rs deleted file mode 100644 index 9dcd594e..00000000 --- a/src/core/theme/breakpoint.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::AutoDefault; -use crate::core::component::{Context, Contextual}; - -// **< Breakpoint >********************************************************************************* - -/// Puntos de corte *responsive*, *mobile-first* (aplican "a partir de" el ancho indicado). -/// -/// No define ningún valor en píxeles por sí mismo; cada tema decide a qué ancho corresponde cada -/// variante en su sistema de diseño (ver [`Theme::breakpoint_min_width()`]). -/// -/// [`Theme::breakpoint_min_width()`]: crate::core::theme::Theme::breakpoint_min_width -#[derive(AutoDefault, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub enum Breakpoint { - /// Base *mobile-first*, equivale a "siempre". - #[default] - Xs, - /// A partir del ancho donde un tema suele pasar de móvil a tableta. - Sm, - /// A partir del ancho donde un tema suele pasar a un escritorio pequeño. - Md, - /// A partir del ancho donde un tema suele pasar a un escritorio normal. - Lg, - /// A partir del ancho donde un tema suele considerar el escritorio ancho. - Xl, - /// A partir del ancho donde un tema suele considerar el escritorio muy ancho. - Xxl, -} - -impl Breakpoint { - // Todas las variantes, en orden mobile-first (de Xs a Xxl). - pub(crate) const ALL: [Breakpoint; 6] = [ - Breakpoint::Xs, - Breakpoint::Sm, - Breakpoint::Md, - Breakpoint::Lg, - Breakpoint::Xl, - Breakpoint::Xxl, - ]; - - /// Ancho mínimo resuelto a través del tema activo del contexto actual, como valor CSS ya - /// formateado (p. ej. `"768px"`), o `""` si la variante se aplica siempre, sin ancho real. - /// - /// Atajo de [`Theme::breakpoint_min_width()`](crate::core::theme::Theme::breakpoint_min_width) - /// a través de [`Context::theme()`]. - pub fn min_width(&self, cx: &Context) -> &'static str { - cx.theme().breakpoint_min_width(*self) - } -} diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 5b3875ec..b8a83d01 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -3,7 +3,7 @@ use crate::base::component::{Html, Intro, IntroOpening, layout}; use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender}; use crate::core::component::{Context, Contextual}; use crate::core::extension::Extension; -use crate::core::theme::{Breakpoint, CoreRegions, Intent}; +use crate::core::theme::{CoreRegions, Intent}; use crate::global; use crate::html::{Markup, html}; use crate::locale::Lc; @@ -65,49 +65,19 @@ pub trait Theme: Extension + Send + Sync { None } - /// Traduce un [`Breakpoint`] al punto de corte *responsive*, *mobile-first*, propio del tema. - /// - /// `Breakpoint` no define ningún valor propio en píxeles. Será cada tema el que decida a qué - /// ancho corresponde cada variante como valor CSS ya formateado (p. ej. `"768px"`), listo para - /// aplicar en un `@media (min-width: ...)` sin ningún cálculo adicional. La cadena vacía (`""`) - /// indica que la variante no representa ningún ancho mínimo y se aplica siempre; es el caso de - /// `Xs`. - /// - /// Normalmente, para resolver un ancho *responsive* no se llamará a este método directamente, - /// sino que se usará [`Breakpoint::min_width()`] a través de [`Context::theme()`]. - /// - /// [`Breakpoint::min_width()`]: crate::core::theme::Breakpoint::min_width - /// [`Context::theme()`]: crate::core::component::Context::theme - #[rustfmt::skip] - fn breakpoint_min_width(&self, bp: Breakpoint) -> &'static str { - if let Some(parent) = self.parent() { - return parent.breakpoint_min_width(bp); - } - match bp { - Breakpoint::Xs => "", - Breakpoint::Sm => "576px", - Breakpoint::Md => "768px", - Breakpoint::Lg => "992px", - Breakpoint::Xl => "1200px", - Breakpoint::Xxl => "1400px", - } - } - /// Traduce una [`Intent`] al nombre de color de la paleta propia del tema. /// /// `Intent` no define ninguna cadena propia. Cada tema decide qué nombre le corresponde a cada /// variante en su paleta (p. ej. un tema basado en Bootstrap traduce `Severe` a `"danger"`). /// Los componentes que generan clases CSS a partir de una `Intent` (`Button`, `Badge`, /// `Dropdown`, etc.) no llaman a este método directamente en su `setup()`, usan mejor - /// [`Intent::color()`] como la forma más sencilla de obtener este mismo valor a través de - /// [`Context::theme()`], para que la clase resultante ya nazca en la paleta del tema activo. + /// [`Intent::color()`](crate::core::theme::Intent::color) como la forma más sencilla de obtener + /// este mismo valor a través de [`Context::theme()`](crate::core::component::Context::theme), + /// para que la clase resultante ya nazca en la paleta del tema activo. /// /// La implementación por defecto devuelve el vocabulario semántico propio de PageTop /// (`"primary"`, `"severe"`, etc.), que actúa como paleta base cuando ningún tema la /// sobrescribe. - /// - /// [`Intent::color()`]: crate::core::theme::Intent::color - /// [`Context::theme()`]: crate::core::component::Context::theme #[rustfmt::skip] fn intent_color(&self, intent: Intent) -> &'static str { if let Some(parent) = self.parent() { diff --git a/src/html.rs b/src/html.rs index 25e03603..aa006aea 100644 --- a/src/html.rs +++ b/src/html.rs @@ -17,7 +17,6 @@ mod assets; pub use assets::favicon::Favicon; pub use assets::javascript::JavaScript; pub use assets::preload::Preload; -pub use assets::responsive::ResponsiveStyles; pub use assets::stylesheet::{StyleSheet, TargetMedia}; pub use assets::{Asset, Assets}; diff --git a/src/html/assets.rs b/src/html/assets.rs index 82e9ab96..b1f78803 100644 --- a/src/html/assets.rs +++ b/src/html/assets.rs @@ -1,7 +1,6 @@ pub mod favicon; pub mod javascript; pub mod preload; -pub mod responsive; pub mod stylesheet; use crate::core::component::Context; diff --git a/src/html/assets/responsive.rs b/src/html/assets/responsive.rs deleted file mode 100644 index 25103fe4..00000000 --- a/src/html/assets/responsive.rs +++ /dev/null @@ -1,215 +0,0 @@ -use crate::core::component::Context; -use crate::core::theme::Breakpoint; -use crate::html::{Markup, PreEscaped, html}; -use crate::{AutoDefault, CowStr, util}; - -// Punto de corte (None si no aplica), clase(s) normalizadas y sus declaraciones `propiedad: valor`. -type Entry = (Option, CowStr, Vec<(CowStr, CowStr)>); - -/// Declaraciones de estilo en línea agrupadas por punto de corte ([`Breakpoint`]). -/// -/// También por clase (o clases) que van a renderizarse en bloques `@media` en el `` del -/// documento. -/// -/// Por tanto, cada entrada se identifica por la pareja (punto de corte, clases) y acumula sus -/// propias declaraciones `propiedad: valor`, en el orden en que se añaden. Las clases se guardan en -/// bruto, normalizadas igual que hace [`Props`](crate::html::Props) (ASCII, minúsculas y un único -/// espacio entre tokens): por ejemplo `"foo bar"`, no `.foo.bar`. -/// -/// El punto de corte es opcional, donde `None` declara una regla siempre activa, sin pasar por el -/// tema ni depender de que resuelva algún [`Breakpoint`]. No ordena ni combina las clases de dos -/// llamadas que las declaren en distinto orden (por ejemplo, `"foo bar"` y `"bar foo"` generan dos -/// entradas distintas). La llamada es a través de [`AssetsOp::AddResponsiveStyle`]. -/// -/// [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle -#[derive(AutoDefault, Clone, Debug)] -pub struct ResponsiveStyles(Vec); - -impl ResponsiveStyles { - /// Crea un conjunto vacío. - pub fn new() -> Self { - Self::default() - } - - /// Añade una declaración de estilo (`property: value`) para las clases indicadas, dentro del - /// punto de corte dado. - /// - /// Si ya existe una declaración para la misma propiedad, en el mismo punto de corte y con las - /// mismas clases, la llamada no hace nada: se conserva el valor ya almacenado, no se sustituye. - /// Pensado para clases utilitarias generadas automáticamente, donde el mismo nombre de clase - /// implica siempre el mismo valor -- declararla de nuevo es entonces una operación de sólo - /// lectura, sin normalizar `value`, en vez de una escritura. - /// - /// Si `classes` contiene caracteres no ASCII, o si `classes`, `property` o `value` quedan - /// vacíos tras recortar espacios, la operación se ignora. - pub fn add_style( - &mut self, - breakpoint: impl Into>, - classes: impl AsRef, - property: impl AsRef, - value: impl AsRef, - ) { - let breakpoint = breakpoint.into(); - - let Some(classes) = util::normalize_ascii(classes.as_ref()) else { - return; - }; - if classes.is_empty() { - return; - } - let classes: CowStr = classes.into_owned().into(); - - let property_norm = property.as_ref().trim().to_ascii_lowercase(); - if property_norm.is_empty() { - return; - } - let property: CowStr = property_norm.into(); - - match self - .0 - .iter_mut() - .find(|(bp, cls, _)| *bp == breakpoint && *cls == classes) - { - Some((_, _, styles)) => { - // Ya declarada: se descarta sin normalizar `value`, el camino habitual (y que debe - // ser barato) cuando muchos componentes comparten la misma clase utilitaria. - if styles.iter().any(|(k, _)| *k == property) { - return; - } - let Some(value) = util::non_blank(value.as_ref()) else { - return; - }; - styles.push((property, value.to_string().into())); - } - None => { - let Some(value) = util::non_blank(value.as_ref()) else { - return; - }; - self.0.push(( - breakpoint, - classes, - vec![(property, value.to_string().into())], - )); - } - } - } - - // **< ResponsiveStyles GETTERS >*************************************************************** - - /// Devuelve el valor de la propiedad indicada para el punto de corte y las clases dados, si - /// existe. - pub fn get_style( - &self, - breakpoint: impl Into>, - classes: impl AsRef, - property: impl AsRef, - ) -> Option { - let styles = self.entry(breakpoint.into(), classes.as_ref())?; - let property = property.as_ref().trim().to_ascii_lowercase(); - styles - .iter() - .find(|(k, _)| k.as_ref() == property) - .map(|(_, v)| v.to_string()) - } - - /// Devuelve todas las declaraciones de estilo del punto de corte y las clases dados, como - /// cadena de texto (separadas por `"; "`), si existen. - pub fn get_styles( - &self, - breakpoint: impl Into>, - classes: impl AsRef, - ) -> Option { - let styles = self.entry(breakpoint.into(), classes.as_ref())?; - if styles.is_empty() { - return None; - } - Some( - styles - .iter() - .map(|(k, v)| util::join!(k.as_ref(), ": ", v.as_ref())) - .collect::>() - .join("; "), - ) - } - - /// Devuelve `true` si no hay ninguna declaración almacenada. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - // **< ResponsiveStyles RENDER >**************************************************************** - - /// Renderiza las declaraciones acumuladas como texto CSS. - /// - /// Emite primero las declaraciones sin punto de corte (`None`), siempre sin envoltorio. Luego - /// recorre los puntos de corte reales en orden *mobile-first* (`Xs` a `Xxl`, omitiendo los que - /// no tengan ninguna declaración) y, para cada uno, agrupa las reglas de todas sus entradas - /// (`.clases { propiedad: valor; ... }`). Si el ancho mínimo resuelto por el tema activo - /// ([`Breakpoint::min_width()`]) es una cadena vacía, las reglas se emiten tal cual, sin punto - /// de corte real; en cualquier otro caso se envuelven en `@media (min-width: ...)`. - /// - /// El resultado no contiene saltos de línea. - pub fn render(&self, cx: &Context) -> Markup { - let mut css = String::new(); - - css.push_str(&self.render_rules(None)); - - for breakpoint in Breakpoint::ALL { - let rules = self.render_rules(Some(breakpoint)); - if rules.is_empty() { - continue; - } - let min_width = breakpoint.min_width(cx); - if min_width.is_empty() { - css.push_str(&rules); - } else { - css.push_str(&util::join!( - "@media (min-width: ", - min_width, - ") { ", - rules, - " }" - )); - } - } - - html! { (PreEscaped(css)) } - } - - // Construye, concatenadas y sin separador, las reglas CSS (`.clases { propiedad: valor; ... }`) - // de todas las entradas del punto de corte indicado. - fn render_rules(&self, breakpoint: Option) -> String { - let mut rules = String::new(); - for (_, classes, styles) in self - .0 - .iter() - .filter(|(bp, _, styles)| *bp == breakpoint && !styles.is_empty()) - { - let selector = classes.replace(' ', "."); - let declarations = styles - .iter() - .map(|(property, value)| util::join!(property.as_ref(), ": ", value.as_ref())) - .collect::>() - .join("; "); - rules.push_str(&util::join!(".", selector, " { ", declarations, " }")); - } - rules - } - - // Normaliza `classes` igual que `add_style()` y busca las declaraciones de la entrada - // correspondiente al punto de corte y las clases dados. - fn entry( - &self, - breakpoint: Option, - classes: &str, - ) -> Option<&Vec<(CowStr, CowStr)>> { - let classes = util::normalize_ascii(classes)?; - if classes.is_empty() { - return None; - } - self.0 - .iter() - .find(|(bp, cls, _)| *bp == breakpoint && cls.as_ref() == classes.as_ref()) - .map(|(_, _, styles)| styles) - } -} diff --git a/src/html/props.rs b/src/html/props.rs index c001d953..55a24d35 100644 --- a/src/html/props.rs +++ b/src/html/props.rs @@ -539,23 +539,29 @@ impl Props { } } PropsOp::AddClasses(classes) => { - let Some(normalized) = util::normalize_ascii(classes.as_ref()) else { + let Some(normalized) = + util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop") + else { return self; }; let pos = self.classes.len(); self.insert_classes(normalized.as_ref().split_ascii_whitespace(), pos); } PropsOp::PrependClasses(classes) => { - let Some(normalized) = util::normalize_ascii(classes.as_ref()) else { + let Some(normalized) = + util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop") + else { return self; }; self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0); } PropsOp::ReplaceClasses(old, new) => { - let Some(old) = util::normalize_ascii(old.as_ref()) else { + let Some(old) = util::normalize_ascii_or_empty(old.as_ref(), "Props::with_prop") + else { return self; }; - let Some(new) = util::normalize_ascii(new.as_ref()) else { + let Some(new) = util::normalize_ascii_or_empty(new.as_ref(), "Props::with_prop") + else { return self; }; let mut pos = self.classes.len(); @@ -572,10 +578,12 @@ impl Props { } } PropsOp::ReplaceAllClasses(old, new) => { - let Some(old) = util::normalize_ascii(old.as_ref()) else { + let Some(old) = util::normalize_ascii_or_empty(old.as_ref(), "Props::with_prop") + else { return self; }; - let Some(new) = util::normalize_ascii(new.as_ref()) else { + 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()) { @@ -591,7 +599,9 @@ impl Props { self.insert_classes(new.as_ref().split_ascii_whitespace(), pos); } PropsOp::RemoveClasses(classes) => { - let Some(normalized) = util::normalize_ascii(classes.as_ref()) else { + let Some(normalized) = + util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop") + else { return self; }; self.classes.retain(|c| { @@ -611,7 +621,9 @@ impl Props { if name.as_ref() == "id" { self.apply_id(value.as_ref()); } else if name.as_ref() == "class" { - if let Some(normalized) = util::normalize_ascii(value.as_ref()) { + if let Some(normalized) = + util::normalize_ascii_or_empty(value.as_ref(), "Props::with_prop") + { self.classes.clear(); self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0); } @@ -753,7 +765,7 @@ impl Props { /// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente. pub fn has_classes(&self, classes: impl AsRef) -> bool { - let Ok(normalized) = util::normalize_ascii_non_blank(classes.as_ref()) else { + let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else { return false; }; normalized @@ -764,7 +776,7 @@ impl Props { /// 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_non_blank(classes.as_ref()) else { + let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else { return false; }; normalized @@ -910,17 +922,15 @@ impl Props { // Añade o sustituye una declaración "propiedad: valor". Si la propiedad ya existe, sustituye // su valor conservando la posición; si no, la añade al final. Ignora la declaración si la - // propiedad o el valor quedan vacíos tras recortar espacios. No aplica `normalize_ascii`: ver - // la documentación de `PropsOp::AddStyle` sobre por qué los valores de estilo no se restringen - // a ASCII. + // propiedad o el valor quedan vacíos tras recortar espacios. No aplica + // normalize_ascii_or_empty: ver la documentación de `PropsOp::AddStyle` sobre por qué los + // valores de estilo no se restringen a ASCII. fn set_style(&mut self, property: &str, value: &str) { - let Some(property) = util::non_blank(property) else { + let property = property.trim().to_ascii_lowercase(); + let value = value.trim(); + if property.is_empty() || value.is_empty() { return; - }; - let property = property.to_ascii_lowercase(); - let Some(value) = util::non_blank(value) else { - return; - }; + } if let Some(pos) = self.styles.iter().position(|(k, _)| k.as_ref() == property) { self.styles[pos].1 = value.to_string().into(); } else { @@ -933,9 +943,10 @@ impl Props { // `style`) y aplica cada declaración con `set_style`. Ignora las declaraciones sin ":". fn parse_styles(&mut self, styles: &str) { for style in Self::split_style_declarations(styles) { - let Some(style) = util::non_blank(style) else { + let style = style.trim(); + if style.is_empty() { continue; - }; + } let Some((property, value)) = style.split_once(':') else { trace::debug!( target = "Props::with_prop", diff --git a/src/locale/request.rs b/src/locale/request.rs index 19dc91b3..10aaa5d0 100644 --- a/src/locale/request.rs +++ b/src/locale/request.rs @@ -1,5 +1,4 @@ use crate::global; -use crate::util; use crate::web::HttpRequest; use super::{LangId, LanguageIdentifier, Locale}; @@ -83,7 +82,7 @@ impl RequestLocale { // En este primer elemento también puede aparecer `;q=...`, así que se // extrae únicamente la etiqueta de idioma: "es-ES;q=0.9" -> "es-ES". - let tag = util::non_blank(first.split(';').next()?)?; + let tag = first.split(';').next()?.trim(); // TODO: Mejorar el soporte de `Accept-Language` en el futuro: // @@ -93,7 +92,9 @@ impl RequestLocale { // - Tener en cuenta rangos de idioma (`es`, `en`, etc.) y variantes // regionales. // - Añadir tests unitarios para distintas combinaciones de cabecera. - if let Locale::Resolved(langid) = Locale::resolve(tag) { + if tag.is_empty() { + None + } else if let Locale::Resolved(langid) = Locale::resolve(tag) { Some(langid) } else { None diff --git a/src/response/page.rs b/src/response/page.rs index cebfe41a..d64c462a 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -24,7 +24,7 @@ use crate::base::component::layout; use crate::core::component::{AssetsOp, ChildOp, ComponentRender}; use crate::core::component::{Context, ContextError, Contextual}; use crate::core::theme::{CoreRegions, RegionName, RegionRef, TemplateRef, ThemeRef}; -use crate::html::{Assets, Favicon, JavaScript, ResponsiveStyles, StyleSheet}; +use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{DOCTYPE, Markup, html}; use crate::html::{Props, PropsOp}; use crate::locale::{CharacterDirection, LangId, LanguageIdentifier, Lc}; @@ -348,10 +348,6 @@ impl Contextual for Page { self.context.javascripts() } - fn responsive_styles(&self) -> &ResponsiveStyles { - self.context.responsive_styles() - } - fn body_props(&self) -> &Props { self.context.body_props() } diff --git a/src/util.rs b/src/util.rs index ed3f9c1e..0c0ebf22 100644 --- a/src/util.rs +++ b/src/util.rs @@ -5,7 +5,6 @@ use crate::trace; use std::borrow::Cow; use std::env; use std::io; -use std::panic::Location; use std::path::{Path, PathBuf}; // **< MACROS INTEGRADAS >************************************************************************** @@ -40,7 +39,7 @@ pub fn build_runtime() -> tokio::runtime::Runtime { tokio::runtime::Runtime::new().expect("Failed to build the Tokio runtime") } -/// Errores al normalizar una cadena ASCII con [`normalize_ascii_non_blank()`]. +/// Errores posibles al normalizar una cadena ASCII con [`normalize_ascii()`]. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum NormalizeAsciiError { /// La entrada está vacía (`""`). @@ -70,10 +69,9 @@ pub fn non_blank(s: &str) -> Option<&str> { if s.is_empty() { None } else { Some(s) } } -/// Normaliza una cadena ASCII con uno o varios tokens, exigiendo contenido no vacío. +/// Normaliza una cadena ASCII con uno o varios tokens separados. /// -/// Los *separadores* de los tokens son caracteres `is_ascii_whitespace()` como `' '`, `'\t'`, -/// `'\n'` o `'\r'`. +/// Los *separadores* son caracteres `is_ascii_whitespace()` como `' '`, `'\t'`, `'\n'` o `'\r'`. /// /// Reglas: /// @@ -87,20 +85,13 @@ pub fn non_blank(s: &str) -> Option<&str> { /// Intenta devolver siempre `Cow::Borrowed` para no reservar memoria, y `Cow::Owned` sólo si ha /// tenido que aplicar cambios para normalizar. /// -/// Normalmente se usará [`normalize_ascii()`], que trata una cadena en blanco como resultado -/// válido, en vez de como un error. Esta variante es para detectar también cuándo un resultado es -/// una cadena vacía para rechazarla de inmediato. -/// /// # Ejemplo /// /// ```rust /// # use pagetop::util; -/// assert_eq!( -/// util::normalize_ascii_non_blank(" Foo\tBAR CLi\r\n").unwrap().as_ref(), -/// "foo bar cli" -/// ); +/// assert_eq!(util::normalize_ascii(" Foo\tBAR CLi\r\n").unwrap().as_ref(), "foo bar cli"); /// ``` -pub fn normalize_ascii_non_blank(input: &str) -> Result, NormalizeAsciiError> { +pub fn normalize_ascii(input: &str) -> Result, NormalizeAsciiError> { let bytes = input.as_bytes(); if bytes.is_empty() { return Err(NormalizeAsciiError::IsEmpty); @@ -172,35 +163,19 @@ pub fn normalize_ascii_non_blank(input: &str) -> Result, NormalizeA Ok(Cow::Owned(output)) } -/// Normaliza una cadena ASCII con uno o varios tokens, aceptando cadenas vacías. +/// Normaliza una cadena ASCII, opcionalmente vacía, con uno o varios tokens separados. /// -/// - Devuelve `Some(Cow)` normalizado (ver [`normalize_ascii_non_blank()`] para las reglas exactas -/// de recorte, colapso de espacios y minúsculas) si la entrada es ASCII válida. -/// - Devuelve `Some(Cow::Borrowed(""))` si la entrada es `""` o queda vacía tras recortar. Una -/// entrada en blanco no es un error, es un resultado trivial. -/// - Devuelve `None` **sólo** si la entrada contiene bytes no ASCII; y emite un `trace::debug!` con -/// la ubicación exacta del llamador (vía `#[track_caller]`) para poder localizar el origen del -/// valor rechazado. -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::util; -/// assert_eq!( -/// util::normalize_ascii(" Foo\tBAR CLi\r\n").unwrap().as_ref(), -/// "foo bar cli" -/// ); -/// assert_eq!(util::normalize_ascii(" ").unwrap().as_ref(), ""); -/// assert_eq!(util::normalize_ascii("ñoño"), None); -/// ``` +/// - 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 no ASCII; y emite un `trace::debug!` con el campo +/// `target`. #[inline] -#[track_caller] -pub fn normalize_ascii(input: &str) -> Option> { - match normalize_ascii_non_blank(input) { +pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Option> { + match normalize_ascii(input) { Ok(s) => Some(s), Err(NormalizeAsciiError::NonAscii) => { trace::debug!( - caller = %Location::caller(), + target = %target, input = %input.escape_default(), "Ignoring due to non-ASCII chars" ); diff --git a/tests/html_responsives.rs b/tests/html_responsives.rs deleted file mode 100644 index 561eb45e..00000000 --- a/tests/html_responsives.rs +++ /dev/null @@ -1,416 +0,0 @@ -use pagetop::prelude::*; - -// **< ResponsiveStyles::add_style >**************************************************************** - -#[pagetop::test] -async fn add_style_basic_adds_declaration() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - assert_eq!( - r.get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%".to_string()) - ); -} - -#[pagetop::test] -async fn add_style_multiple_calls_accumulate_in_order() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - r.add_style(Breakpoint::Md, "col", "margin-inline-start", "0"); - assert_eq!( - r.get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%; margin-inline-start: 0".to_string()) - ); -} - -#[pagetop::test] -async fn add_style_keeps_first_value_when_property_already_exists() { - // First-write-wins: a later call for the same (breakpoint, classes, property) is a no-op, - // it does not overwrite the value already stored. - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - r.add_style(Breakpoint::Md, "col", "margin-inline-start", "0"); - r.add_style(Breakpoint::Md, "col", "flex-basis", "33%"); - assert_eq!( - r.get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%; margin-inline-start: 0".to_string()) - ); -} - -#[pagetop::test] -async fn add_style_property_name_is_case_insensitive() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "Flex-Basis", "50%"); - r.add_style(Breakpoint::Md, "col", "FLEX-BASIS", "33%"); - assert_eq!( - r.get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%".to_string()) - ); -} - -#[pagetop::test] -async fn add_style_value_preserves_case() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "font-family", "Arial"); - assert_eq!( - r.get_style(Breakpoint::Md, "col", "font-family"), - Some("Arial".to_string()) - ); -} - -#[pagetop::test] -async fn add_style_trims_whitespace_in_property_and_value() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", " flex-basis ", " 50% "); - assert_eq!( - r.get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%".to_string()) - ); -} - -#[pagetop::test] -async fn add_style_repeated_call_never_looks_at_value_once_property_exists() { - // The already-exists path returns before normalizing `value`, so even a blank value on a - // repeated call has no effect on the declaration already stored. - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - r.add_style(Breakpoint::Md, "col", "flex-basis", " "); - assert_eq!( - r.get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%".to_string()) - ); -} - -#[pagetop::test] -async fn add_style_ignores_empty_property_or_value() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "", "50%"); - r.add_style(Breakpoint::Md, "col", "flex-basis", ""); - r.add_style(Breakpoint::Md, "col", " ", " "); - assert!(r.is_empty()); -} - -#[pagetop::test] -async fn add_style_ignores_empty_or_blank_classes() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "", "flex-basis", "50%"); - r.add_style(Breakpoint::Md, " ", "flex-basis", "50%"); - assert!(r.is_empty()); -} - -#[pagetop::test] -async fn add_style_ignores_non_ascii_classes() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "cañón", "flex-basis", "50%"); - assert!(r.is_empty()); -} - -// **< Class normalization >************************************************************************ - -#[pagetop::test] -async fn add_style_normalizes_classes_case_and_whitespace() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, " Foo BAR ", "color", "red"); - assert_eq!( - r.get_styles(Breakpoint::Md, "foo bar"), - Some("color: red".to_string()) - ); -} - -#[pagetop::test] -async fn add_style_does_not_reorder_classes_tokens() { - // Documented behavior: classes are stored as normalized but NOT sorted, so declaring the - // same classes in a different token order creates a separate entry instead of merging. - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "foo bar", "color", "red"); - r.add_style(Breakpoint::Md, "bar foo", "font-weight", "bold"); - assert_eq!( - r.get_styles(Breakpoint::Md, "foo bar"), - Some("color: red".to_string()) - ); - assert_eq!( - r.get_styles(Breakpoint::Md, "bar foo"), - Some("font-weight: bold".to_string()) - ); -} - -// **< Breakpoint isolation >*********************************************************************** - -#[pagetop::test] -async fn add_style_same_classes_different_breakpoints_do_not_mix() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Sm, "col", "flex-basis", "100%"); - r.add_style(Breakpoint::Lg, "col", "flex-basis", "50%"); - assert_eq!( - r.get_styles(Breakpoint::Sm, "col"), - Some("flex-basis: 100%".to_string()) - ); - assert_eq!( - r.get_styles(Breakpoint::Lg, "col"), - Some("flex-basis: 50%".to_string()) - ); -} - -// **< No breakpoint (None) >*********************************************************************** - -#[pagetop::test] -async fn add_style_accepts_none_as_breakpoint() { - let mut r = ResponsiveStyles::new(); - r.add_style(None, "col", "flex-basis", "50%"); - assert_eq!( - r.get_styles(None, "col"), - Some("flex-basis: 50%".to_string()) - ); -} - -#[pagetop::test] -async fn add_style_none_and_xs_do_not_mix() { - let mut r = ResponsiveStyles::new(); - r.add_style(None, "col", "flex-basis", "100%"); - r.add_style(Breakpoint::Xs, "col", "flex-basis", "50%"); - assert_eq!( - r.get_styles(None, "col"), - Some("flex-basis: 100%".to_string()) - ); - assert_eq!( - r.get_styles(Breakpoint::Xs, "col"), - Some("flex-basis: 50%".to_string()) - ); -} - -// **< is_empty >*********************************************************************************** - -#[pagetop::test] -async fn responsive_styles_is_empty_on_default() { - assert!(ResponsiveStyles::new().is_empty()); -} - -#[pagetop::test] -async fn responsive_styles_is_empty_false_after_add_style() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - assert!(!r.is_empty()); -} - -// **< get_style / get_styles >********************************************************************* - -#[pagetop::test] -async fn get_style_returns_none_for_missing_breakpoint_or_classes() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - assert_eq!(r.get_style(Breakpoint::Lg, "col", "flex-basis"), None); - assert_eq!(r.get_style(Breakpoint::Md, "other", "flex-basis"), None); -} - -#[pagetop::test] -async fn get_style_returns_none_for_missing_property() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - assert_eq!(r.get_style(Breakpoint::Md, "col", "margin"), None); -} - -#[pagetop::test] -async fn get_style_is_case_insensitive_and_trims_input() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - assert_eq!( - r.get_style(Breakpoint::Md, "col", "FLEX-BASIS"), - Some("50%".to_string()) - ); - assert_eq!( - r.get_style(Breakpoint::Md, "col", " flex-basis "), - Some("50%".to_string()) - ); -} - -#[pagetop::test] -async fn get_styles_returns_none_when_nothing_stored() { - assert_eq!( - ResponsiveStyles::new().get_styles(Breakpoint::Md, "col"), - None - ); -} - -#[pagetop::test] -async fn get_styles_matches_classes_after_normalization() { - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "foo bar", "color", "red"); - assert_eq!( - r.get_styles(Breakpoint::Md, " FOO BAR "), - Some("color: red".to_string()) - ); -} - -// **< ResponsiveStyles::render >******************************************************************** - -#[pagetop::test] -async fn render_is_empty_when_nothing_stored() { - let cx = Context::default(); - let r = ResponsiveStyles::new(); - assert_eq!(r.render(&cx).into_string(), ""); -} - -#[pagetop::test] -async fn render_zero_min_width_breakpoint_has_no_media_query() { - // The default theme resolves `Breakpoint::Xs` to `UnitValue::Zero`. - let cx = Context::default(); - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Xs, "col", "flex-basis", "100%"); - assert_eq!(r.render(&cx).into_string(), ".col { flex-basis: 100% }"); -} - -#[pagetop::test] -async fn render_none_breakpoint_has_no_media_query() { - let cx = Context::default(); - let mut r = ResponsiveStyles::new(); - r.add_style(None, "col", "flex-basis", "100%"); - assert_eq!(r.render(&cx).into_string(), ".col { flex-basis: 100% }"); -} - -#[pagetop::test] -async fn render_none_comes_before_every_breakpoint() { - let cx = Context::default(); - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - r.add_style(None, "row", "display", "flex"); - assert_eq!( - r.render(&cx).into_string(), - ".row { display: flex }@media (min-width: 768px) { .col { flex-basis: 50% } }" - ); -} - -#[pagetop::test] -async fn render_non_zero_breakpoint_wraps_in_media_query() { - // The default theme resolves `Breakpoint::Md` to `768px`. - let cx = Context::default(); - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - assert_eq!( - r.render(&cx).into_string(), - "@media (min-width: 768px) { .col { flex-basis: 50% } }" - ); -} - -#[pagetop::test] -async fn render_groups_multiple_properties_in_the_same_rule() { - let cx = Context::default(); - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - r.add_style(Breakpoint::Md, "col", "margin-inline-start", "0"); - assert_eq!( - r.render(&cx).into_string(), - "@media (min-width: 768px) { .col { flex-basis: 50%; margin-inline-start: 0 } }" - ); -} - -#[pagetop::test] -async fn render_concatenates_rules_of_different_selectors_in_the_same_breakpoint() { - let cx = Context::default(); - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - r.add_style(Breakpoint::Md, "row", "display", "flex"); - assert_eq!( - r.render(&cx).into_string(), - "@media (min-width: 768px) { .col { flex-basis: 50% }.row { display: flex } }" - ); -} - -#[pagetop::test] -async fn render_converts_multiple_classes_into_a_compound_selector() { - let cx = Context::default(); - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "foo bar", "color", "red"); - assert_eq!( - r.render(&cx).into_string(), - "@media (min-width: 768px) { .foo.bar { color: red } }" - ); -} - -#[pagetop::test] -async fn render_orders_breakpoints_mobile_first_regardless_of_insertion_order() { - let cx = Context::default(); - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Lg, "col", "flex-basis", "33%"); - r.add_style(Breakpoint::Xs, "col", "flex-basis", "100%"); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - assert_eq!( - r.render(&cx).into_string(), - ".col { flex-basis: 100% }\ - @media (min-width: 768px) { .col { flex-basis: 50% } }\ - @media (min-width: 992px) { .col { flex-basis: 33% } }" - ); -} - -#[pagetop::test] -async fn render_has_no_line_breaks() { - let cx = Context::default(); - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Xs, "col", "flex-basis", "100%"); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - r.add_style(Breakpoint::Md, "row", "display", "flex"); - assert!(!r.render(&cx).into_string().contains('\n')); -} - -// **< Context / AssetsOp integration >************************************************************* - -#[pagetop::test] -async fn context_add_responsive_style_feeds_responsives() { - let cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle( - Some(Breakpoint::Md), - "col", - "flex-basis", - "50%", - )); - assert_eq!( - cx.responsive_styles().get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%".to_string()) - ); -} - -#[pagetop::test] -async fn context_add_responsive_style_accumulates_across_calls() { - let cx = Context::default() - .with_assets(AssetsOp::AddResponsiveStyle( - Some(Breakpoint::Md), - "col", - "flex-basis", - "50%", - )) - .with_assets(AssetsOp::AddResponsiveStyle( - Some(Breakpoint::Md), - "col", - "margin-inline-start", - "0", - )); - assert_eq!( - cx.responsive_styles().get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%; margin-inline-start: 0".to_string()) - ); -} - -#[pagetop::test] -async fn context_default_has_no_responsive_styles() { - assert!(Context::default().responsive_styles().is_empty()); -} - -// **< Context::render_assets integration >********************************************************* - -#[pagetop::test] -async fn render_assets_includes_style_tag_with_responsive_styles() { - let mut cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle( - Some(Breakpoint::Xs), - "col", - "flex-basis", - "100%", - )); - assert_eq!( - cx.render_assets().into_string(), - "" - ); -} - -#[pagetop::test] -async fn render_assets_omits_style_tag_when_no_responsive_styles() { - let mut cx = Context::default(); - assert_eq!(cx.render_assets().into_string(), ""); -} diff --git a/tests/util.rs b/tests/util.rs index 4ed69e01..0730a2c4 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -7,10 +7,10 @@ async fn setup() { Application::new().await; } -// **< Testing normalize_ascii_non_blank() >******************************************************** +// **< Testing normalize_ascii() >****************************************************************** fn assert_err(input: &str, expected: util::NormalizeAsciiError) { - let out = util::normalize_ascii_non_blank(input); + let out = util::normalize_ascii(input); assert_eq!( out, Err(expected), @@ -22,7 +22,7 @@ fn assert_err(input: &str, expected: util::NormalizeAsciiError) { } fn assert_borrowed(input: &str, expected: &str) { - let out = util::normalize_ascii_non_blank(input).expect("Expected Ok(..)"); + let out = util::normalize_ascii(input).expect("Expected Ok(..)"); assert_eq!(out.as_ref(), expected, "Input {:?}", input); assert!( matches!(out, Cow::Borrowed(_)), @@ -33,7 +33,7 @@ fn assert_borrowed(input: &str, expected: &str) { } fn assert_owned(input: &str, expected: &str) { - let out = util::normalize_ascii_non_blank(input).expect("Expected Ok(..)"); + let out = util::normalize_ascii(input).expect("Expected Ok(..)"); assert_eq!(out.as_ref(), expected, "Input {:?}", input); assert!( matches!(out, Cow::Owned(_)), @@ -248,8 +248,8 @@ async fn normalize_is_idempotent() { continue; } - let first = util::normalize_ascii_non_blank(input).unwrap(); - let second = util::normalize_ascii_non_blank(first.as_ref()).unwrap(); + let first = util::normalize_ascii(input).unwrap(); + let second = util::normalize_ascii(first.as_ref()).unwrap(); assert_eq!( first.as_ref(), second.as_ref(),