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