Añade trait JoinClasses para unir clases CSS

También elimina macros sin uso `join_op!` y `join_strict!` (KISS).
This commit is contained in:
Manuel Cillero 2025-11-08 08:07:59 +01:00
parent 6a4ad213d8
commit 6365e1a077
6 changed files with 90 additions and 97 deletions

View file

@ -87,6 +87,9 @@ use crate::{core, AutoDefault};
#[allow(type_alias_bounds)]
pub type OptionComponent<C: core::component::Component> = core::component::Typed<C>;
mod join_classes;
pub use join_classes::JoinClasses;
mod unit;
pub use unit::UnitValue;

View file

@ -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))) };

View file

@ -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! {

67
src/html/join_classes.rs Normal file
View file

@ -0,0 +1,67 @@
/// Añade a los *slices* de elementos [`AsRef<str>`] 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<str>`] 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<T> JoinClasses for [T]
where
T: AsRef<str>,
{
#[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
}
}

View file

@ -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::*;

View file

@ -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<String>`] con un separador opcional.
///
/// Acepta cualquier número de fragmentos que implementen [`AsRef<str>`]. 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<str>`], 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<String>`] **si ninguno está vacío**.
///
/// Si alguno de los fragmentos, que deben implementar [`AsRef<str>`], 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::<str>::as_ref(&first_val);
let second = AsRef::<str>::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::<str>::as_ref(&separator_val)
};
$crate::util::concat_string!(first, separator, second)
}};
}