From 6365e1a07710295f604bc1d8c26fc9a45df03fc6 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 8 Nov 2025 08:07:59 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20A=C3=B1ade=20trait=20`JoinClasses`?= =?UTF-8?q?=20para=20unir=20clases=20CSS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit También elimina macros sin uso `join_op!` y `join_strict!` (KISS). --- src/html.rs | 3 + src/html/assets/javascript.rs | 6 +- src/html/assets/stylesheet.rs | 2 +- src/html/join_classes.rs | 67 ++++++++++++++++++++++ src/prelude.rs | 6 +- src/util.rs | 103 +++++----------------------------- 6 files changed, 90 insertions(+), 97 deletions(-) create mode 100644 src/html/join_classes.rs diff --git a/src/html.rs b/src/html.rs index 5f5b833..f8709dc 100644 --- a/src/html.rs +++ b/src/html.rs @@ -87,6 +87,9 @@ use crate::{core, AutoDefault}; #[allow(type_alias_bounds)] pub type OptionComponent = core::component::Typed; +mod join_classes; +pub use join_classes::JoinClasses; + mod unit; pub use unit::UnitValue; diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index 0e86f0d..6394842 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -215,13 +215,13 @@ impl Asset for JavaScript { fn render(&self, cx: &mut Context) -> Markup { match &self.source { Source::From(path) => html! { - script src=(join_pair!(path, "?v=", self.version.as_str())) {}; + script src=(join_pair!(path, "?v=", &self.version)) {}; }, Source::Defer(path) => html! { - script src=(join_pair!(path, "?v=", self.version.as_str())) defer {}; + script src=(join_pair!(path, "?v=", &self.version)) defer {}; }, Source::Async(path) => html! { - script src=(join_pair!(path, "?v=", self.version.as_str())) async {}; + script src=(join_pair!(path, "?v=", &self.version)) async {}; }, Source::Inline(_, f) => html! { script { (PreEscaped((f)(cx))) }; diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs index 5f0eaaa..68a13da 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -170,7 +170,7 @@ impl Asset for StyleSheet { Source::From(path) => html! { link rel="stylesheet" - href=(join_pair!(path, "?v=", self.version.as_str())) + href=(join_pair!(path, "?v=", &self.version)) media=[self.media.as_str_opt()]; }, Source::Inline(_, f) => html! { diff --git a/src/html/join_classes.rs b/src/html/join_classes.rs new file mode 100644 index 0000000..3f7d7e7 --- /dev/null +++ b/src/html/join_classes.rs @@ -0,0 +1,67 @@ +/// Añade a los *slices* de elementos [`AsRef`] un método para unir clases CSS. +/// +/// El método es [`join_classes()`](JoinClasses::join_classes), que une las cadenas **no vacías** +/// del *slice* usando un espacio como separador. +pub trait JoinClasses { + /// Une las cadenas **no vacías** de un *slice* usando un espacio como separador. + /// + /// Son cadenas vacías únicamente los elementos del *slice* cuya longitud es `0` (p. ej., `""`); + /// no se realiza recorte ni normalización, por lo que elementos como `" "` no se consideran + /// vacíos. + /// + /// Si todas las cadenas están vacías, devuelve una cadena vacía. Acepta elementos que + /// implementen [`AsRef`] como `&str`, [`String`] o `Cow<'_, str>`. + /// + /// # Ejemplos + /// + /// ```rust + /// # use pagetop::prelude::*; + /// let classes = ["btn", "", "btn-primary"]; + /// assert_eq!(classes.join_classes(), "btn btn-primary"); + /// + /// let empty: [&str; 3] = ["", "", ""]; + /// assert_eq!(empty.join_classes(), ""); + /// + /// let border = String::from("border"); + /// let border_top = String::from("border-top-0"); + /// let v = vec![&border, "", "", "", &border_top]; + /// assert_eq!(v.as_slice().join_classes(), "border border-top-0"); + /// + /// // Elementos con espacios afectan al resultado. + /// let spaced = ["btn", " ", "primary "]; + /// assert_eq!(spaced.join_classes(), "btn primary "); + /// ``` + fn join_classes(&self) -> String; +} + +impl JoinClasses for [T] +where + T: AsRef, +{ + #[inline] + fn join_classes(&self) -> String { + let mut count = 0usize; + let mut total = 0usize; + for s in self.iter().map(T::as_ref).filter(|s| !s.is_empty()) { + count += 1; + total += s.len(); + } + if count == 0 { + return String::new(); + } + let separator = " "; + let mut result = String::with_capacity(total + separator.len() * count.saturating_sub(1)); + for (i, s) in self + .iter() + .map(T::as_ref) + .filter(|s| !s.is_empty()) + .enumerate() + { + if i > 0 { + result.push_str(separator); + } + result.push_str(s); + } + result + } +} diff --git a/src/prelude.rs b/src/prelude.rs index 377dcdf..0919e99 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,7 +9,7 @@ pub use crate::{AutoDefault, StaticResources, UniqueId, Weight}; // MACROS. // crate::util -pub use crate::{hm, join, join_opt, join_pair, join_strict}; +pub use crate::{hm, join, join_pair}; // crate::config pub use crate::include_config; // crate::locale @@ -33,8 +33,8 @@ pub use crate::trace; // alias obsoletos se volverá a declarar como `pub use crate::html::*;`. pub use crate::html::{ display, html_private, Asset, Assets, AttrClasses, AttrId, AttrL10n, AttrName, AttrValue, - ClassesOp, Escaper, Favicon, JavaScript, Markup, PageTopSvg, PreEscaped, PrepareMarkup, - StyleSheet, TargetMedia, UnitValue, DOCTYPE, + ClassesOp, Escaper, Favicon, JavaScript, JoinClasses, Markup, PageTopSvg, PreEscaped, + PrepareMarkup, StyleSheet, TargetMedia, UnitValue, DOCTYPE, }; pub use crate::locale::*; diff --git a/src/util.rs b/src/util.rs index 3d07361..30b994f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -65,53 +65,11 @@ macro_rules! hm { /// ``` #[macro_export] macro_rules! join { - ($($arg:tt)*) => { - $crate::util::concat_string!($($arg)*) + ($($arg:expr),+) => { + $crate::util::concat_string!($($arg),+) }; } -/// Concatena los fragmentos **no vacíos** en un [`Option`] con un separador opcional. -/// -/// Acepta cualquier número de fragmentos que implementen [`AsRef`]. Si todos los fragmentos -/// están vacíos, devuelve `None`. -/// -/// # Ejemplos -/// -/// ```rust -/// # use pagetop::prelude::*; -/// // Concatena los fragmentos no vacíos con un espacio como separador. -/// let result_with_separator = join_opt!(["Hello", "", "World"]; " "); -/// assert_eq!(result_with_separator, Some("Hello World".to_string())); -/// -/// // Concatena los fragmentos no vacíos sin un separador. -/// let result_without_separator = join_opt!(["Hello", "", "World"]); -/// assert_eq!(result_without_separator, Some("HelloWorld".to_string())); -/// -/// // Devuelve `None` si todos los fragmentos están vacíos. -/// let result_empty = join_opt!(["", "", ""]; ","); -/// assert_eq!(result_empty, None); -/// ``` -#[macro_export] -macro_rules! join_opt { - ([$($arg:expr),* $(,)?]) => {{ - let s = $crate::util::concat_string!($($arg),*); - (!s.is_empty()).then_some(s) - }}; - ([$($arg:expr),* $(,)?]; $separator:expr) => {{ - let sep = ($separator).as_ref(); - let mut s = String::new(); - for part in [ $( ($arg).as_ref() ),* ] { - if !(part as &str).is_empty() { - if !s.is_empty() { - s.push_str(sep); - } - s.push_str(part); - } - } - (!s.is_empty()).then_some(s) - }}; -} - /// Concatena dos fragmentos en un [`String`] usando un separador. /// /// Une los dos fragmentos, que deben implementar [`AsRef`], usando el separador proporcionado. @@ -145,54 +103,19 @@ macro_rules! join_opt { #[macro_export] macro_rules! join_pair { ($first:expr, $separator:expr, $second:expr) => {{ - if $first.is_empty() { - String::from($second) - } else if $second.is_empty() { - String::from($first) - } else { - $crate::util::concat_string!($first, $separator, $second) - } - }}; -} + let first_val = $first; + let second_val = $second; + let separator_val = $separator; -/// Concatena varios fragmentos en un [`Option`] **si ninguno está vacío**. -/// -/// Si alguno de los fragmentos, que deben implementar [`AsRef`], está vacío, devuelve -/// [`None`]. Opcionalmente se puede indicar un separador entre los fragmentos concatenados. -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// // Concatena los fragmentos. -/// let result = join_strict!(["Hello", "World"]); -/// assert_eq!(result, Some("HelloWorld".to_string())); -/// -/// // Concatena los fragmentos con un separador. -/// let result_with_separator = join_strict!(["Hello", "World"]; " "); -/// assert_eq!(result_with_separator, Some("Hello World".to_string())); -/// -/// // Devuelve `None` si alguno de los fragmentos está vacío. -/// let result_with_empty = join_strict!(["Hello", "", "World"]); -/// assert_eq!(result_with_empty, None); -/// ``` -#[macro_export] -macro_rules! join_strict { - ([$($arg:expr),* $(,)?]) => {{ - let fragments = [$($arg),*]; - if fragments.iter().any(|&item| item.is_empty()) { - None + let first = AsRef::::as_ref(&first_val); + let second = AsRef::::as_ref(&second_val); + let separator = if first.is_empty() || second.is_empty() { + "" } else { - Some(fragments.concat()) - } - }}; - ([$($arg:expr),* $(,)?]; $separator:expr) => {{ - let fragments = [$($arg),*]; - if fragments.iter().any(|&item| item.is_empty()) { - None - } else { - Some(fragments.join($separator)) - } + AsRef::::as_ref(&separator_val) + }; + + $crate::util::concat_string!(first, separator, second) }}; }