[minimal] Añade macros declarativas a utilidades

- Incorpora nuevo *crate* `pagetop-minimal` con macros básicas para
  operaciones con cadenas, bloques de texto o colecciones clave-valor.
- Refactoriza código para usar `util::join!` y `util::join_pair!` en la
  concatenación de cadenas.
- Normaliza la gestión de localización usando `util::kv!` para los
  argumentos con pares clave-valor.
- Actualizada documentación y archivos README para reflejar la nueva
  estructura y funcionalidades.
This commit is contained in:
Manuel Cillero 2025-12-07 11:55:26 +01:00
parent b7c356b2e0
commit 7b23e9c1ea
25 changed files with 504 additions and 154 deletions

View file

@ -21,7 +21,7 @@ impl Component for PoweredBy {
/// configurada en [`global::SETTINGS`], en el formato `YYYY © Nombre de la aplicación`.
fn new() -> Self {
let year = Utc::now().format("%Y").to_string();
let c = join!(year, " © ", global::SETTINGS.app.name);
let c = util::join!(year, " © ", global::SETTINGS.app.name);
PoweredBy { copyright: Some(c) }
}

View file

@ -6,7 +6,7 @@ use crate::html::{html, Markup};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::locale::{LangId, LangMatch, LanguageIdentifier};
use crate::service::HttpRequest;
use crate::{builder_fn, join};
use crate::{builder_fn, util};
use std::any::Any;
use std::collections::HashMap;
@ -546,7 +546,7 @@ impl Contextual for Context {
prefix
};
self.id_counter += 1;
join!(prefix, "-", self.id_counter.to_string())
util::join!(prefix, "-", self.id_counter.to_string())
}
}
}

View file

@ -24,7 +24,7 @@
use crate::core::component::Context;
use crate::html::{html, Markup};
use crate::locale::L10n;
use crate::{join, AutoDefault};
use crate::{util, AutoDefault};
// **< Region >*************************************************************************************
@ -83,7 +83,7 @@ pub trait Region {
@let region = cx.render_region(self);
@if !region.is_empty() {
div
class=(join!("region region-", self.name()))
class=(util::join!("region region-", self.name()))
role="region"
aria-label=[self.label().lookup(cx)]
{

View file

@ -1,7 +1,7 @@
use crate::core::component::Context;
use crate::html::assets::Asset;
use crate::html::{html, Markup, PreEscaped};
use crate::{join, join_pair, AutoDefault, Weight};
use crate::{util, AutoDefault, Weight};
// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador.
//
@ -215,21 +215,21 @@ 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)) {};
script src=(util::join_pair!(path, "?v=", &self.version)) {};
},
Source::Defer(path) => html! {
script src=(join_pair!(path, "?v=", &self.version)) defer {};
script src=(util::join_pair!(path, "?v=", &self.version)) defer {};
},
Source::Async(path) => html! {
script src=(join_pair!(path, "?v=", &self.version)) async {};
script src=(util::join_pair!(path, "?v=", &self.version)) async {};
},
Source::Inline(_, f) => html! {
script { (PreEscaped((f)(cx))) };
},
Source::OnLoad(_, f) => html! { script { (PreEscaped(join!(
Source::OnLoad(_, f) => html! { script { (PreEscaped(util::join!(
"document.addEventListener(\"DOMContentLoaded\",function(){", (f)(cx), "});"
))) } },
Source::OnLoadAsync(_, f) => html! { script { (PreEscaped(join!(
Source::OnLoadAsync(_, f) => html! { script { (PreEscaped(util::join!(
"document.addEventListener(\"DOMContentLoaded\",async()=>{", (f)(cx), "});"
))) } },
}

View file

@ -1,7 +1,7 @@
use crate::core::component::Context;
use crate::html::assets::Asset;
use crate::html::{html, Markup, PreEscaped};
use crate::{join_pair, AutoDefault, Weight};
use crate::{util, AutoDefault, Weight};
// Define el origen del recurso CSS y cómo se incluye en el documento.
//
@ -170,7 +170,7 @@ impl Asset for StyleSheet {
Source::From(path) => html! {
link
rel="stylesheet"
href=(join_pair!(path, "?v=", &self.version))
href=(util::join_pair!(path, "?v=", &self.version))
media=[self.media.as_str()];
},
Source::Inline(_, f) => html! {

View file

@ -91,7 +91,7 @@
use crate::html::{Markup, PreEscaped};
use crate::service::HttpRequest;
use crate::{global, hm, AutoDefault};
use crate::{global, util, AutoDefault};
pub use fluent_templates;
pub use unic_langid::{CharacterDirection, LanguageIdentifier};
@ -110,7 +110,7 @@ use std::fmt;
// Asocia cada identificador de idioma (como "en-US") con su respectivo [`LanguageIdentifier`] y la
// clave en *locale/.../languages.ftl* para obtener el nombre del idioma según la localización.
static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
hm![
util::kv![
"en" => ( langid!("en-US"), "english" ),
"en-gb" => ( langid!("en-GB"), "english_british" ),
"en-us" => ( langid!("en-US"), "english_united_states" ),
@ -411,7 +411,7 @@ impl L10n {
self
}
/// Añade varios argumentos a la traducción de una sola vez (p. ej. usando la macro [`hm!`],
/// Añade varios argumentos a la traducción de una vez (p. ej. usando la macro [`util::kv!`],
/// también vec![("k", "v")], incluso un array de duplas u otras colecciones).
pub fn with_args<I, K, V>(mut self, args: I) -> Self
where

View file

@ -10,8 +10,6 @@ pub use crate::{AutoDefault, Getters, StaticResources, UniqueId, Weight};
// MACROS.
// crate::util
pub use crate::{hm, join, join_pair};
// crate::config
pub use crate::include_config;
// crate::locale

View file

@ -15,6 +15,9 @@ pub use pagetop_statics::ResourceFiles;
#[doc(hidden)]
pub use actix_web::test;
#[doc(hidden)]
pub use paste::paste;
/// Configura un servicio web para publicar archivos estáticos.
///
/// La macro ofrece tres modos para configurar el servicio:
@ -72,7 +75,7 @@ macro_rules! static_files_service {
}
}
if serve_embedded {
$crate::util::paste! {
$crate::service::paste! {
mod [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
@ -92,7 +95,7 @@ macro_rules! static_files_service {
route = $route,
);
let _ = span.in_scope(|| {
$crate::util::paste! {
$crate::service::paste! {
mod [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}

View file

@ -8,116 +8,7 @@ use std::path::{Path, PathBuf};
// **< MACROS INTEGRADAS >**************************************************************************
#[doc(hidden)]
pub use paste::paste;
#[doc(hidden)]
pub use concat_string::concat_string;
pub use indoc::{concatdoc, formatdoc, indoc};
// **< MACROS ÚTILES >******************************************************************************
/// Macro para construir una colección de pares clave-valor.
///
/// ```rust
/// use pagetop::hm;
/// use std::collections::HashMap;
///
/// let args:HashMap<&str, String> = hm![
/// "userName" => "Roberto",
/// "photoCount" => "3",
/// "userGender" => "male",
/// ];
/// ```
#[macro_export]
macro_rules! hm {
( $($key:expr => $value:expr),* $(,)? ) => {{
let mut a = std::collections::HashMap::new();
$(
a.insert($key.into(), $value.into());
)*
a
}};
}
/// Concatena eficientemente varios fragmentos en un [`String`].
///
/// Esta macro exporta [`concat_string!`](https://docs.rs/concat-string). Acepta cualquier número de
/// fragmentos que implementen [`AsRef<str>`] y construye un [`String`] con el tamaño óptimo, de
/// forma eficiente y evitando el uso de cadenas de formato que penalicen el rendimiento.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// // Concatena todos los fragmentos directamente.
/// let result = join!("Hello", " ", "World");
/// assert_eq!(result, "Hello World".to_string());
///
/// // También funciona con valores vacíos.
/// let result_with_empty = join!("Hello", "", "World");
/// assert_eq!(result_with_empty, "HelloWorld".to_string());
///
/// // Un único fragmento devuelve el mismo valor.
/// let single_result = join!("Hello");
/// assert_eq!(single_result, "Hello".to_string());
/// ```
#[macro_export]
macro_rules! join {
($($arg:expr),+) => {
$crate::util::concat_string!($($arg),+)
};
}
/// Concatena dos fragmentos en un [`String`] usando un separador.
///
/// Une los dos fragmentos, que deben implementar [`AsRef<str>`], usando el separador proporcionado.
/// Si uno de ellos está vacío, devuelve directamente el otro; y si ambos están vacíos devuelve un
/// [`String`] vacío.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let first = "Hello";
/// let separator = "-";
/// let second = "World";
///
/// // Concatena los dos fragmentos cuando ambos no están vacíos.
/// let result = join_pair!(first, separator, second);
/// assert_eq!(result, "Hello-World".to_string());
///
/// // Si el primer fragmento está vacío, devuelve el segundo.
/// let result_empty_first = join_pair!("", separator, second);
/// assert_eq!(result_empty_first, "World".to_string());
///
/// // Si el segundo fragmento está vacío, devuelve el primero.
/// let result_empty_second = join_pair!(first, separator, "");
/// assert_eq!(result_empty_second, "Hello".to_string());
///
/// // Si ambos fragmentos están vacíos, devuelve una cadena vacía.
/// let result_both_empty = join_pair!("", separator, "");
/// assert_eq!(result_both_empty, "".to_string());
/// ```
#[macro_export]
macro_rules! join_pair {
($first:expr, $separator:expr, $second:expr) => {{
let first_val = $first;
let second_val = $second;
let separator_val = $separator;
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 {
AsRef::<str>::as_ref(&separator_val)
};
$crate::util::concat_string!(first, separator, second)
}};
}
pub use pagetop_minimal::{concatdoc, formatdoc, indoc, join, join_pair, kv};
// **< FUNCIONES ÚTILES >***************************************************************************