diff --git a/CREDITS.md b/CREDITS.md
index c5a7bd2..f5c1b0f 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -1,7 +1,8 @@
# 🔃 Dependencias
-PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
-algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs) como son:
+`PageTop` está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes
+aprovechando algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs)
+como son:
* [Actix Web](https://actix.rs/) para los servicios web.
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
@@ -10,14 +11,14 @@ algunas de las librerías más robustas y populares del [ecosistema Rust](https:
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
* Además de otros *crates* adicionales que se pueden explorar en los archivos `Cargo.toml` de
- PageTop y sus extensiones.
+ `PageTop` y sus extensiones.
# 🗚 FIGfonts
-PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
-para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
-[FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
+`PageTop` usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por
+*yuanbohan* para mostrar un banner de presentación en el terminal con el nombre de la aplicación en
+caracteres [FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) de *Glenn Chappell*
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) de *Glenn Chappell*
diff --git a/Cargo.lock b/Cargo.lock
index 3053e20..944027d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1307,12 +1307,6 @@ dependencies = [
"hashbrown 0.15.4",
]
-[[package]]
-name = "indoc"
-version = "2.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
-
[[package]]
name = "inout"
version = "0.1.4"
@@ -1574,7 +1568,6 @@ dependencies = [
"config",
"figlet-rs",
"fluent-templates",
- "indoc",
"itoa",
"pagetop-build",
"pagetop-macros",
diff --git a/Cargo.toml b/Cargo.toml
index ab7551f..8ccd69e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,6 @@ colored = "3.0.0"
concat-string = "1.0.1"
config = { version = "0.15.13", default-features = false, features = ["toml"] }
figlet-rs = "0.1.5"
-indoc = "2.0.6"
itoa = "1.0.15"
parking_lot = "0.12.4"
paste = { package = "pastey", version = "0.1.0" }
diff --git a/README.md b/README.md
index c6c12e0..e7fab94 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,8 @@
-PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
-creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
+`PageTop` reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para
+la creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
según las necesidades de cada proyecto, incluyendo:
@@ -24,14 +24,14 @@ según las necesidades de cada proyecto, incluyendo:
* **Componentes** (*components*): encapsulan HTML, CSS y JavaScript en unidades funcionales,
configurables y reutilizables.
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
- de PageTop o de terceros.
+ de `PageTop` o de terceros.
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
componentes sin comprometer su funcionalidad.
# ⚡️ Guía rápida
-La aplicación más sencilla de PageTop se ve así:
+La aplicación más sencilla de `PageTop` se ve así:
```rust,no_run
use pagetop::prelude::*;
@@ -42,10 +42,10 @@ async fn main() -> std::io::Result<()> {
}
```
-Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
-bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
+Este código arranca el servidor de `PageTop`. Con la configuración por defecto, muestra una página
+de bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
-Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera:
+Para personalizar el servicio, se puede crear una extensión de `PageTop` de la siguiente manera:
```rust,no_run
use pagetop::prelude::*;
@@ -59,8 +59,8 @@ impl Extension for HelloWorld {
}
async fn hello_world(request: HttpRequest) -> ResultPage {
- Page::new(request)
- .add_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
+ Page::new(Some(request))
+ .with_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
.render()
}
@@ -86,15 +86,15 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
es la librería que permite incluir archivos estáticos en el ejecutable de las aplicaciones
- PageTop para servirlos de forma eficiente, con detección de cambios que optimizan el tiempo de
- compilación.
+ `PageTop` para servirlos de forma eficiente, con detección de cambios que optimizan el tiempo
+ de compilación.
* **[pagetop-build](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build)**,
prepara los archivos estáticos o archivos SCSS compilados para incluirlos en el binario de las
- aplicaciones PageTop durante la compilación de los ejecutables.
+ aplicaciones `PageTop` durante la compilación de los ejecutables.
* **[pagetop-macros](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros)**,
- proporciona una colección de macros que mejoran la experiencia de desarrollo con PageTop.
+ proporciona una colección de macros que mejoran la experiencia de desarrollo con `PageTop`.
# 🧪 Pruebas
@@ -116,7 +116,7 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
# 🚧 Advertencia
-**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
+`PageTop` es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
diff --git a/examples/hello-name.rs b/examples/hello-name.rs
index e1285d0..7a6db54 100644
--- a/examples/hello-name.rs
+++ b/examples/hello-name.rs
@@ -13,8 +13,8 @@ async fn hello_name(
path: service::web::Path,
) -> ResultPage {
let name = path.into_inner();
- Page::new(request)
- .add_component(Html::with(move |_| html! { h1 { "Hello " (name) "!" } }))
+ Page::new(Some(request))
+ .with_component(Html::with(move |_| html! { h1 { "Hello " (name) "!" } }))
.render()
}
diff --git a/examples/hello-world.rs b/examples/hello-world.rs
index d56f210..ba268dc 100644
--- a/examples/hello-world.rs
+++ b/examples/hello-world.rs
@@ -9,8 +9,8 @@ impl Extension for HelloWorld {
}
async fn hello_world(request: HttpRequest) -> ResultPage {
- Page::new(request)
- .add_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
+ Page::new(Some(request))
+ .with_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
.render()
}
diff --git a/helpers/pagetop-build/README.md b/helpers/pagetop-build/README.md
index 57273e8..80d6bba 100644
--- a/helpers/pagetop-build/README.md
+++ b/helpers/pagetop-build/README.md
@@ -113,7 +113,7 @@ impl Extension for MyExtension {
# 🚧 Advertencia
-**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
+`PageTop` es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
diff --git a/helpers/pagetop-macros/README.md b/helpers/pagetop-macros/README.md
index 7c9c2e8..e58d24c 100644
--- a/helpers/pagetop-macros/README.md
+++ b/helpers/pagetop-macros/README.md
@@ -26,12 +26,12 @@ Esta librería incluye entre sus macros una adaptación de
[SmartDefault](https://crates.io/crates/smart_default) (0.7.1) de
[Jane Doe](https://crates.io/users/jane-doe), llamada `AutoDefault`. Estas macros eliminan la
necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de
-cada proyecto PageTop.
+cada proyecto `PageTop`.
# 🚧 Advertencia
-**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
+`PageTop` es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs
index 5af5f9c..6421ca6 100644
--- a/helpers/pagetop-macros/src/lib.rs
+++ b/helpers/pagetop-macros/src/lib.rs
@@ -27,7 +27,7 @@ Esta librería incluye entre sus macros una adaptación de
[SmartDefault](https://crates.io/crates/smart_default) (0.7.1) de
[Jane Doe](https://crates.io/users/jane-doe), llamada `AutoDefault`. Estas macros eliminan la
necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de
-cada proyecto PageTop.
+cada proyecto `PageTop`.
*/
#).
#[proc_macro]
@@ -107,221 +107,119 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
/// `alter_...()`, que permitirá más adelante modificar instancias existentes.
#[proc_macro_attribute]
pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
- use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type};
-
- let ts: proc_macro2::TokenStream = item.clone().into();
-
- enum Kind {
- Impl(ImplItemFn),
- Trait(TraitItemFn),
- }
-
- // Detecta si estamos en `impl` o `trait`.
- let kind = if let Ok(it) = parse2::(ts.clone()) {
- Kind::Impl(it)
- } else if let Ok(tt) = parse2::(ts.clone()) {
- Kind::Trait(tt)
- } else {
- return quote! {
- compile_error!("#[builder_fn] only supports methods in `impl` blocks or `trait` items");
- }
- .into();
- };
-
- // Extrae piezas comunes (sig, attrs, vis, bloque?, es_trait?).
- let (sig, attrs, vis, body_opt, is_trait) = match &kind {
- Kind::Impl(m) => (&m.sig, &m.attrs, Some(&m.vis), Some(&m.block), false),
- Kind::Trait(t) => (&t.sig, &t.attrs, None, t.default.as_ref(), true),
- };
-
- let with_name = sig.ident.clone();
- let with_name_str = sig.ident.to_string();
+ let fn_with = parse_macro_input!(item as ItemFn);
+ let fn_with_name = fn_with.sig.ident.clone();
+ let fn_with_name_str = fn_with.sig.ident.to_string();
// Valida el nombre del método.
- if !with_name_str.starts_with("with_") {
- return quote_spanned! {
- sig.ident.span() => compile_error!("expected a named `with_...()` method");
- }
- .into();
- }
-
- // Sólo se exige `pub` en `impl` (en `trait` no aplica).
- let vis_pub = match (is_trait, vis) {
- (false, Some(v)) => quote! { #v },
- _ => quote! {},
- };
-
- // Validaciones comunes.
- if sig.asyncness.is_some() {
- return quote_spanned! {
- sig.asyncness.span() => compile_error!("`with_...()` cannot be `async`");
- }
- .into();
- }
- if sig.constness.is_some() {
- return quote_spanned! {
- sig.constness.span() => compile_error!("`with_...()` cannot be `const`");
- }
- .into();
- }
- if sig.abi.is_some() {
- return quote_spanned! {
- sig.abi.span() => compile_error!("`with_...()` cannot be `extern`");
- }
- .into();
- }
- if sig.unsafety.is_some() {
- return quote_spanned! {
- sig.unsafety.span() => compile_error!("`with_...()` cannot be `unsafe`");
- }
- .into();
- }
-
- // En `impl` se exige exactamente `mut self`; y en `trait` se exige `self` (sin &).
- let receiver_ok = match sig.inputs.first() {
- Some(FnArg::Receiver(r)) => {
- // Rechaza `self: SomeType`.
- if r.colon_token.is_some() {
- false
- } else if is_trait {
- // Exactamente `self` (sin &, sin mut).
- r.reference.is_none() && r.mutability.is_none()
- } else {
- // Exactamente `mut self`.
- r.reference.is_none() && r.mutability.is_some()
- }
- }
- _ => false,
- };
- if !receiver_ok {
- let msg = if is_trait {
- "expected `self` (not `mut self`, `&self` or `&mut self`) in trait method"
- } else {
- "expected first argument to be exactly `mut self`"
+ if !fn_with_name_str.starts_with("with_") {
+ let expanded = quote_spanned! {
+ fn_with.sig.ident.span() =>
+ compile_error!("expected a \"pub fn with_...(mut self, ...) -> Self\" method");
};
- let err = sig
- .inputs
- .first()
- .map(|a| a.span())
- .unwrap_or(sig.ident.span());
+ return expanded.into();
+ }
+ // Valida que el método es público.
+ if !matches!(fn_with.vis, syn::Visibility::Public(_)) {
return quote_spanned! {
- err => compile_error!(#msg);
+ fn_with.sig.ident.span() => compile_error!("expected method to be `pub`");
}
.into();
}
-
- // Valida que el método devuelve exactamente `Self`.
- match &sig.output {
- ReturnType::Type(_, ty) => match ty.as_ref() {
- Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self") => {}
- _ => {
- return quote_spanned! {
- ty.span() => compile_error!("expected return type to be exactly `Self`");
- }
- .into();
- }
- },
- _ => {
+ // Valida que el primer argumento es exactamente `mut self`.
+ if let Some(syn::FnArg::Receiver(receiver)) = fn_with.sig.inputs.first() {
+ if receiver.mutability.is_none() || receiver.reference.is_some() {
return quote_spanned! {
- sig.output.span() => compile_error!("expected return type to be exactly `Self`");
+ receiver.span() => compile_error!("expected `mut self` as the first argument");
}
.into();
}
+ } else {
+ return quote_spanned! {
+ fn_with.sig.ident.span() => compile_error!("expected `mut self` as the first argument");
+ }
+ .into();
+ }
+ // Valida que el método devuelve exactamente `Self`.
+ if let syn::ReturnType::Type(_, ty) = &fn_with.sig.output {
+ if let syn::Type::Path(type_path) = ty.as_ref() {
+ if type_path.qself.is_some() || !type_path.path.is_ident("Self") {
+ return quote_spanned! { ty.span() =>
+ compile_error!("expected return type to be exactly `Self`");
+ }
+ .into();
+ }
+ } else {
+ return quote_spanned! { ty.span() =>
+ compile_error!("expected return type to be exactly `Self`");
+ }
+ .into();
+ }
+ } else {
+ return quote_spanned! {
+ fn_with.sig.output.span() => compile_error!("expected method to return `Self`");
+ }
+ .into();
}
// Genera el nombre del método alter_...().
- let stem = with_name_str.strip_prefix("with_").expect("validated");
- let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span());
+ let fn_alter_name_str = fn_with_name_str.replace("with_", "alter_");
+ let fn_alter_name = syn::Ident::new(&fn_alter_name_str, fn_with.sig.ident.span());
// Extrae genéricos y cláusulas where.
- let generics = &sig.generics;
- let where_clause = &sig.generics.where_clause;
+ let fn_generics = &fn_with.sig.generics;
+ let where_clause = &fn_with.sig.generics.where_clause;
- // Extrae identificadores de los argumentos para la llamada (sin `mut` ni patrones complejos).
- let args: Vec<_> = sig.inputs.iter().skip(1).collect();
- let call_idents: Vec = {
- let mut v = Vec::new();
- for arg in sig.inputs.iter().skip(1) {
- match arg {
- FnArg::Typed(pat) => {
- if let Pat::Ident(pat_ident) = pat.pat.as_ref() {
- v.push(pat_ident.ident.clone());
- } else {
- return quote_spanned! {
- pat.pat.span() => compile_error!(
- "each parameter must be a simple identifier, e.g. `value: T`"
- );
- }
- .into();
- }
- }
- _ => {
- return quote_spanned! {
- arg.span() => compile_error!("unexpected receiver in parameter list");
- }
- .into();
- }
- }
- }
- v
- };
-
- // Extrae atributos descartando la documentación para incluir en `alter_...()`.
- let non_doc_attrs: Vec<_> = attrs
+ // Extrae argumentos y parámetros de llamada.
+ let args: Vec<_> = fn_with.sig.inputs.iter().skip(1).collect();
+ let params: Vec<_> = fn_with
+ .sig
+ .inputs
.iter()
- .filter(|&a| !a.path().is_ident("doc"))
- .cloned()
+ .skip(1)
+ .map(|arg| match arg {
+ syn::FnArg::Typed(pat) => &pat.pat,
+ _ => panic!("unexpected argument type"),
+ })
.collect();
- // Documentación del método alter_...().
- let alter_doc =
- format!("Equivalente a [`Self::{with_name_str}()`], pero fuera del patrón *builder*.");
+ // Extrae bloque del método.
+ let fn_with_block = &fn_with.block;
+
+ // Extrae documentación y otros atributos del método.
+ let fn_with_attrs = &fn_with.attrs;
+
+ // Genera el método alter_...() con el código del método with_...().
+ let fn_alter_doc =
+ format!("Igual que [`Self::{fn_with_name_str}()`], pero sin usar el patrón *builder*.");
+
+ let fn_alter = quote! {
+ #[doc = #fn_alter_doc]
+ pub fn #fn_alter_name #fn_generics(&mut self, #(#args),*) -> &mut Self #where_clause {
+ #fn_with_block
+ }
+ };
+
+ // Redefine el método with_...() para que llame a alter_...().
+ let fn_with = quote! {
+ #(#fn_with_attrs)*
+ #[inline]
+ pub fn #fn_with_name #fn_generics(mut self, #(#args),*) -> Self #where_clause {
+ self.#fn_alter_name(#(#params),*);
+ self
+ }
+ };
// Genera el código final.
- let expanded = match body_opt {
- None => {
- quote! {
- #(#attrs)*
- fn #with_name #generics (self, #(#args),*) -> Self #where_clause;
-
- #(#non_doc_attrs)*
- #[doc = #alter_doc]
- fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause;
- }
- }
- Some(body) => {
- let with_fn = if is_trait {
- quote! {
- #vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause {
- let mut s = self;
- s.#alter_ident(#(#call_idents),*);
- s
- }
- }
- } else {
- quote! {
- #vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause {
- self.#alter_ident(#(#call_idents),*);
- self
- }
- }
- };
- quote! {
- #(#attrs)*
- #with_fn
-
- #(#non_doc_attrs)*
- #[doc = #alter_doc]
- #vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause {
- #body
- }
- }
- }
+ let expanded = quote! {
+ #fn_with
+ #[inline]
+ #fn_alter
};
expanded.into()
}
-/// Define una función `main` asíncrona como punto de entrada de PageTop.
+/// Define una función `main` asíncrona como punto de entrada de `PageTop`.
///
/// # Ejemplo
///
@@ -342,7 +240,7 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
output
}
-/// Define funciones de prueba asíncronas para usar con PageTop.
+/// Define funciones de prueba asíncronas para usar con `PageTop`.
///
/// # Ejemplo
///
diff --git a/helpers/pagetop-statics/README.md b/helpers/pagetop-statics/README.md
index 4168cd4..92999c0 100644
--- a/helpers/pagetop-statics/README.md
+++ b/helpers/pagetop-statics/README.md
@@ -16,7 +16,7 @@ configurables, basadas en HTML, CSS y JavaScript.
## Descripción general
-Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
+Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones `PageTop` para
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
compilación.
@@ -28,13 +28,13 @@ Para ello, adapta el código de los *crates* [static-files](https://crates.io/cr
[4.0.1](https://github.com/kilork/actix-web-static-files/tree/v4.0.1)), desarrollados ambos por
[Alexander Korolev](https://crates.io/users/kilork).
-Estas implementaciones se integran en PageTop para evitar que cada proyecto tenga que declarar
+Estas implementaciones se integran en `PageTop` para evitar que cada proyecto tenga que declarar
`static-files` manualmente como dependencia en su `Cargo.toml`.
# 🚧 Advertencia
-**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
+`PageTop` es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
diff --git a/helpers/pagetop-statics/src/lib.rs b/helpers/pagetop-statics/src/lib.rs
index 201d90e..dab50d9 100644
--- a/helpers/pagetop-statics/src/lib.rs
+++ b/helpers/pagetop-statics/src/lib.rs
@@ -17,7 +17,7 @@ configurables, basadas en HTML, CSS y JavaScript.
## Descripción general
-Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
+Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones `PageTop` para
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
compilación.
@@ -29,7 +29,7 @@ Para ello, adapta el código de los *crates* [static-files](https://crates.io/cr
[4.0.1](https://github.com/kilork/actix-web-static-files/tree/v4.0.1)), desarrollados ambos por
[Alexander Korolev](https://crates.io/users/kilork).
-Estas implementaciones se integran en PageTop para evitar que cada proyecto tenga que declarar
+Estas implementaciones se integran en `PageTop` para evitar que cada proyecto tenga que declarar
`static-files` manualmente como dependencia en su `Cargo.toml`.
*/
diff --git a/src/app.rs b/src/app.rs
index c8ffba1..c3576fc 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,11 +1,8 @@
-//! Prepara y ejecuta una aplicación creada con PageTop.
+//! Prepara y ejecuta una aplicación creada con `Pagetop`.
mod figfont;
use crate::core::{extension, extension::ExtensionRef};
-use crate::html::Markup;
-use crate::response::page::{ErrorPage, ResultPage};
-use crate::service::HttpRequest;
use crate::{global, locale, service, trace};
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
@@ -17,7 +14,7 @@ use substring::Substring;
use std::io::Error;
use std::sync::LazyLock;
-/// Punto de entrada de una aplicación PageTop.
+/// Punto de entrada de una aplicación `PageTop`.
///
/// No almacena datos, **encapsula** el inicio completo de configuración y puesta en marcha. Para
/// instanciarla se puede usar [`new()`](Application::new) o [`prepare()`](Application::prepare).
@@ -84,7 +81,7 @@ impl Application {
if let Some((Width(term_width), _)) = terminal_size() {
if term_width >= 80 {
let maxlen: usize = ((term_width / 10) - 2).into();
- let mut app = app_name.substring(0, maxlen).to_string();
+ let mut app = app_name.substring(0, maxlen).to_owned();
if app_name.len() > maxlen {
app = format!("{app}...");
}
@@ -173,12 +170,6 @@ impl Application {
InitError = (),
>,
> {
- service::App::new()
- .configure(extension::all::configure_services)
- .default_service(service::web::route().to(service_not_found))
+ service::App::new().configure(extension::all::configure_services)
}
}
-
-async fn service_not_found(request: HttpRequest) -> ResultPage {
- Err(ErrorPage::NotFound(request))
-}
diff --git a/src/base/action.rs b/src/base/action.rs
index 977ae9e..be35e92 100644
--- a/src/base/action.rs
+++ b/src/base/action.rs
@@ -1,4 +1,4 @@
-//! Acciones predefinidas para alterar el funcionamiento interno de PageTop.
+//! Acciones predefinidas para alterar el funcionamiento interno de `PageTop`.
use crate::prelude::*;
diff --git a/src/base/action/component/after_render_component.rs b/src/base/action/component/after_render_component.rs
index 0cb0334..917f322 100644
--- a/src/base/action/component/after_render_component.rs
+++ b/src/base/action/component/after_render_component.rs
@@ -6,7 +6,7 @@ use crate::base::action::FnActionWithComponent;
pub struct AfterRender {
f: FnActionWithComponent,
referer_type_id: Option,
- referer_id: AttrId,
+ referer_id: OptionId,
weight: Weight,
}
@@ -34,7 +34,7 @@ impl AfterRender {
AfterRender {
f,
referer_type_id: Some(UniqueId::of::()),
- referer_id: AttrId::default(),
+ referer_id: OptionId::default(),
weight: 0,
}
}
diff --git a/src/base/action/component/before_render_component.rs b/src/base/action/component/before_render_component.rs
index 46ff9aa..8c2e38d 100644
--- a/src/base/action/component/before_render_component.rs
+++ b/src/base/action/component/before_render_component.rs
@@ -6,7 +6,7 @@ use crate::base::action::FnActionWithComponent;
pub struct BeforeRender {
f: FnActionWithComponent,
referer_type_id: Option,
- referer_id: AttrId,
+ referer_id: OptionId,
weight: Weight,
}
@@ -34,7 +34,7 @@ impl BeforeRender {
BeforeRender {
f,
referer_type_id: Some(UniqueId::of::()),
- referer_id: AttrId::default(),
+ referer_id: OptionId::default(),
weight: 0,
}
}
diff --git a/src/base/action/component/is_renderable.rs b/src/base/action/component/is_renderable.rs
index 5a0e244..baa86f1 100644
--- a/src/base/action/component/is_renderable.rs
+++ b/src/base/action/component/is_renderable.rs
@@ -11,7 +11,7 @@ pub type FnIsRenderable = fn(component: &C, cx: &Context) -> bool;
pub struct IsRenderable {
f: FnIsRenderable,
referer_type_id: Option,
- referer_id: AttrId,
+ referer_id: OptionId,
weight: Weight,
}
@@ -39,7 +39,7 @@ impl IsRenderable {
IsRenderable {
f,
referer_type_id: Some(UniqueId::of::()),
- referer_id: AttrId::default(),
+ referer_id: OptionId::default(),
weight: 0,
}
}
diff --git a/src/base/component.rs b/src/base/component.rs
index 4df64ff..27f0f73 100644
--- a/src/base/component.rs
+++ b/src/base/component.rs
@@ -1,10 +1,4 @@
-//! Componentes nativos proporcionados por PageTop.
+//! Componentes nativos proporcionados por `PageTop`.
mod html;
pub use html::Html;
-
-mod block;
-pub use block::Block;
-
-mod poweredby;
-pub use poweredby::PoweredBy;
diff --git a/src/base/component/block.rs b/src/base/component/block.rs
deleted file mode 100644
index c96f2ba..0000000
--- a/src/base/component/block.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-use crate::prelude::*;
-
-/// Componente genérico que representa un bloque de contenido.
-///
-/// Los bloques se utilizan como contenedores de otros componentes o contenidos, con un título
-/// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*).
-#[rustfmt::skip]
-#[derive(AutoDefault)]
-pub struct Block {
- id : AttrId,
- classes : AttrClasses,
- title : L10n,
- children: Children,
-}
-
-impl Component for Block {
- fn new() -> Self {
- Block::default()
- }
-
- fn id(&self) -> Option {
- self.id.get()
- }
-
- fn setup_before_prepare(&mut self, _cx: &mut Context) {
- self.alter_classes(ClassesOp::Prepend, "block");
- }
-
- fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
- let block_body = self.children().render(cx);
-
- if block_body.is_empty() {
- return PrepareMarkup::None;
- }
-
- let id = cx.required_id::(self.id());
-
- PrepareMarkup::With(html! {
- div id=(id) class=[self.classes().get()] {
- @if let Some(title) = self.title().lookup(cx) {
- h2 class="block__title" { span { (title) } }
- }
- div class="block__body" { (block_body) }
- }
- })
- }
-}
-
-impl Block {
- // Block BUILDER *******************************************************************************
-
- /// Establece el identificador único (`id`) del bloque.
- #[builder_fn]
- pub fn with_id(mut self, id: impl AsRef) -> Self {
- self.id.alter_value(id);
- self
- }
-
- /// Modifica la lista de clases CSS aplicadas al bloque.
- #[builder_fn]
- pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self {
- self.classes.alter_value(op, classes);
- self
- }
-
- /// Establece el título del bloque.
- #[builder_fn]
- pub fn with_title(mut self, title: L10n) -> Self {
- self.title = title;
- self
- }
-
- /// Añade un nuevo componente hijo al bloque.
- pub fn add_component(mut self, component: impl Component) -> Self {
- self.children
- .alter_child(ChildOp::Add(Child::with(component)));
- self
- }
-
- /// Modifica la lista de hijos (`children`) aplicando una operación.
- #[builder_fn]
- pub fn with_child(mut self, op: ChildOp) -> Self {
- self.children.alter_child(op);
- self
- }
-
- // Block GETTERS *******************************************************************************
-
- /// Devuelve las clases CSS asociadas al bloque.
- pub fn classes(&self) -> &AttrClasses {
- &self.classes
- }
-
- /// Devuelve el título del bloque como [`L10n`].
- pub fn title(&self) -> &L10n {
- &self.title
- }
-
- /// Devuelve la lista de hijos (`children`) del bloque.
- pub fn children(&self) -> &Children {
- &self.children
- }
-}
diff --git a/src/base/component/html.rs b/src/base/component/html.rs
index 8fa5690..8f273ed 100644
--- a/src/base/component/html.rs
+++ b/src/base/component/html.rs
@@ -25,7 +25,7 @@ use crate::prelude::*;
/// use pagetop::prelude::*;
///
/// let component = Html::with(|cx| {
-/// let user = cx.param::("username").cloned().unwrap_or("visitor".to_string());
+/// let user = cx.get_param::("username").unwrap_or(String::from("visitor"));
/// html! {
/// h1 { "Hello, " (user) }
/// }
@@ -44,13 +44,11 @@ impl Component for Html {
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
- PrepareMarkup::With(self.html(cx))
+ PrepareMarkup::With((self.0)(cx))
}
}
impl Html {
- // Html BUILDER ********************************************************************************
-
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
///
/// El método [`prepare_component()`](crate::core::component::Component::prepare_component)
@@ -68,24 +66,11 @@ impl Html {
/// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando
/// [`prepare_component()`](crate::core::component::Component::prepare_component) invoque esta
/// instancia. La nueva función también recibe una referencia al contexto ([`Context`]).
- #[builder_fn]
- pub fn with_fn(mut self, f: F) -> Self
+ pub fn alter_html(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
{
self.0 = Box::new(f);
self
}
-
- // Html GETTERS ********************************************************************************
-
- /// Aplica la función interna de renderizado con el [`Context`] proporcionado.
- ///
- /// Normalmente no se invoca manualmente, ya que el proceso de renderizado de los componentes lo
- /// invoca automáticamente durante la construcción de la página. Puede usarse, no obstante, para
- /// sobrescribir [`prepare_component()`](crate::core::component::Component::prepare_component)
- /// y alterar el comportamiento del componente.
- pub fn html(&self, cx: &mut Context) -> Markup {
- (self.0)(cx)
- }
}
diff --git a/src/base/component/poweredby.rs b/src/base/component/poweredby.rs
deleted file mode 100644
index bfe3835..0000000
--- a/src/base/component/poweredby.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-use crate::prelude::*;
-
-// Enlace a la página oficial de PageTop.
-const LINK: &str = "PageTop";
-
-/// Componente que renderiza la sección 'Powered by' (*Funciona con*) típica del pie de página.
-///
-/// Por defecto, usando [`default()`](Self::default) sólo se muestra un reconocimiento a PageTop.
-/// Sin embargo, se puede usar [`new()`](Self::new) para crear una instancia con un texto de
-/// copyright predeterminado.
-#[derive(AutoDefault)]
-pub struct PoweredBy {
- copyright: Option,
-}
-
-impl Component for PoweredBy {
- /// Crea una nueva instancia de `PoweredBy`.
- ///
- /// El copyright se genera automáticamente con el año actual y el nombre de la aplicación
- /// configurada en [`global::SETTINGS`].
- fn new() -> Self {
- let year = Utc::now().format("%Y").to_string();
- let c = join!(year, " © ", global::SETTINGS.app.name);
- PoweredBy { copyright: Some(c) }
- }
-
- fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
- PrepareMarkup::With(html! {
- div id=[self.id()] class="poweredby" {
- @if let Some(c) = self.copyright() {
- span class="poweredby__copyright" { (c) "." } " "
- }
- span class="poweredby__pagetop" {
- (L10n::l("poweredby_pagetop").with_arg("pagetop_link", LINK).using(cx))
- }
- }
- })
- }
-}
-
-impl PoweredBy {
- // PoweredBy BUILDER ***************************************************************************
-
- /// Establece el texto de copyright que mostrará el componente.
- ///
- /// Al pasar `Some(valor)` se sobrescribe el texto de copyright por defecto. Al pasar `None` se
- /// eliminará, pero en este caso es necesario especificar el tipo explícitamente:
- ///
- /// ```rust
- /// use pagetop::prelude::*;
- ///
- /// let p1 = PoweredBy::default().with_copyright(Some("2001 © Foo Inc."));
- /// let p2 = PoweredBy::new().with_copyright(None::);
- /// ```
- #[builder_fn]
- pub fn with_copyright(mut self, copyright: Option>) -> Self {
- self.copyright = copyright.map(Into::into);
- self
- }
-
- // PoweredBy GETTERS ***************************************************************************
-
- /// Devuelve el texto de copyright actual, si existe.
- pub fn copyright(&self) -> Option<&str> {
- self.copyright.as_deref()
- }
-}
diff --git a/src/base/extension.rs b/src/base/extension.rs
index 1f94fe2..49e408d 100644
--- a/src/base/extension.rs
+++ b/src/base/extension.rs
@@ -1,4 +1,4 @@
-//! Extensiones para funcionalidades avanzadas de PageTop.
+//! Extensiones para funcionalidades avanzadas de `PageTop`.
mod welcome;
pub use welcome::Welcome;
diff --git a/src/base/extension/welcome.rs b/src/base/extension/welcome.rs
index 5c6fec5..3dda43e 100644
--- a/src/base/extension/welcome.rs
+++ b/src/base/extension/welcome.rs
@@ -1,6 +1,6 @@
use crate::prelude::*;
-/// Página de bienvenida predeterminada de PageTop.
+/// Página de bienvenida predeterminada de `PageTop`.
///
/// Esta extensión se instala por defecto y muestra una página en la ruta raíz (`/`) cuando no se ha
/// configurado ninguna página de inicio personalizada. Permite confirmar que el servidor está
@@ -24,32 +24,93 @@ impl Extension for Welcome {
async fn homepage(request: HttpRequest) -> ResultPage {
let app = &global::SETTINGS.app.name;
- Page::new(request)
+ Page::new(Some(request))
+ .with_title(L10n::l("welcome_page"))
.with_theme("Basic")
- .with_layout("PageTopIntro")
- .with_title(L10n::l("welcome_title"))
- .with_description(L10n::l("welcome_intro").with_arg("app", app))
- .with_param("intro_button_txt", L10n::l("welcome_powered"))
- .with_param("intro_button_lnk", "https://pagetop.cillero.es".to_string())
- .add_component(
- Block::new()
- .with_title(L10n::l("welcome_status_title"))
- .add_component(Html::with(move |cx| {
- html! {
- p { (L10n::l("welcome_status_1").using(cx)) }
- p { (L10n::l("welcome_status_2").using(cx)) }
+ .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css")))
+ .with_component(Html::with(move |cx| html! {
+ div id="main-header" {
+ header {
+ h1
+ id="header-title"
+ aria-label=(L10n::l("welcome_aria").with_arg("app", app).to_markup(cx))
+ {
+ span { (L10n::l("welcome_title").to_markup(cx)) }
+ (L10n::l("welcome_intro").with_arg("app", app).to_markup(cx))
}
- })),
- )
- .add_component(
- Block::new()
- .with_title(L10n::l("welcome_support_title"))
- .add_component(Html::with(move |cx| {
- html! {
- p { (L10n::l("welcome_support_1").using(cx)) }
- p { (L10n::l("welcome_support_2").with_arg("app", app).using(cx)) }
+ }
+ aside id="header-image" aria-hidden="true" {
+ div id="monster" {
+ picture {
+ source
+ type="image/avif"
+ src="/img/monster-pagetop_250.avif"
+ srcset="/img/monster-pagetop_500.avif 1.5x";
+ source
+ type="image/webp"
+ src="/img/monster-pagetop_250.webp"
+ srcset="/img/monster-pagetop_500.webp 1.5x";
+ img
+ src="/img/monster-pagetop_250.png"
+ srcset="/img/monster-pagetop_500.png 1.5x"
+ alt="Monster PageTop";
+ }
}
- })),
- )
+ }
+ }
+
+ main id="main-content" {
+ section class="content-body" {
+ div id="poweredby-button" {
+ a
+ id="poweredby-link"
+ href="https://pagetop.cillero.es"
+ target="_blank"
+ rel="noreferrer"
+ {
+ span {} span {} span {}
+ div id="poweredby-text" { (L10n::l("welcome_powered").to_markup(cx)) }
+ }
+ }
+ div class="content-text" {
+ p { (L10n::l("welcome_text1").to_markup(cx)) }
+ p { (L10n::l("welcome_text2").to_markup(cx)) }
+
+ div class="subcontent" {
+ h1 { span { (L10n::l("welcome_about").to_markup(cx)) } }
+ p { (L10n::l("welcome_pagetop").to_markup(cx)) }
+ p { (L10n::l("welcome_issues1").to_markup(cx)) }
+ p { (L10n::l("welcome_issues2").with_arg("app", app).to_markup(cx)) }
+ }
+ }
+ }
+ }
+
+ footer id="footer" {
+ section class="footer-inner" {
+ div class="footer-logo" {
+ svg
+ viewBox="0 0 1614 1614"
+ xmlns="http://www.w3.org/2000/svg"
+ role="img"
+ aria-label=[L10n::l("pagetop_logo").using(cx)]
+ preserveAspectRatio="xMidYMid slice"
+ focusable="false"
+ {
+ path fill="rgb(255,255,255)" d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {}
+ path fill="rgb(255,255,255)" d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {}
+ path fill="rgb(255,255,255)" d="M 1071,648 C 998,648 939,707 939,780 939,853 998,912 1071,912 1144,912 1203,853 1203,780 1203,707 1144,648 1071,648 L 1071,648 1071,648 Z M 1071,859 C 1027,859 992,824 992,780 992,736 1027,701 1071,701 1115,701 1150,736 1150,780 1150,824 1115,859 1071,859 L 1071,859 1071,859 Z" {}
+ }
+ }
+ div class="footer-links" {
+ a href="https://crates.io/crates/pagetop" target="_blank" rel="noreferrer" { ("Crates.io") }
+ a href="https://docs.rs/pagetop" target="_blank" rel="noreferrer" { ("Docs.rs") }
+ a href="https://git.cillero.es/manuelcillero/pagetop" target="_blank" rel="noreferrer" { (L10n::l("welcome_code").to_markup(cx)) }
+ em { (L10n::l("welcome_have_fun").to_markup(cx)) }
+ }
+ }
+ }
+
+ }))
.render()
}
diff --git a/src/base/theme.rs b/src/base/theme.rs
index 40129bf..ea9eeb6 100644
--- a/src/base/theme.rs
+++ b/src/base/theme.rs
@@ -1,4 +1,4 @@
-//! Temas básicos soportados por PageTop.
+//! Temas básicos soportados por `PageTop`.
mod basic;
pub use basic::Basic;
diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs
index 2f49274..b02abfb 100644
--- a/src/base/theme/basic.rs
+++ b/src/base/theme/basic.rs
@@ -1,33 +1,8 @@
-/// Es el tema básico que incluye PageTop por defecto.
+//! Es el tema básico que incluye `PageTop` por defecto.
+
use crate::prelude::*;
/// Tema básico por defecto.
-///
-/// Ofrece las siguientes composiciones (*layouts*):
-///
-/// - **Composición predeterminada**
-/// - Renderizado genérico con
-/// [`ThemePage::render_body()`](crate::core::theme::ThemePage::render_body) usando las regiones
-/// predefinidas en [`page_regions()`](crate::core::theme::Theme::page_regions).
-///
-/// - **`Intro`**
-/// - Página de entrada con cabecera visual, título y descripción y un botón opcional de llamada a
-/// la acción. Ideal para una página de inicio o bienvenida en el contexto de PageTop.
-/// - **Regiones:** `content` (se renderiza dentro de `.intro-content__body`).
-/// - **Parámetros:**
-/// - `intro_button_txt` (`L10n`) – Texto del botón.
-/// - `intro_button_lnk` (`Option`) – URL del botón; si no se indica, el botón no se
-/// muestra.
-///
-/// - **`PageTopIntro`**
-/// - Variante de `Intro` con textos predefinidos sobre PageTop al inicio del contenido. Añade una
-/// banda de *badges* con la versión de [PageTop en crates.io](https://crates.io/crates/pagetop)
-/// más la fecha de la última versión publicada y la licencia de uso.
-/// - **Regiones:** `content` (igual que `Intro`).
-/// - **Parámetros:** los mismos que `Intro`.
-///
-/// **Nota:** si no se especifica `layout` o el valor no coincide con ninguno de los anteriores, se
-/// aplica la composición predeterminada.
pub struct Basic;
impl Extension for Basic {
@@ -37,152 +12,11 @@ impl Extension for Basic {
}
impl Theme for Basic {
- fn render_page_body(&self, page: &mut Page) -> Markup {
- match page.layout() {
- "Intro" => render_intro(page),
- "PageTopIntro" => render_pagetop_intro(page),
- _ => ::render_body(self, page, self.page_regions()),
- }
- }
-
fn after_render_page_body(&self, page: &mut Page) {
- let styles = match page.layout() {
- "Intro" => "/css/intro.css",
- "PageTopIntro" => "/css/intro.css",
- _ => "/css/basic.css",
- };
page.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/css/normalize.css")
.with_version("8.0.1")
.with_weight(-99),
- ))
- .alter_assets(AssetsOp::AddStyleSheet(
- StyleSheet::from(styles)
- .with_version(env!("CARGO_PKG_VERSION"))
- .with_weight(-99),
));
}
}
-
-fn render_intro(page: &mut Page) -> Markup {
- let title = page.title().unwrap_or_default();
- let intro = page.description().unwrap_or_default();
-
- let intro_button_txt: L10n = page.param_or_default("intro_button_txt");
- let intro_button_lnk: Option<&String> = page.param("intro_button_lnk");
-
- html! {
- body id=[page.body_id().get()] class=[page.body_classes().get()] {
- header class="intro-header" {
- section class="intro-header__body" {
- h1 class="intro-header__title" {
- span { (title) }
- (intro)
- }
- }
- aside class="intro-header__image" aria-hidden="true" {
- div class="intro-header__monster" {
- picture {
- source
- type="image/avif"
- src="/img/monster-pagetop_250.avif"
- srcset="/img/monster-pagetop_500.avif 1.5x";
- source
- type="image/webp"
- src="/img/monster-pagetop_250.webp"
- srcset="/img/monster-pagetop_500.webp 1.5x";
- img
- src="/img/monster-pagetop_250.png"
- srcset="/img/monster-pagetop_500.png 1.5x"
- alt="Monster PageTop";
- }
- }
- }
- }
- main class="intro-content" {
- section class="intro-content__body" {
- @if intro_button_lnk.is_some() {
- div class="intro-button" {
- a
- class="intro-button__link"
- href=[intro_button_lnk]
- target="_blank"
- rel="noreferrer"
- {
- span {} span {} span {}
- div class="intro-button__text" {
- (intro_button_txt.using(page))
- }
- }
- }
- }
- div class="intro-text" { (page.render_region("content")) }
- }
- }
- footer class="intro-footer" {
- section class="intro-footer__body" {
- div class="intro-footer__logo" {
- svg
- viewBox="0 0 1614 1614"
- xmlns="http://www.w3.org/2000/svg"
- role="img"
- aria-label=[L10n::l("pagetop_logo").lookup(page)]
- preserveAspectRatio="xMidYMid slice"
- focusable="false"
- {
- path fill="rgb(255,255,255)" d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {}
- path fill="rgb(255,255,255)" d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {}
- path fill="rgb(255,255,255)" d="M 1071,648 C 998,648 939,707 939,780 939,853 998,912 1071,912 1144,912 1203,853 1203,780 1203,707 1144,648 1071,648 L 1071,648 1071,648 Z M 1071,859 C 1027,859 992,824 992,780 992,736 1027,701 1071,701 1115,701 1150,736 1150,780 1150,824 1115,859 1071,859 L 1071,859 1071,859 Z" {}
- }
- }
- div class="intro-footer__links" {
- a href="https://crates.io/crates/pagetop" target="_blank" rel="noreferrer" { ("Crates.io") }
- a href="https://docs.rs/pagetop" target="_blank" rel="noreferrer" { ("Docs.rs") }
- a href="https://git.cillero.es/manuelcillero/pagetop" target="_blank" rel="noreferrer" { (L10n::l("intro_code").using(page)) }
- em { (L10n::l("intro_have_fun").using(page)) }
- }
- }
- }
- }
- }
-}
-
-fn render_pagetop_intro(page: &mut Page) -> Markup {
- page.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
- util::indoc!(r#"
- try {
- const resp = await fetch("https://crates.io/api/v1/crates/pagetop");
- const data = await resp.json();
- const date = new Date(data.versions[0].created_at);
- const formatted = date.toLocaleDateString("LANGID", { year: "numeric", month: "2-digit", day: "2-digit" });
- document.getElementById("intro-release").src = `https://img.shields.io/badge/Release%20date-${encodeURIComponent(formatted)}-blue?label=LABEL&style=for-the-badge`;
- document.getElementById("intro-badges").style.display = "block";
- } catch (e) {
- console.error("Failed to fetch release date from crates.io:", e);
- }
- "#)
- .replace("LANGID", cx.langid().to_string().as_str())
- .replace("LABEL", L10n::l("intro_release_label").using(cx).as_str())
- .to_string(),
- )))
- .alter_child_in("content", ChildOp::Prepend(Child::with(Html::with(|cx| html! {
- p { (L10n::l("intro_text1").using(cx)) }
- div id="intro-badges" style="display: none; margin-bottom: 1.1rem;" {
- img
- src="https://img.shields.io/crates/v/pagetop.svg?label=PageTop&style=for-the-badge"
- alt=[L10n::l("intro_pagetop_label").lookup(cx)] {} (" ")
- img
- id="intro-release"
- alt=[L10n::l("intro_release_label").lookup(cx)] {} (" ")
- img
- src=(format!(
- "https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label={}&style=for-the-badge",
- L10n::l("intro_license_label").lookup(cx).unwrap_or_default()
- ))
- alt=[L10n::l("intro_license_label").lookup(cx)] {}
- }
- p { (L10n::l("intro_text2").using(cx)) }
- }))));
-
- render_intro(page)
-}
diff --git a/src/config.rs b/src/config.rs
index f2fb9f7..27cf630 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -3,7 +3,7 @@
//! Estos ajustes se obtienen de archivos [TOML](https://toml.io) como pares `clave = valor` que se
//! mapean a estructuras **fuertemente tipadas** y valores predefinidos.
//!
-//! Siguiendo la metodología [Twelve-Factor App](https://12factor.net/config), PageTop separa el
+//! Siguiendo la metodología [Twelve-Factor App](https://12factor.net/config), `PageTop` separa el
//! **código** de la **configuración**, lo que permite tener configuraciones diferentes para cada
//! despliegue, como *dev*, *staging* o *production*, sin modificar el código fuente.
//!
@@ -13,14 +13,14 @@
//! Si tu aplicación necesita archivos de configuración, crea un directorio `config` en la raíz del
//! proyecto, al mismo nivel que el archivo *Cargo.toml* o que el binario de la aplicación.
//!
-//! PageTop carga en este orden, y siempre de forma opcional, los siguientes archivos TOML:
+//! `PageTop` carga en este orden, y siempre de forma opcional, los siguientes archivos TOML:
//!
//! 1. **config/common.toml**, para ajustes comunes a todos los entornos. Este enfoque simplifica el
//! mantenimiento al centralizar los valores de configuración comunes.
//!
//! 2. **config/{rm}.toml**, donde `{rm}` es el valor de la variable de entorno `PAGETOP_RUN_MODE`:
//!
-//! * Si `PAGETOP_RUN_MODE` no está definida, se asume el valor `default`, y PageTop intentará
+//! * Si `PAGETOP_RUN_MODE` no está definida, se asume el valor `default`, y `PageTop` intentará
//! cargar *config/default.toml* si el archivo existe.
//!
//! * Útil para definir configuraciones específicas por entorno, garantizando que cada uno (p.ej.
diff --git a/src/core.rs b/src/core.rs
index 79d9207..0c8aa21 100644
--- a/src/core.rs
+++ b/src/core.rs
@@ -117,7 +117,7 @@ impl TypeInfo {
///
/// Este *trait* se implementa automáticamente para **todos** los tipos que implementen [`Any`], de
/// modo que basta con traer [`AnyInfo`] al ámbito (`use crate::AnyInfo;`) para disponer de estos
-/// métodos adicionales, o usar el [`prelude`](crate::prelude) de PageTop.
+/// métodos adicionales, o usar el [`prelude`](crate::prelude) de `PageTop`.
///
/// # Ejemplo
///
diff --git a/src/core/component.rs b/src/core/component.rs
index 3691472..17b9b73 100644
--- a/src/core/component.rs
+++ b/src/core/component.rs
@@ -7,6 +7,3 @@ mod children;
pub use children::Children;
pub use children::{Child, ChildOp};
pub use children::{Typed, TypedOp};
-
-mod slot;
-pub use slot::TypedSlot;
diff --git a/src/core/component/children.rs b/src/core/component/children.rs
index cb112e1..fb85db7 100644
--- a/src/core/component/children.rs
+++ b/src/core/component/children.rs
@@ -9,13 +9,13 @@ use std::vec::IntoIter;
/// Representa un componente encapsulado de forma segura y compartida.
///
-/// Esta estructura permite manipular y renderizar un componente que implemente [`Component`], y
-/// habilita acceso concurrente mediante [`Arc>`].
+/// Esta estructura permite manipular y renderizar cualquier tipo que implemente [`Component`],
+/// garantizando acceso concurrente a través de [`Arc>`].
#[derive(Clone)]
pub struct Child(Arc>);
impl Child {
- /// Crea un nuevo `Child` a partir de un componente.
+ /// Crea un nuevo [`Child`] a partir de un componente.
pub fn with(component: impl Component) -> Self {
Child(Arc::new(RwLock::new(component)))
}
@@ -46,8 +46,7 @@ impl Child {
/// Variante tipada de [`Child`] para evitar conversiones durante el uso.
///
-/// Esta estructura permite manipular y renderizar un componente concreto que implemente
-/// [`Component`], y habilita acceso concurrente mediante [`Arc>`].
+/// Facilita el acceso a componentes del mismo tipo sin necesidad de hacer `downcast`.
pub struct Typed(Arc>);
impl Clone for Typed {
@@ -57,7 +56,7 @@ impl Clone for Typed {
}
impl Typed {
- /// Crea un nuevo `Typed` a partir de un componente.
+ /// Crea un nuevo [`Typed`] a partir de un componente.
pub fn with(component: C) -> Self {
Typed(Arc::new(RwLock::new(component)))
}
@@ -285,7 +284,7 @@ impl IntoIterator for Children {
///
/// # Ejemplo de uso:
///
- /// ```rust,ignore
+ /// ```rust#ignore
/// let children = Children::new().with(child1).with(child2);
/// for child in children {
/// println!("{:?}", child.id());
@@ -304,7 +303,7 @@ impl<'a> IntoIterator for &'a Children {
///
/// # Ejemplo de uso:
///
- /// ```rust,ignore
+ /// ```rust#ignore
/// let children = Children::new().with(child1).with(child2);
/// for child in &children {
/// println!("{:?}", child.id());
@@ -323,7 +322,7 @@ impl<'a> IntoIterator for &'a mut Children {
///
/// # Ejemplo de uso:
///
- /// ```rust,ignore
+ /// ```rust#ignore
/// let mut children = Children::new().with(child1).with(child2);
/// for child in &mut children {
/// child.render(&mut context);
diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs
index 333cf69..2818570 100644
--- a/src/core/component/definition.rs
+++ b/src/core/component/definition.rs
@@ -1,6 +1,6 @@
use crate::base::action;
use crate::core::{AnyInfo, TypeInfo};
-use crate::html::{html, Context, Markup, PrepareMarkup};
+use crate::html::{html, Context, Markup, PrepareMarkup, Render};
/// Define la función de renderizado para todos los componentes.
///
@@ -11,7 +11,7 @@ pub trait ComponentRender {
fn render(&mut self, cx: &mut Context) -> Markup;
}
-/// Interfaz común que debe implementar un componente renderizable en PageTop.
+/// Interfaz común que debe implementar un componente renderizable en `PageTop`.
///
/// Se recomienda que los componentes deriven [`AutoDefault`](crate::AutoDefault). También deben
/// implementar explícitamente el método [`new()`](Self::new) y pueden sobrescribir los otros
@@ -29,14 +29,14 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
TypeInfo::ShortName.of::()
}
- /// Devuelve una descripción del componente, si existe.
+ /// Devuelve una descripción opcional del componente.
///
/// Por defecto, no se proporciona ninguna descripción (`None`).
fn description(&self) -> Option {
None
}
- /// Devuelve el identificador del componente, si existe.
+ /// Devuelve un identificador opcional para el componente.
///
/// Este identificador puede usarse para referenciar el componente en el HTML. Por defecto, no
/// tiene ningún identificador (`None`).
@@ -51,17 +51,12 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
#[allow(unused_variables)]
fn setup_before_prepare(&mut self, cx: &mut Context) {}
- /// Devuelve una representación renderizada del componente.
+ /// Devuelve una representación estructurada del componente lista para renderizar.
///
/// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente
/// durante el proceso de construcción del documento. Puede sobrescribirse para generar
/// dinámicamente el contenido HTML con acceso al contexto de renderizado.
///
- /// Este método debe ser capaz de preparar el renderizado del componente con los métodos del
- /// propio componente y el contexto proporcionado, no debería hacerlo accediendo directamente a
- /// los campos de la estructura del componente. Es una forma de garantizar que los programadores
- /// podrán sobrescribir este método sin preocuparse por los detalles internos del componente.
- ///
/// Por defecto, devuelve [`PrepareMarkup::None`].
#[allow(unused_variables)]
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
diff --git a/src/core/component/slot.rs b/src/core/component/slot.rs
deleted file mode 100644
index 19ed72a..0000000
--- a/src/core/component/slot.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-use crate::builder_fn;
-use crate::core::component::{Component, Typed};
-use crate::html::{html, Context, Markup};
-
-/// Contenedor para un componente [`Typed`] opcional.
-///
-/// Un `TypedSlot` actúa como un contenedor dentro de otro componente para incluir o no un
-/// subcomponente. Internamente encapsula `Option>`, pero proporciona una API más sencilla
-/// para construir estructuras jerárquicas.
-///
-/// # Ejemplo
-///
-/// ```rust,ignore
-/// use pagetop::prelude::*;
-///
-/// let comp = MyComponent::new();
-/// let opt = TypedSlot::new(comp);
-/// assert!(opt.get().is_some());
-/// ```
-pub struct TypedSlot(Option>);
-
-impl Default for TypedSlot {
- fn default() -> Self {
- TypedSlot(None)
- }
-}
-
-impl TypedSlot {
- /// Crea un nuevo [`TypedSlot`].
- ///
- /// El componente se envuelve automáticamente en un [`Typed`] y se almacena.
- pub fn new(component: C) -> Self {
- TypedSlot(Some(Typed::with(component)))
- }
-
- // TypedSlot BUILDER *********************************************************************
-
- /// Establece un componente nuevo, o lo vacía.
- ///
- /// Si se proporciona `Some(component)`, se guarda en [`Typed`]; y si es `None`, se limpia.
- #[builder_fn]
- pub fn with_value(mut self, component: Option) -> Self {
- self.0 = component.map(Typed::with);
- self
- }
-
- // TypedSlot GETTERS *********************************************************************
-
- /// Devuelve un clon (incrementa el contador `Arc`) de [`Typed`], si existe.
- pub fn get(&self) -> Option> {
- self.0.clone()
- }
-
- // TypedSlot RENDER ************************************************************************
-
- /// Renderiza el componente, si existe.
- pub fn render(&self, cx: &mut Context) -> Markup {
- if let Some(component) = &self.0 {
- component.render(cx)
- } else {
- html! {}
- }
- }
-}
diff --git a/src/core/extension.rs b/src/core/extension.rs
index 6ae6d33..cabae5c 100644
--- a/src/core/extension.rs
+++ b/src/core/extension.rs
@@ -1,6 +1,6 @@
//! API para añadir nuevas funcionalidades usando extensiones.
//!
-//! Cada funcionalidad adicional que quiera incorporarse a una aplicación PageTop se debe modelar
+//! Cada funcionalidad adicional que quiera incorporarse a una aplicación `PageTop` se debe modelar
//! como una **extensión**. Todas comparten la misma interfaz declarada en [`Extension`].
mod definition;
diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs
index 90bdbad..ac29259 100644
--- a/src/core/extension/definition.rs
+++ b/src/core/extension/definition.rs
@@ -10,7 +10,7 @@ use crate::{actions_boxed, service};
/// cualquier hilo de la ejecución sin necesidad de sincronización adicional.
pub type ExtensionRef = &'static dyn Extension;
-/// Interfaz común que debe implementar cualquier extensión de PageTop.
+/// Interfaz común que debe implementar cualquier extensión de `PageTop`.
///
/// Este *trait* es fácil de implementar, basta con declarar una estructura de tamaño cero para la
/// extensión y sobreescribir los métodos que sea necesario.
@@ -26,7 +26,7 @@ pub type ExtensionRef = &'static dyn Extension;
/// }
/// ```
pub trait Extension: AnyInfo + Send + Sync {
- /// Nombre localizado de la extensión legible para el usuario.
+ /// Nombre legible para el usuario.
///
/// Predeterminado por el [`short_name()`](AnyInfo::short_name) del tipo asociado a la
/// extensión.
@@ -34,15 +34,18 @@ pub trait Extension: AnyInfo + Send + Sync {
L10n::n(self.short_name())
}
- /// Descripción corta localizada de la extensión para paneles, listados, etc.
+ /// Descripción corta para paneles, listados, etc.
fn description(&self) -> L10n {
L10n::default()
}
- /// Devuelve una referencia a esta misma extensión cuando se trata de un tema.
+ /// Los temas son extensiones que implementan [`Extension`] y también
+ /// [`Theme`](crate::core::theme::Theme).
///
- /// Para ello, debe implementar [`Extension`] y también [`Theme`](crate::core::theme::Theme). Si
- /// la extensión no es un tema, este método devuelve `None` por defecto.
+ /// Si la extensión no es un tema, este método devuelve `None` por defecto.
+ ///
+ /// En caso contrario, este método debe implementarse para devolver una referencia de sí mismo
+ /// como tema. Por ejemplo:
///
/// ```rust
/// use pagetop::prelude::*;
@@ -63,7 +66,7 @@ pub trait Extension: AnyInfo + Send + Sync {
/// Otras extensiones que deben habilitarse **antes** de esta.
///
- /// PageTop las resolverá automáticamente respetando el orden durante el arranque de la
+ /// `PageTop` las resolverá automáticamente respetando el orden durante el arranque de la
/// aplicación.
fn dependencies(&self) -> Vec {
vec![]
@@ -78,7 +81,7 @@ pub trait Extension: AnyInfo + Send + Sync {
actions_boxed![]
}
- /// Inicializa la extensión durante la fase de arranque de la aplicación.
+ /// Inicializa la extensión durante la lógica de arranque de la aplicación.
///
/// Se llama una sola vez, después de que todas las dependencias se han inicializado y antes de
/// aceptar cualquier petición HTTP.
@@ -101,8 +104,8 @@ pub trait Extension: AnyInfo + Send + Sync {
#[allow(unused_variables)]
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
- /// Permite crear extensiones para deshabilitar y desinstalar recursos de otras de versiones
- /// anteriores de la aplicación.
+ /// Permite crear extensiones para deshabilitar y desinstalar los recursos de otras extensiones
+ /// utilizadas en versiones anteriores de la aplicación.
///
/// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones.
fn drop_extensions(&self) -> Vec {
diff --git a/src/core/theme.rs b/src/core/theme.rs
index 61d820b..0a0f819 100644
--- a/src/core/theme.rs
+++ b/src/core/theme.rs
@@ -1,24 +1,26 @@
//! API para añadir y gestionar nuevos temas.
//!
-//! En PageTop un tema es la *piel* de la aplicación, decide cómo se muestra cada documento HTML,
+//! En `PageTop` un tema es la *piel* de la aplicación, decide cómo se muestra cada documento HTML,
//! especialmente las páginas de contenido ([`Page`](crate::response::page::Page)), sin alterar la
//! lógica interna de sus componentes.
//!
//! Un tema **declara las regiones** (*cabecera*, *barra lateral*, *pie*, etc.) que estarán
//! disponibles para colocar contenido. Los temas son responsables últimos de los estilos,
//! tipografías, espaciados y cualquier otro detalle visual o de comportamiento (como animaciones,
-//! scripts de interfaz, etc.).
+//! *scripts* de interfaz, etc.).
//!
-//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension); por
-//! lo que se instancian, declaran sus dependencias y se inician igual que el resto de extensiones;
-//! pero serán temas si además implementan [`theme()`](crate::core::extension::Extension::theme) y
-//! [`Theme`].
+//! Es una extensión más (implementando [`Extension`](crate::core::extension::Extension)). Se
+//! instala, activa y declara dependencias igual que el resto de extensiones; y se señala a sí misma
+//! como tema (implementando [`theme()`](crate::core::extension::Extension::theme) y [`Theme`]).
mod definition;
-pub use definition::{Theme, ThemePage, ThemeRef};
+pub use definition::{Theme, ThemeRef};
mod regions;
-pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT};
-pub use regions::{InRegion, Region};
+pub(crate) use regions::ChildrenInRegions;
+pub use regions::InRegion;
pub(crate) mod all;
+
+/// Nombre de la región por defecto: `content`.
+pub const CONTENT_REGION_NAME: &str = "content";
diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs
index 38a0bfc..8de88bd 100644
--- a/src/core/theme/definition.rs
+++ b/src/core/theme/definition.rs
@@ -1,49 +1,52 @@
use crate::core::extension::Extension;
-use crate::core::theme::Region;
+use crate::core::theme::CONTENT_REGION_NAME;
use crate::global;
use crate::html::{html, Markup};
use crate::locale::L10n;
use crate::response::page::Page;
-use std::sync::LazyLock;
-
-/// Referencia estática a un tema.
+/// Representa una referencia a un tema.
///
-/// Los temas son también extensiones. Por tanto, deben declararse como **instancias estáticas** que
-/// implementen [`Theme`] y, a su vez, [`Extension`].
+/// Los temas son también extensiones. Por tanto se deben definir igual, es decir, como instancias
+/// estáticas globales que implementan [`Theme`], pero también [`Extension`].
pub type ThemeRef = &'static dyn Theme;
-/// Métodos predefinidos de renderizado para las páginas de un tema.
+/// Interfaz común que debe implementar cualquier tema de `PageTop`.
///
-/// Contiene las implementaciones base de las **secciones** `` y ``. Se implementa
-/// automáticamente para cualquier tipo que implemente [`Theme`], por lo que normalmente no requiere
-/// implementación explícita.
+/// Un tema implementará [`Theme`] y los métodos que sean necesarios de [`Extension`], aunque el
+/// único obligatorio es [`theme()`](Extension::theme).
///
-/// Si un tema **sobrescribe** [`render_page_head()`](Theme::render_page_head) o
-/// [`render_page_body()`](Theme::render_page_body), se puede volver al comportamiento por defecto
-/// cuando se necesite usando FQS (*Fully Qualified Syntax*):
+/// ```rust
+/// use pagetop::prelude::*;
///
-/// - `::render_body(self, page, self.page_regions())`
-/// - `::render_head(self, page)`
-pub trait ThemePage {
- /// Renderiza el contenido del `` de la página.
- ///
- /// Recorre `regions` en el **orden declarado** y, para cada región con contenido, genera un
- /// contenedor con `role="region"` y un `aria-label` localizado. Se asume que cada identificador
- /// de región es **único** dentro de la página.
- fn render_body(&self, page: &mut Page, regions: &[(Region, L10n)]) -> Markup {
+/// pub struct MyTheme;
+///
+/// impl Extension for MyTheme {
+/// fn name(&self) -> L10n { L10n::n("My theme") }
+/// fn description(&self) -> L10n { L10n::n("Un tema personal") }
+///
+/// fn theme(&self) -> Option {
+/// Some(&Self)
+/// }
+/// }
+///
+/// impl Theme for MyTheme {}
+/// ```
+pub trait Theme: Extension + Send + Sync {
+ fn regions(&self) -> Vec<(&'static str, L10n)> {
+ vec![(CONTENT_REGION_NAME, L10n::l("content"))]
+ }
+
+ #[allow(unused_variables)]
+ fn before_render_page_body(&self, page: &mut Page) {}
+
+ fn render_page_body(&self, page: &mut Page) -> Markup {
html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
- @for (region, region_label) in regions {
- @let output = page.render_region(region.key());
+ @for (region_name, _) in self.regions() {
+ @let output = page.render_region(region_name);
@if !output.is_empty() {
- @let region_name = region.name();
- div
- id=(region_name)
- class={ "region region--" (region_name) }
- role="region"
- aria-label=[region_label.lookup(page)]
- {
+ div id=(region_name) class={ "region-container region-" (region_name) } {
(output)
}
}
@@ -52,12 +55,10 @@ pub trait ThemePage {
}
}
- /// Renderiza el contenido del `` de la página.
- ///
- /// Por defecto incluye las etiquetas básicas (`charset`, `title`, `description`, `viewport`,
- /// `X-UA-Compatible`), los metadatos (`name/content`) y propiedades (`property/content`),
- /// además de los recursos CSS/JS de la página.
- fn render_head(&self, page: &mut Page) -> Markup {
+ #[allow(unused_variables)]
+ fn after_render_page_body(&self, page: &mut Page) {}
+
+ fn render_page_head(&self, page: &mut Page) -> Markup {
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
html! {
head {
@@ -87,115 +88,12 @@ pub trait ThemePage {
}
}
}
-}
-/// Interfaz común que debe implementar cualquier tema de PageTop.
-///
-/// Un tema implementa [`Theme`] y los métodos necesarios de [`Extension`]. El único método
-/// **obligatorio** de `Extension` para un tema es [`theme()`](Extension::theme).
-///
-/// ```rust
-/// use pagetop::prelude::*;
-///
-/// pub struct MyTheme;
-///
-/// impl Extension for MyTheme {
-/// fn name(&self) -> L10n {
-/// L10n::n("My theme")
-/// }
-///
-/// fn description(&self) -> L10n {
-/// L10n::n("A personal theme")
-/// }
-///
-/// fn theme(&self) -> Option {
-/// Some(&Self)
-/// }
-/// }
-///
-/// impl Theme for MyTheme {}
-/// ```
-pub trait Theme: Extension + ThemePage + Send + Sync {
- /// **Obsoleto desde la versión 0.4.0**: usar [`page_regions()`](Self::page_regions) en su
- /// lugar.
- #[deprecated(since = "0.4.0", note = "Use `page_regions()` instead")]
- fn regions(&self) -> Vec<(&'static str, L10n)> {
- vec![("content", L10n::l("content"))]
+ fn error403(&self, _page: &mut Page) -> Markup {
+ html! { div { h1 { ("FORBIDDEN ACCESS") } } }
}
- /// Declaración ordenada de las regiones disponibles en la página.
- ///
- /// Devuelve una **lista estática** de pares `(Region, L10n)` que se usará para renderizar todas
- /// las regiones que componen una página en el orden indicado .
- ///
- /// Si un tema necesita un conjunto distinto de regiones, se puede **sobrescribir** este método
- /// con los siguientes requisitos y recomendaciones:
- ///
- /// - Los identificadores deben ser **estables** (p. ej. `"sidebar-left"`, `"content"`).
- /// - La región `"content"` es **obligatoria**. Se puede usar [`Region::default()`] para
- /// declararla.
- /// - La etiqueta `L10n` se evalúa con el idioma activo de la página.
- ///
- /// Por defecto devuelve:
- ///
- /// - `"header"`: cabecera.
- /// - `"content"`: contenido principal (**obligatoria**).
- /// - `"footer"`: pie.
- fn page_regions(&self) -> &'static [(Region, L10n)] {
- static REGIONS: LazyLock<[(Region, L10n); 3]> = LazyLock::new(|| {
- [
- (Region::declare("header"), L10n::l("region_header")),
- (Region::default(), L10n::l("region_content")),
- (Region::declare("footer"), L10n::l("region_footer")),
- ]
- });
- ®IONS[..]
- }
-
- /// Acciones específicas del tema antes de renderizar el `` de la página.
- ///
- /// Útil para preparar clases, inyectar recursos o ajustar metadatos.
- #[allow(unused_variables)]
- fn before_render_page_body(&self, page: &mut Page) {}
-
- /// Renderiza el contenido del `` de la página.
- ///
- /// Si se sobrescribe este método, se puede volver al comportamiento base con:
- /// `::render_body(self, page, self.page_regions())`.
- #[inline]
- fn render_page_body(&self, page: &mut Page) -> Markup {
- ::render_body(self, page, self.page_regions())
- }
-
- /// Acciones específicas del tema después de renderizar el `` de la página.
- ///
- /// Útil para *tracing*, métricas o ajustes finales del estado de la página.
- #[allow(unused_variables)]
- fn after_render_page_body(&self, page: &mut Page) {}
-
- /// Renderiza el contenido del `` de la página.
- ///
- /// Si se sobrescribe este método, se puede volver al comportamiento base con:
- /// `::render_head(self, page)`.
- #[inline]
- fn render_page_head(&self, page: &mut Page) -> Markup {
- ::render_head(self, page)
- }
-
- /// Contenido predeterminado para la página de error "*403 – Forbidden*".
- ///
- /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema.
- fn error403(&self, page: &mut Page) -> Markup {
- html! { div { h1 { (L10n::l("error403_notice").using(page)) } } }
- }
-
- /// Contenido predeterminado para la página de error "*404 – Not Found*".
- ///
- /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema.
- fn error404(&self, page: &mut Page) -> Markup {
- html! { div { h1 { (L10n::l("error404_notice").using(page)) } } }
+ fn error404(&self, _page: &mut Page) -> Markup {
+ html! { div { h1 { ("RESOURCE NOT FOUND") } } }
}
}
-
-/// Se implementa automáticamente `ThemePage` para cualquier tema.
-impl ThemePage for T {}
diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs
index 8082aac..22ab6f2 100644
--- a/src/core/theme/regions.rs
+++ b/src/core/theme/regions.rs
@@ -1,5 +1,5 @@
use crate::core::component::{Child, ChildOp, Children};
-use crate::core::theme::ThemeRef;
+use crate::core::theme::{ThemeRef, CONTENT_REGION_NAME};
use crate::{builder_fn, AutoDefault, UniqueId};
use parking_lot::RwLock;
@@ -7,81 +7,25 @@ use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::LazyLock;
-// Conjunto de regiones globales asociadas a un tema específico.
+// Regiones globales con componentes para un tema dado.
static THEME_REGIONS: LazyLock>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
-// Conjunto de regiones globales comunes a todos los temas.
+// Regiones globales con componentes para cualquier tema.
static COMMON_REGIONS: LazyLock> =
LazyLock::new(|| RwLock::new(ChildrenInRegions::default()));
-/// Nombre de la región de contenido por defecto (`"content"`).
-pub const REGION_CONTENT: &str = "content";
-
-/// Identificador de una región de página.
-///
-/// Incluye una **clave estática** ([`key()`](Self::key)) que identifica la región en el tema, y un
-/// **nombre normalizado** ([`name()`](Self::name)) en minúsculas para su uso en atributos HTML
-/// (p.ej., clases `region__{name}`).
-///
-/// Se utiliza para declarar las regiones que componen una página en un tema (ver
-/// [`page_regions()`](crate::core::theme::Theme::page_regions)).
-pub struct Region {
- key: &'static str,
- name: String,
-}
-
-impl Default for Region {
- #[inline]
- fn default() -> Self {
- Self {
- key: REGION_CONTENT,
- name: REGION_CONTENT.to_string(),
- }
- }
-}
-
-impl Region {
- /// Declara una región a partir de su clave estática.
- ///
- /// Genera además un nombre normalizado de la clave, eliminando espacios iniciales y finales,
- /// convirtiendo a minúsculas y sustituyendo los espacios intermedios por guiones (`-`).
- ///
- /// Esta clave se usará para añadir componentes a la región; por ello se recomiendan nombres
- /// sencillos, limitando los caracteres a `[a-z0-9-]` (p.ej., `"sidebar"` o `"main-menu"`), cuyo
- /// nombre normalizado coincidirá con la clave.
- #[inline]
- pub fn declare(key: &'static str) -> Self {
- Self {
- key,
- name: key.trim().to_ascii_lowercase().replace(' ', "-"),
- }
- }
-
- /// Devuelve la clave estática asignada a la región.
- #[inline]
- pub fn key(&self) -> &'static str {
- self.key
- }
-
- /// Devuelve el nombre normalizado de la región (para atributos y búsquedas).
- #[inline]
- pub fn name(&self) -> &str {
- &self.name
- }
-}
-
-// Contenedor interno de componentes agrupados por región.
+// Estructura interna para mantener los componentes de una región.
#[derive(AutoDefault)]
pub struct ChildrenInRegions(HashMap<&'static str, Children>);
impl ChildrenInRegions {
pub fn with(region_name: &'static str, child: Child) -> Self {
- ChildrenInRegions::default().with_child_in(region_name, ChildOp::Add(child))
+ ChildrenInRegions::default().with_child_in_region(region_name, ChildOp::Add(child))
}
#[builder_fn]
- pub fn with_child_in(mut self, region_name: &'static str, op: ChildOp) -> Self {
+ pub fn with_child_in_region(mut self, region_name: &'static str, op: ChildOp) -> Self {
if let Some(region) = self.0.get_mut(region_name) {
region.alter_child(op);
} else {
@@ -104,24 +48,25 @@ impl ChildrenInRegions {
}
}
-/// Punto de acceso para añadir componentes a regiones globales o específicas de un tema.
+/// Permite añadir componentes a regiones globales o regiones de temas concretos.
///
-/// Según la variante, se pueden añadir componentes ([`add()`](Self::add)) que permanecerán
-/// disponibles durante toda la ejecución.
+/// Dada una región, según la variante seleccionada, se le podrán añadir ([`add()`](Self::add))
+/// componentes que se mantendrán durante la ejecución de la aplicación.
///
-/// Estos componentes se renderizarán automáticamente al procesar los documentos HTML que incluyen
-/// estas regiones, como las páginas de contenido ([`Page`](crate::response::page::Page)).
+/// Estas estructuras de componentes se renderizarán automáticamente al procesar los documentos HTML
+/// que las usan, como las páginas de contenido ([`Page`](crate::response::page::Page)), por
+/// ejemplo.
pub enum InRegion {
- /// Región de contenido por defecto.
+ /// Representa la región por defecto en la que se pueden añadir componentes.
Content,
- /// Región identificada por el nombre proporcionado.
+ /// Representa la región con el nombre del argumento.
Named(&'static str),
- /// Región identificada por un nombre y asociada a un tema concreto.
+ /// Representa la región con el nombre y del tema especificado en los argumentos.
OfTheme(&'static str, ThemeRef),
}
impl InRegion {
- /// Añade un componente a la región indicada por la variante.
+ /// Permite añadir un componente en la región de la variante seleccionada.
///
/// # Ejemplo
///
@@ -143,17 +88,17 @@ impl InRegion {
InRegion::Content => {
COMMON_REGIONS
.write()
- .alter_child_in(REGION_CONTENT, ChildOp::Add(child));
+ .alter_child_in_region(CONTENT_REGION_NAME, ChildOp::Add(child));
}
- InRegion::Named(region_name) => {
+ InRegion::Named(name) => {
COMMON_REGIONS
.write()
- .alter_child_in(region_name, ChildOp::Add(child));
+ .alter_child_in_region(name, ChildOp::Add(child));
}
InRegion::OfTheme(region_name, theme_ref) => {
let mut regions = THEME_REGIONS.write();
if let Some(r) = regions.get_mut(&theme_ref.type_id()) {
- r.alter_child_in(region_name, ChildOp::Add(child));
+ r.alter_child_in_region(region_name, ChildOp::Add(child));
} else {
regions.insert(
theme_ref.type_id(),
diff --git a/src/global.rs b/src/global.rs
index ccc6d9d..8a03589 100644
--- a/src/global.rs
+++ b/src/global.rs
@@ -50,11 +50,12 @@ pub struct App {
pub theme: String,
/// Idioma por defecto para la aplicación.
///
- /// Si no está definido o no es válido, el idioma efectivo para el renderizado se resolverá
- /// según la implementación de [`LangId`](crate::locale::LangId) en este orden: primero intenta
- /// con el establecido en [`Contextual::with_langid()`](crate::html::Contextual::with_langid);
- /// pero si no se ha definido explícitamente, usará el indicado en la cabecera `Accept-Language`
- /// del navegador; y, si ninguno aplica, se empleará el idioma de respaldo ("en-US").
+ /// Si no se especifica un valor válido, normalmente se usará el idioma devuelto por la
+ /// implementación de [`LangId`](crate::locale::LangId) para [`Context`](crate::html::Context),
+ /// en el siguiente orden: primero, el idioma establecido explícitamente con
+ /// [`Context::with_langid()`](crate::html::Context::with_langid); si no se ha definido, se
+ /// usará el indicado en la cabecera `Accept-Language` del navegador; y, si ninguno aplica, se
+ /// empleará el idioma de respaldo ("en-US").
pub language: String,
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
/// *"Starwars"*.
@@ -67,7 +68,7 @@ pub struct App {
#[derive(Debug, Deserialize)]
/// Sección `[Dev]` de la configuración. Forma parte de [`Settings`].
pub struct Dev {
- /// Directorio desde el que servir los archivos estáticos de PageTop.
+ /// Directorio desde el que servir los archivos estáticos de `PageTop`.
///
/// Por defecto, los archivos se integran en el binario de la aplicación. Si aquí se indica una
/// ruta válida, ya sea absoluta o relativa al directorio del proyecto o del binario en
diff --git a/src/html.rs b/src/html.rs
index 4858bbf..82fa906 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -1,84 +1,54 @@
//! HTML en código.
mod maud;
-pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE};
-
-// HTML DOCUMENT ASSETS ****************************************************************************
+pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE};
mod assets;
pub use assets::favicon::Favicon;
pub use assets::javascript::JavaScript;
pub use assets::stylesheet::{StyleSheet, TargetMedia};
-pub use assets::{Asset, Assets};
-
-// HTML DOCUMENT CONTEXT ***************************************************************************
+pub(crate) use assets::Assets;
mod context;
-pub use context::{AssetsOp, Context, Contextual, ErrorParam};
+pub use context::{AssetsOp, Context, ErrorParam};
-// HTML ATTRIBUTES *********************************************************************************
+mod opt_id;
+pub use opt_id::OptionId;
-mod attr_id;
-pub use attr_id::AttrId;
-/// **Obsoleto desde la versión 0.4.0**: usar [`AttrId`] en su lugar.
-#[deprecated(since = "0.4.0", note = "Use `AttrId` instead")]
-pub type OptionId = AttrId;
+mod opt_name;
+pub use opt_name::OptionName;
-mod attr_name;
-pub use attr_name::AttrName;
-/// **Obsoleto desde la versión 0.4.0**: usar [`AttrName`] en su lugar.
-#[deprecated(since = "0.4.0", note = "Use `AttrName` instead")]
-pub type OptionName = AttrName;
+mod opt_string;
+pub use opt_string::OptionString;
-mod attr_value;
-pub use attr_value::AttrValue;
-/// **Obsoleto desde la versión 0.4.0**: usar [`AttrValue`] en su lugar.
-#[deprecated(since = "0.4.0", note = "Use `AttrValue` instead")]
-pub type OptionString = AttrValue;
+mod opt_translated;
+pub use opt_translated::OptionTranslated;
-mod attr_l10n;
-pub use attr_l10n::AttrL10n;
-/// **Obsoleto desde la versión 0.4.0**: usar [`AttrL10n`] en su lugar.
-#[deprecated(since = "0.4.0", note = "Use `AttrL10n` instead")]
-pub type OptionTranslated = AttrL10n;
+mod opt_classes;
+pub use opt_classes::{ClassesOp, OptionClasses};
-mod attr_classes;
-pub use attr_classes::{AttrClasses, ClassesOp};
-/// **Obsoleto desde la versión 0.4.0**: usar [`AttrClasses`] en su lugar.
-#[deprecated(since = "0.4.0", note = "Use `AttrClasses` instead")]
-pub type OptionClasses = AttrClasses;
+mod opt_component;
+pub use opt_component::OptionComponent;
-use crate::{core, AutoDefault};
-
-/// **Obsoleto desde la versión 0.4.0**: usar [`TypedSlot`](crate::core::component::TypedSlot) en su
-/// lugar.
-#[deprecated(
- since = "0.4.0",
- note = "Use `pagetop::core::component::TypedSlot` instead"
-)]
-#[allow(type_alias_bounds)]
-pub type OptionComponent = core::component::TypedSlot;
+use crate::AutoDefault;
/// Prepara contenido HTML para su conversión a [`Markup`].
///
-/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML sin escapar o
-/// fragmentos ya procesados) para renderizarlos de forma homogénea en plantillas, sin interferir
-/// con el uso estándar de [`Markup`].
+/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML escapado o marcado
+/// ya procesado) para renderizar de forma homogénea en plantillas sin interferir con el uso
+/// estándar de [`Markup`].
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
-/// // Texto normal, se escapa automáticamente para evitar inyección de HTML.
-/// let fragment = PrepareMarkup::Escaped("Hola mundo".to_string());
+/// let fragment = PrepareMarkup::Text(String::from("Hola mundo"));
/// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>");
///
-/// // HTML literal, se inserta directamente, sin escapado adicional.
-/// let raw_html = PrepareMarkup::Raw("negrita".to_string());
+/// let raw_html = PrepareMarkup::Escaped(String::from("negrita"));
/// assert_eq!(raw_html.render().into_string(), "negrita");
///
-/// // Fragmento ya preparado con la macro `html!`.
/// let prepared = PrepareMarkup::With(html! {
/// h2 { "Título de ejemplo" }
/// p { "Este es un párrafo con contenido dinámico." }
@@ -90,22 +60,14 @@ pub type OptionComponent = core::component::Typed
/// ```
#[derive(AutoDefault)]
pub enum PrepareMarkup {
- /// No se genera contenido HTML (equivale a `html! {}`).
+ /// No se genera contenido HTML (devuelve `html! {}`).
#[default]
None,
- /// Texto plano que se **escapará automáticamente** para que no sea interpretado como HTML.
- ///
- /// Úsalo con textos que provengan de usuarios u otras fuentes externas para garantizar la
- /// seguridad contra inyección de código.
+ /// Texto estático que se escapará automáticamente para no ser interpretado como HTML.
+ Text(String),
+ /// Contenido sin escapado adicional, útil para HTML generado externamente.
Escaped(String),
- /// HTML literal que se inserta **sin escapado adicional**.
- ///
- /// Úsalo únicamente para contenido generado de forma confiable o controlada, ya que cualquier
- /// etiqueta o script incluido será renderizado directamente en el documento.
- Raw(String),
/// Fragmento HTML ya preparado como [`Markup`], listo para insertarse directamente.
- ///
- /// Normalmente proviene de expresiones `html! { ... }`.
With(Markup),
}
@@ -114,18 +76,20 @@ impl PrepareMarkup {
pub fn is_empty(&self) -> bool {
match self {
PrepareMarkup::None => true,
- PrepareMarkup::Escaped(text) => text.is_empty(),
- PrepareMarkup::Raw(string) => string.is_empty(),
+ PrepareMarkup::Text(text) => text.is_empty(),
+ PrepareMarkup::Escaped(string) => string.is_empty(),
PrepareMarkup::With(markup) => markup.is_empty(),
}
}
+}
+impl Render for PrepareMarkup {
/// Integra el renderizado fácilmente en la macro [`html!`].
- pub fn render(&self) -> Markup {
+ fn render(&self) -> Markup {
match self {
PrepareMarkup::None => html! {},
- PrepareMarkup::Escaped(text) => html! { (text) },
- PrepareMarkup::Raw(string) => html! { (PreEscaped(string)) },
+ PrepareMarkup::Text(text) => html! { (text) },
+ PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) },
PrepareMarkup::With(markup) => html! { (markup) },
}
}
diff --git a/src/html/assets.rs b/src/html/assets.rs
index 41cd471..894b7e8 100644
--- a/src/html/assets.rs
+++ b/src/html/assets.rs
@@ -2,55 +2,25 @@ pub mod favicon;
pub mod javascript;
pub mod stylesheet;
-use crate::html::{html, Context, Markup};
+use crate::html::{html, Markup, Render};
use crate::{AutoDefault, Weight};
-/// Representación genérica de un script [`JavaScript`](crate::html::JavaScript) o una hoja de
-/// estilos [`StyleSheet`](crate::html::StyleSheet).
-///
-/// Estos recursos se incluyen en los conjuntos de recursos ([`Assets`]) que suelen renderizarse en
-/// un documento HTML.
-///
-/// Cada recurso se identifica por un **nombre único** ([`Asset::name()`]), usado como clave; y un
-/// **peso** ([`Asset::weight()`]), que determina su orden relativo de renderizado.
-pub trait Asset {
- /// Devuelve el nombre del recurso, utilizado como clave única.
+pub trait AssetsTrait: Render {
+ // Devuelve el nombre del recurso, utilizado como clave única.
fn name(&self) -> &str;
- /// Devuelve el peso del recurso, usado para ordenar el renderizado de menor a mayor peso.
+ // Devuelve el peso del recurso, durante el renderizado se procesan de menor a mayor peso.
fn weight(&self) -> Weight;
-
- /// Renderiza el recurso en el contexto proporcionado.
- fn render(&self, cx: &mut Context) -> Markup;
}
-/// Gestión común para conjuntos de recursos como [`JavaScript`](crate::html::JavaScript) y
-/// [`StyleSheet`](crate::html::StyleSheet).
-///
-/// Se emplea normalmente para agrupar, administrar y renderizar los recursos de un documento HTML.
-/// Cada recurso se identifica por un nombre único ([`Asset::name()`]) y tiene asociado un peso
-/// ([`Asset::weight()`]) que determina su orden de renderizado.
-///
-/// Durante el renderizado, los recursos se procesan en orden ascendente de peso. En caso de
-/// igualdad, se respeta el orden de inserción.
#[derive(AutoDefault)]
-pub struct Assets(Vec);
+pub(crate) struct Assets(Vec);
-impl Assets {
- /// Crea un nuevo conjunto vacío de recursos.
- ///
- /// Normalmente no se instancia directamente, sino como parte de la gestión de recursos que
- /// hacen páginas o temas.
+impl Assets {
pub fn new() -> Self {
- Self(Vec::new())
+ Assets::(Vec::::new())
}
- /// Inserta un recurso.
- ///
- /// Si no existe otro con el mismo nombre, lo añade. Si ya existe y su peso era mayor, lo
- /// reemplaza. Y si su peso era menor o igual, entonces no realiza ningún cambio.
- ///
- /// Devuelve `true` si el recurso fue insertado o reemplazado.
pub fn add(&mut self, asset: T) -> bool {
match self.0.iter().position(|x| x.name() == asset.name()) {
Some(index) => {
@@ -69,9 +39,6 @@ impl Assets {
}
}
- /// Elimina un recurso por nombre.
- ///
- /// Devuelve `true` si el recurso existía y fue eliminado.
pub fn remove(&mut self, name: impl AsRef) -> bool {
if let Some(index) = self.0.iter().position(|x| x.name() == name.as_ref()) {
self.0.remove(index);
@@ -80,13 +47,16 @@ impl Assets {
false
}
}
+}
- pub fn render(&self, cx: &mut Context) -> Markup {
+impl Render for Assets {
+ fn render(&self) -> Markup {
let mut assets = self.0.iter().collect::>();
assets.sort_by_key(|a| a.weight());
+
html! {
@for a in assets {
- (a.render(cx))
+ (a.render())
}
}
}
diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs
index d731b8f..1a8b29e 100644
--- a/src/html/assets/favicon.rs
+++ b/src/html/assets/favicon.rs
@@ -1,4 +1,4 @@
-use crate::html::{html, Context, Markup};
+use crate::html::{html, Markup, Render};
use crate::AutoDefault;
/// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio.
@@ -129,7 +129,7 @@ impl Favicon {
icon_color: Option,
) -> Self {
let icon_type = match icon_source.rfind('.') {
- Some(i) => match icon_source[i..].to_string().to_lowercase().as_str() {
+ Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() {
".avif" => Some("image/avif"),
".gif" => Some("image/gif"),
".ico" => Some("image/x-icon"),
@@ -151,12 +151,10 @@ impl Favicon {
});
self
}
+}
- /// Renderiza el **Favicon** completo con todas las etiquetas declaradas.
- ///
- /// El parámetro `Context` se acepta por coherencia con el resto de *assets*, aunque en este
- /// caso es ignorado.
- pub fn render(&self, _cx: &mut Context) -> Markup {
+impl Render for Favicon {
+ fn render(&self) -> Markup {
html! {
@for item in &self.0 {
(item)
diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs
index a8ed3e8..db5754e 100644
--- a/src/html/assets/javascript.rs
+++ b/src/html/assets/javascript.rs
@@ -1,45 +1,35 @@
-use crate::html::assets::Asset;
-use crate::html::{html, Context, Markup, PreEscaped};
+use crate::html::assets::AssetsTrait;
+use crate::html::{html, Markup, Render};
use crate::{join, join_pair, AutoDefault, Weight};
// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador.
//
// Los distintos modos de carga permiten optimizar el rendimiento y controlar el comportamiento del
-// script en relación con el análisis del documento HTML y la ejecución del resto de scripts.
+// script.
//
-// - [`From`] – Carga estándar con la etiqueta ``. El parámetro `name` se usa como identificador interno del
- /// script.
- ///
- /// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
- pub fn inline(name: impl Into, f: F) -> Self
- where
- F: Fn(&mut Context) -> String + Send + Sync + 'static,
- {
+ /// *script*.
+ pub fn inline(name: impl Into, script: impl Into) -> Self {
JavaScript {
- source: Source::Inline(name.into(), Box::new(f)),
+ source: Source::Inline(name.into(), script.into()),
..Default::default()
}
}
- /// Crea un **script embebido** que se ejecuta cuando **el DOM está listo**.
+ /// Crea un **script embebido** que se ejecuta automáticamente al terminar de cargarse el
+ /// documento HTML.
///
- /// El código se envuelve en un `addEventListener('DOMContentLoaded',function(){...})` que lo
- /// ejecuta tras analizar el documento HTML, **no** espera imágenes ni otros recursos externos.
- /// Útil para inicializaciones que no dependen de `await`. El parámetro `name` se usa como
- /// identificador interno del script.
- ///
- /// Los scripts con `defer` se ejecutan antes de `DOMContentLoaded`.
- ///
- /// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
- pub fn on_load(name: impl Into, f: F) -> Self
- where
- F: Fn(&mut Context) -> String + Send + Sync + 'static,
- {
+ /// El código se envuelve automáticamente en un `addEventListener('DOMContentLoaded', ...)`. El
+ /// parámetro `name` se usa como identificador interno del *script*.
+ pub fn on_load(name: impl Into, script: impl Into) -> Self {
JavaScript {
- source: Source::OnLoad(name.into(), Box::new(f)),
- ..Default::default()
- }
- }
-
- /// Crea un **script embebido** con un **manejador asíncrono**.
- ///
- /// El código se envuelve en un `addEventListener('DOMContentLoaded',async()=>{...})`, que
- /// emplea una función `async` para que el cuerpo devuelto por la función *closure* pueda usar
- /// `await`. Ideal para hidratar la interfaz, cargar módulos dinámicos o realizar lecturas
- /// iniciales.
- ///
- /// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
- pub fn on_load_async(name: impl Into, f: F) -> Self
- where
- F: Fn(&mut Context) -> String + Send + Sync + 'static,
- {
- JavaScript {
- source: Source::OnLoadAsync(name.into(), Box::new(f)),
+ source: Source::OnLoad(name.into(), script.into()),
..Default::default()
}
}
// JavaScript BUILDER **************************************************************************
- /// Asocia una **versión** al recurso (usada para control de la caché del navegador).
+ /// Asocia una versión al recurso (usada para control de la caché del navegador).
///
- /// Si `version` está vacío, **no** se añade ningún parámetro a la URL.
+ /// Si `version` está vacío, no se añade ningún parámetro a la URL.
pub fn with_version(mut self, version: impl Into) -> Self {
self.version = version.into();
self
}
- /// Modifica el **peso** del recurso.
+ /// Modifica el peso del recurso.
///
/// Los recursos se renderizan de menor a mayor peso. Por defecto es `0`, que respeta el orden
/// de creación.
@@ -191,10 +137,8 @@ impl JavaScript {
}
}
-impl Asset for JavaScript {
- /// Devuelve el nombre del recurso, utilizado como clave única.
- ///
- /// Para scripts externos es la ruta del recurso; para scripts embebidos, un identificador.
+impl AssetsTrait for JavaScript {
+ // Para *scripts* externos es la ruta; para *scripts* embebidos, un identificador.
fn name(&self) -> &str {
match &self.source {
Source::From(path) => path,
@@ -202,15 +146,16 @@ impl Asset for JavaScript {
Source::Async(path) => path,
Source::Inline(name, _) => name,
Source::OnLoad(name, _) => name,
- Source::OnLoadAsync(name, _) => name,
}
}
fn weight(&self) -> Weight {
self.weight
}
+}
- fn render(&self, cx: &mut Context) -> Markup {
+impl Render for JavaScript {
+ fn render(&self) -> Markup {
match &self.source {
Source::From(path) => html! {
script src=(join_pair!(path, "?v=", self.version.as_str())) {};
@@ -221,15 +166,12 @@ impl Asset for JavaScript {
Source::Async(path) => html! {
script src=(join_pair!(path, "?v=", self.version.as_str())) async {};
},
- Source::Inline(_, f) => html! {
- script { (PreEscaped((f)(cx))) };
+ Source::Inline(_, code) => html! {
+ script { (code) };
},
- Source::OnLoad(_, f) => html! { script { (PreEscaped(join!(
- "document.addEventListener(\"DOMContentLoaded\",function(){", (f)(cx), "});"
- ))) } },
- Source::OnLoadAsync(_, f) => html! { script { (PreEscaped(join!(
- "document.addEventListener(\"DOMContentLoaded\",async()=>{", (f)(cx), "});"
- ))) } },
+ Source::OnLoad(_, code) => html! { (join!(
+ "document.addEventListener('DOMContentLoaded',function(){", code, "});"
+ )) },
}
}
}
diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs
index 3ecc77f..bb60b01 100644
--- a/src/html/assets/stylesheet.rs
+++ b/src/html/assets/stylesheet.rs
@@ -1,5 +1,5 @@
-use crate::html::assets::Asset;
-use crate::html::{html, Context, Markup, PreEscaped};
+use crate::html::assets::AssetsTrait;
+use crate::html::{html, Markup, PreEscaped, Render};
use crate::{join_pair, AutoDefault, Weight};
// Define el origen del recurso CSS y cómo se incluye en el documento.
@@ -14,8 +14,7 @@ use crate::{join_pair, AutoDefault, Weight};
enum Source {
#[default]
From(String),
- // `name`, `closure(Context) -> String`.
- Inline(String, Box String + Send + Sync>),
+ Inline(String, String),
}
/// Define el medio objetivo para la hoja de estilos.
@@ -35,7 +34,7 @@ pub enum TargetMedia {
Speech,
}
-/// Devuelve el valor para el atributo `media` (`Some(...)`) o `None` para `Default`.
+/// Devuelve el texto asociado al punto de interrupción usado por Bootstrap.
#[rustfmt::skip]
impl TargetMedia {
fn as_str_opt(&self) -> Option<&str> {
@@ -70,12 +69,12 @@ impl TargetMedia {
/// .with_weight(-10);
///
/// // Crea una hoja de estilos embebida en el documento HTML.
-/// let embedded = StyleSheet::inline("custom_theme", |_| r#"
+/// let embedded = StyleSheet::inline("custom_theme", r#"
/// body {
/// background-color: #f5f5f5;
/// font-family: 'Segoe UI', sans-serif;
/// }
-/// "#.to_string());
+/// "#);
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
@@ -101,14 +100,9 @@ impl StyleSheet {
///
/// Equivale a ``. El parámetro `name` se usa como identificador interno del
/// recurso.
- ///
- /// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
- pub fn inline(name: impl Into, f: F) -> Self
- where
- F: Fn(&mut Context) -> String + Send + Sync + 'static,
- {
+ pub fn inline(name: impl Into, styles: impl Into) -> Self {
StyleSheet {
- source: Source::Inline(name.into(), Box::new(f)),
+ source: Source::Inline(name.into(), styles.into()),
..Default::default()
}
}
@@ -139,19 +133,17 @@ impl StyleSheet {
/// Según el argumento `media`:
///
/// - `TargetMedia::Default` - Se aplica en todos los casos (medio por defecto).
- /// - `TargetMedia::Print` - Se aplica cuando el documento se imprime.
- /// - `TargetMedia::Screen` - Se aplica en pantallas.
- /// - `TargetMedia::Speech` - Se aplica en dispositivos que convierten el texto a voz.
+ /// - `TargetMedia::Print` - Se aplican cuando el documento se imprime.
+ /// - `TargetMedia::Screen` - Se aplican en pantallas.
+ /// - `TargetMedia::Speech` - Se aplican en dispositivos que convierten el texto a voz.
pub fn for_media(mut self, media: TargetMedia) -> Self {
self.media = media;
self
}
}
-impl Asset for StyleSheet {
- /// Devuelve el nombre del recurso, utilizado como clave única.
- ///
- /// Para hojas de estilos externas es la ruta del recurso; para las embebidas, un identificador.
+impl AssetsTrait for StyleSheet {
+ // Para hojas de estilos externas es la ruta; para las embebidas, un identificador.
fn name(&self) -> &str {
match &self.source {
Source::From(path) => path,
@@ -162,8 +154,10 @@ impl Asset for StyleSheet {
fn weight(&self) -> Weight {
self.weight
}
+}
- fn render(&self, cx: &mut Context) -> Markup {
+impl Render for StyleSheet {
+ fn render(&self) -> Markup {
match &self.source {
Source::From(path) => html! {
link
@@ -171,8 +165,8 @@ impl Asset for StyleSheet {
href=(join_pair!(path, "?v=", self.version.as_str()))
media=[self.media.as_str_opt()];
},
- Source::Inline(_, f) => html! {
- style { (PreEscaped((f)(cx))) };
+ Source::Inline(_, code) => html! {
+ style { (PreEscaped(code)) };
},
}
}
diff --git a/src/html/attr_id.rs b/src/html/attr_id.rs
deleted file mode 100644
index 8bb1d33..0000000
--- a/src/html/attr_id.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-use crate::{builder_fn, AutoDefault};
-
-/// Identificador normalizado para el atributo `id` o similar de HTML.
-///
-/// Este tipo encapsula `Option` garantizando un valor normalizado para su uso:
-///
-/// - Se eliminan los espacios al principio y al final.
-/// - Se convierte a minúsculas.
-/// - Se sustituyen los espacios intermedios por guiones bajos (`_`).
-/// - Si el resultado es una cadena vacía, se guarda `None`.
-///
-/// # Ejemplo
-///
-/// ```rust
-/// use pagetop::prelude::*;
-///
-/// let id = AttrId::new(" main Section ");
-/// assert_eq!(id.as_str(), Some("main_section"));
-///
-/// let empty = AttrId::default();
-/// assert_eq!(empty.get(), None);
-/// ```
-#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
-pub struct AttrId(Option);
-
-impl AttrId {
- /// Crea un nuevo `AttrId` normalizando el valor.
- pub fn new(value: impl AsRef) -> Self {
- AttrId::default().with_value(value)
- }
-
- // AttrId BUILDER ******************************************************************************
-
- /// Establece un identificador nuevo normalizando el valor.
- #[builder_fn]
- pub fn with_value(mut self, value: impl AsRef) -> Self {
- let value = value.as_ref().trim().to_ascii_lowercase().replace(' ', "_");
- self.0 = if value.is_empty() { None } else { Some(value) };
- self
- }
-
- // AttrId GETTERS ******************************************************************************
-
- /// Devuelve el identificador normalizado, si existe.
- pub fn get(&self) -> Option {
- self.0.as_ref().cloned()
- }
-
- /// Devuelve el identificador normalizado (sin clonar), si existe.
- pub fn as_str(&self) -> Option<&str> {
- self.0.as_deref()
- }
-
- /// Devuelve el identificador normalizado (propiedad), si existe.
- pub fn into_inner(self) -> Option {
- self.0
- }
-
- /// `true` si no hay valor.
- pub fn is_empty(&self) -> bool {
- self.0.is_none()
- }
-}
diff --git a/src/html/attr_l10n.rs b/src/html/attr_l10n.rs
deleted file mode 100644
index 8250c74..0000000
--- a/src/html/attr_l10n.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use crate::html::Markup;
-use crate::locale::{L10n, LangId};
-use crate::{builder_fn, AutoDefault};
-
-/// Texto para [traducir](crate::locale) en atributos HTML.
-///
-/// Encapsula un [`L10n`] para manejar traducciones de forma segura en atributos.
-///
-/// # Ejemplo
-///
-/// ```rust
-/// use pagetop::prelude::*;
-///
-/// // Traducción por clave en las locales por defecto de PageTop.
-/// let hello = AttrL10n::new(L10n::l("test-hello-world"));
-///
-/// // Español disponible.
-/// assert_eq!(
-/// hello.lookup(&LangMatch::resolve("es-ES")),
-/// Some("¡Hola mundo!".to_string())
-/// );
-///
-/// // Japonés no disponible, traduce al idioma de respaldo ("en-US").
-/// assert_eq!(
-/// hello.lookup(&LangMatch::resolve("ja-JP")),
-/// Some("Hello world!".to_string())
-/// );
-///
-/// // Uso típico en un atributo:
-/// let title = hello.value(&LangMatch::resolve("es-ES"));
-/// // Ejemplo: html! { a title=(title) { "Link" } }
-/// ```
-#[derive(AutoDefault, Clone, Debug)]
-pub struct AttrL10n(L10n);
-
-impl AttrL10n {
- /// Crea una nueva instancia `AttrL10n`.
- pub fn new(value: L10n) -> Self {
- AttrL10n(value)
- }
-
- // AttrL10n BUILDER ****************************************************************************
-
- /// Establece una traducción nueva.
- #[builder_fn]
- pub fn with_value(mut self, value: L10n) -> Self {
- self.0 = value;
- self
- }
-
- // AttrL10n GETTERS ****************************************************************************
-
- /// Devuelve la traducción para `language`, si existe.
- pub fn lookup(&self, language: &impl LangId) -> Option {
- self.0.lookup(language)
- }
-
- /// Devuelve la traducción para `language` o una cadena vacía si no existe.
- pub fn value(&self, language: &impl LangId) -> String {
- self.0.lookup(language).unwrap_or_default()
- }
-
- /// **Obsoleto desde la versión 0.4.0**: no recomendado para atributos HTML.
- #[deprecated(since = "0.4.0", note = "For attributes use `lookup()` or `value()`")]
- pub fn to_markup(&self, language: &impl LangId) -> Markup {
- self.0.using(language)
- }
-}
diff --git a/src/html/attr_name.rs b/src/html/attr_name.rs
deleted file mode 100644
index 928f841..0000000
--- a/src/html/attr_name.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-use crate::{builder_fn, AutoDefault};
-
-/// Nombre normalizado para el atributo `name` o similar de HTML.
-///
-/// Este tipo encapsula `Option` garantizando un valor normalizado para su uso:
-///
-/// - Se eliminan los espacios al principio y al final.
-/// - Se convierte a minúsculas.
-/// - Se sustituyen los espacios intermedios por guiones bajos (`_`).
-/// - Si el resultado es una cadena vacía, se guarda `None`.
-///
-/// # Ejemplo
-///
-/// ```rust
-/// use pagetop::prelude::*;
-///
-/// let name = AttrName::new(" DISplay name ");
-/// assert_eq!(name.as_str(), Some("display_name"));
-///
-/// let empty = AttrName::default();
-/// assert_eq!(empty.get(), None);
-/// ```
-#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
-pub struct AttrName(Option);
-
-impl AttrName {
- /// Crea un nuevo `AttrName` normalizando el valor.
- pub fn new(value: impl AsRef) -> Self {
- AttrName::default().with_value(value)
- }
-
- // AttrName BUILDER ****************************************************************************
-
- /// Establece un nombre nuevo normalizando el valor.
- #[builder_fn]
- pub fn with_value(mut self, value: impl AsRef) -> Self {
- let value = value.as_ref().trim().to_ascii_lowercase().replace(' ', "_");
- self.0 = if value.is_empty() { None } else { Some(value) };
- self
- }
-
- // AttrName GETTERS ****************************************************************************
-
- /// Devuelve el nombre normalizado, si existe.
- pub fn get(&self) -> Option {
- self.0.as_ref().cloned()
- }
-
- /// Devuelve el nombre normalizado (sin clonar), si existe.
- pub fn as_str(&self) -> Option<&str> {
- self.0.as_deref()
- }
-
- /// Devuelve el nombre normalizado (propiedad), si existe.
- pub fn into_inner(self) -> Option {
- self.0
- }
-
- /// `true` si no hay valor.
- pub fn is_empty(&self) -> bool {
- self.0.is_none()
- }
-}
diff --git a/src/html/attr_value.rs b/src/html/attr_value.rs
deleted file mode 100644
index 4e03120..0000000
--- a/src/html/attr_value.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-use crate::{builder_fn, AutoDefault};
-
-/// Cadena normalizada para renderizar en atributos HTML.
-///
-/// Este tipo encapsula `Option` garantizando un valor normalizado para su uso:
-///
-/// - Se eliminan los espacios al principio y al final.
-/// - Si el resultado es una cadena vacía, se guarda `None`.
-///
-/// # Ejemplo
-///
-/// ```rust
-/// use pagetop::prelude::*;
-///
-/// let s = AttrValue::new(" a new string ");
-/// assert_eq!(s.as_str(), Some("a new string"));
-///
-/// let empty = AttrValue::default();
-/// assert_eq!(empty.get(), None);
-/// ```
-#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
-pub struct AttrValue(Option);
-
-impl AttrValue {
- /// Crea un nuevo `AttrValue` normalizando el valor.
- pub fn new(value: impl AsRef) -> Self {
- AttrValue::default().with_value(value)
- }
-
- // AttrValue BUILDER ***************************************************************************
-
- /// Establece una cadena nueva normalizando el valor.
- #[builder_fn]
- pub fn with_value(mut self, value: impl AsRef) -> Self {
- let value = value.as_ref().trim();
- self.0 = if value.is_empty() {
- None
- } else {
- Some(value.to_string())
- };
- self
- }
-
- // AttrValue GETTERS ***************************************************************************
-
- /// Devuelve la cadena normalizada, si existe.
- pub fn get(&self) -> Option {
- self.0.as_ref().cloned()
- }
-
- /// Devuelve la cadena normalizada (sin clonar), si existe.
- pub fn as_str(&self) -> Option<&str> {
- self.0.as_deref()
- }
-
- /// Devuelve la cadena normalizada (propiedad), si existe.
- pub fn into_inner(self) -> Option {
- self.0
- }
-
- /// `true` si no hay valor.
- pub fn is_empty(&self) -> bool {
- self.0.is_none()
- }
-}
diff --git a/src/html/context.rs b/src/html/context.rs
index 7b78268..5fbb39b 100644
--- a/src/html/context.rs
+++ b/src/html/context.rs
@@ -7,10 +7,13 @@ use crate::locale::{LangId, LangMatch, LanguageIdentifier, DEFAULT_LANGID, FALLB
use crate::service::HttpRequest;
use crate::{builder_fn, join};
-use std::any::Any;
use std::collections::HashMap;
+use std::error::Error;
+use std::str::FromStr;
-/// Operaciones para modificar el contexto ([`Context`]) de un documento.
+use std::fmt;
+
+/// Operaciones para modificar el contexto ([`Context`]) del documento.
pub enum AssetsOp {
// Favicon.
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
@@ -25,138 +28,38 @@ pub enum AssetsOp {
RemoveStyleSheet(&'static str),
// JavaScripts.
- /// Añade un script JavaScript al documento.
+ /// Añade un *script* JavaScript al documento.
AddJavaScript(JavaScript),
- /// Elimina un script por su ruta o identificador.
+ /// Elimina un *script* por su ruta o identificador.
RemoveJavaScript(&'static str),
}
-/// Errores de acceso a parámetros dinámicos del contexto.
-///
-/// - [`ErrorParam::NotFound`]: la clave no existe.
-/// - [`ErrorParam::TypeMismatch`]: la clave existe, pero el valor guardado no coincide con el tipo
-/// solicitado. Incluye nombre de la clave (`key`), tipo esperado (`expected`) y tipo realmente
-/// guardado (`saved`) para facilitar el diagnóstico.
+/// Errores de lectura o conversión de parámetros almacenados en el contexto.
#[derive(Debug)]
pub enum ErrorParam {
+ /// El parámetro solicitado no existe.
NotFound,
- TypeMismatch {
- key: &'static str,
- expected: &'static str,
- saved: &'static str,
- },
+ /// El valor del parámetro no pudo convertirse al tipo requerido.
+ ParseError(String),
}
-/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
-///
-/// `Contextual` extiende [`LangId`] y define los métodos para:
-///
-/// - Establecer el **idioma** del documento.
-/// - Almacenar la **solicitud HTTP** de origen.
-/// - Seleccionar **tema** y **composición** (*layout*) de renderizado.
-/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
-/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`].
-/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
-/// - Generar **identificadores únicos** por tipo de componente.
-///
-/// Lo implementan, típicamente, estructuras que representan el contexto de renderizado, como
-/// [`Context`](crate::html::Context) o [`Page`](crate::response::page::Page).
-///
-/// # Ejemplo
-///
-/// ```rust
-/// use pagetop::prelude::*;
-///
-/// fn prepare_context(cx: C) -> C {
-/// cx.with_langid(&LangMatch::resolve("es-ES"))
-/// .with_theme("aliner")
-/// .with_layout("default")
-/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
-/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
-/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js")))
-/// .with_param("usuario_id", 42_i32)
-/// }
-/// ```
-pub trait Contextual: LangId {
- // Contextual BUILDER **************************************************************************
-
- /// Establece el idioma del documento.
- #[builder_fn]
- fn with_langid(self, language: &impl LangId) -> Self;
-
- /// Almacena la solicitud HTTP de origen en el contexto.
- #[builder_fn]
- fn with_request(self, request: Option) -> Self;
-
- /// Especifica el tema para renderizar el documento.
- #[builder_fn]
- fn with_theme(self, theme_name: &'static str) -> Self;
-
- /// Especifica la composición para renderizar el documento.
- #[builder_fn]
- fn with_layout(self, layout_name: &'static str) -> Self;
-
- /// Añade o modifica un parámetro dinámico del contexto.
- #[builder_fn]
- fn with_param(self, key: &'static str, value: T) -> Self;
-
- /// Define los recursos del contexto usando [`AssetsOp`].
- #[builder_fn]
- fn with_assets(self, op: AssetsOp) -> Self;
-
- // Contextual GETTERS **************************************************************************
-
- /// Devuelve una referencia a la solicitud HTTP asociada, si existe.
- fn request(&self) -> Option<&HttpRequest>;
-
- /// Devuelve el tema que se usará para renderizar el documento.
- fn theme(&self) -> ThemeRef;
-
- /// Devuelve la composición para renderizar el documento. Por defecto es `"default"`.
- fn layout(&self) -> &str;
-
- /// Recupera un parámetro como [`Option`].
- fn param(&self, key: &'static str) -> Option<&T>;
-
- /// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
- fn param_or_default(&self, key: &'static str) -> T {
- self.param::(key).cloned().unwrap_or_default()
+impl fmt::Display for ErrorParam {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ErrorParam::NotFound => write!(f, "Parameter not found"),
+ ErrorParam::ParseError(e) => write!(f, "Parse error: {e}"),
+ }
}
-
- /// Devuelve el parámetro clonado o un **valor por defecto** si no existe.
- fn param_or(&self, key: &'static str, default: T) -> T {
- self.param::(key).cloned().unwrap_or(default)
- }
-
- /// Devuelve el parámetro clonado o el **valor evaluado** por la función `f` si no existe.
- fn param_or_else T>(&self, key: &'static str, f: F) -> T {
- self.param::(key).cloned().unwrap_or_else(f)
- }
-
- /// Devuelve el Favicon de los recursos del contexto.
- fn favicon(&self) -> Option<&Favicon>;
-
- /// Devuelve las hojas de estilo de los recursos del contexto.
- fn stylesheets(&self) -> &Assets;
-
- /// Devuelve los scripts JavaScript de los recursos del contexto.
- fn javascripts(&self) -> &Assets;
-
- // Contextual HELPERS **************************************************************************
-
- /// Genera un identificador único por tipo (`-`) cuando no se aporta uno explícito.
- ///
- /// Es útil para componentes u otros elementos HTML que necesitan un identificador predecible si
- /// no se proporciona ninguno.
- fn required_id(&mut self, id: Option) -> String;
}
-/// Implementa un **contexto de renderizado** para un documento HTML.
+impl Error for ErrorParam {}
+
+/// Representa el contexto de un documento HTML.
///
-/// Extiende [`Contextual`] con métodos para **instanciar** y configurar un nuevo contexto,
-/// **renderizar los recursos** del documento (incluyendo el [`Favicon`], las hojas de estilo
-/// [`StyleSheet`] y los scripts [`JavaScript`]), o extender el uso de **parámetros dinámicos
-/// tipados** con nuevos métodos.
+/// Se crea internamente para manejar información relevante del documento, como la solicitud HTTP de
+/// origen, el idioma, tema y composición para el renderizado, los recursos *favicon* ([`Favicon`]),
+/// hojas de estilo ([`StyleSheet`]) y *scripts* ([`JavaScript`]), así como parámetros de contexto
+/// definidos en tiempo de ejecución.
///
/// # Ejemplos
///
@@ -193,7 +96,7 @@ pub trait Contextual: LangId {
/// assert_eq!(active_theme.short_name(), "aliner");
///
/// // Recupera el parámetro a su tipo original.
-/// let id: i32 = *cx.get_param::("usuario_id").unwrap();
+/// let id: i32 = cx.get_param("usuario_id").unwrap();
/// assert_eq!(id, 42);
///
/// // Genera un identificador para un componente de tipo `Menu`.
@@ -211,16 +114,10 @@ pub struct Context {
favicon : Option, // Favicon, si se ha definido.
stylesheets: Assets, // Hojas de estilo CSS.
javascripts: Assets, // Scripts JavaScript.
- params : HashMap<&'static str, (Box, &'static str)>, // Parámetros en ejecución.
+ params : HashMap<&'static str, String>, // Parámetros definidos en tiempo de ejecución.
id_counter : usize, // Contador para generar identificadores únicos.
}
-impl Default for Context {
- fn default() -> Self {
- Context::new(None)
- }
-}
-
impl Context {
/// Crea un nuevo contexto asociado a una solicitud HTTP.
///
@@ -250,199 +147,40 @@ impl Context {
favicon : None,
stylesheets: Assets::::new(),
javascripts: Assets::::new(),
- params : HashMap::default(),
+ params : HashMap::<&str, String>::new(),
id_counter : 0,
}
}
- // Context RENDER ******************************************************************************
-
- /// Renderiza los recursos del contexto.
- pub fn render_assets(&mut self) -> Markup {
- use std::mem::take as mem_take;
-
- // Extrae temporalmente los recursos.
- let favicon = mem_take(&mut self.favicon); // Deja valor por defecto (None) en self.
- let stylesheets = mem_take(&mut self.stylesheets); // Assets::default() en self.
- let javascripts = mem_take(&mut self.javascripts); // Assets::default() en self.
-
- // Renderiza con `&mut self` como contexto.
- let markup = html! {
- @if let Some(fi) = &favicon {
- (fi.render(self))
- }
- (stylesheets.render(self))
- (javascripts.render(self))
- };
-
- // Restaura los campos tal y como estaban.
- self.favicon = favicon;
- self.stylesheets = stylesheets;
- self.javascripts = javascripts;
-
- markup
- }
-
- // Context PARAMS ******************************************************************************
-
- /// Recupera una *referencia tipada* al parámetro solicitado.
- ///
- /// Devuelve:
- ///
- /// - `Ok(&T)` si la clave existe y el tipo coincide.
- /// - `Err(ErrorParam::NotFound)` si la clave no existe.
- /// - `Err(ErrorParam::TypeMismatch)` si la clave existe pero el tipo no coincide.
- ///
- /// # Ejemplos
- ///
- /// ```rust
- /// use pagetop::prelude::*;
- ///
- /// let cx = Context::new(None)
- /// .with_param("usuario_id", 42_i32)
- /// .with_param("titulo", "Hola".to_string());
- ///
- /// let id: &i32 = cx.get_param("usuario_id").unwrap();
- /// let titulo: &String = cx.get_param("titulo").unwrap();
- ///
- /// // Error de tipo:
- /// assert!(cx.get_param::("usuario_id").is_err());
- /// ```
- pub fn get_param(&self, key: &'static str) -> Result<&T, ErrorParam> {
- let (any, type_name) = self.params.get(key).ok_or(ErrorParam::NotFound)?;
- any.downcast_ref::()
- .ok_or_else(|| ErrorParam::TypeMismatch {
- key,
- expected: TypeInfo::FullName.of::(),
- saved: type_name,
- })
- }
-
- /// Recupera el parámetro solicitado y lo elimina del contexto.
- ///
- /// Devuelve:
- ///
- /// - `Ok(T)` si la clave existía y el tipo coincide.
- /// - `Err(ErrorParam::NotFound)` si la clave no existe.
- /// - `Err(ErrorParam::TypeMismatch)` si el tipo no coincide.
- ///
- /// # Ejemplos
- ///
- /// ```rust
- /// use pagetop::prelude::*;
- ///
- /// let mut cx = Context::new(None)
- /// .with_param("contador", 7_i32)
- /// .with_param("titulo", "Hola".to_string());
- ///
- /// let n: i32 = cx.take_param("contador").unwrap();
- /// assert!(cx.get_param::("contador").is_err()); // ya no está
- ///
- /// // Error de tipo:
- /// assert!(cx.take_param::("titulo").is_err());
- /// ```
- pub fn take_param(&mut self, key: &'static str) -> Result {
- let (boxed, saved) = self.params.remove(key).ok_or(ErrorParam::NotFound)?;
- boxed
- .downcast::()
- .map(|b| *b)
- .map_err(|_| ErrorParam::TypeMismatch {
- key,
- expected: TypeInfo::FullName.of::(),
- saved,
- })
- }
-
- /// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó.
- ///
- /// Devuelve `false` en caso contrario. Usar cuando solo interesa borrar la entrada.
- ///
- /// # Ejemplos
- ///
- /// ```rust
- /// use pagetop::prelude::*;
- ///
- /// let mut cx = Context::new(None).with_param("temp", 1u8);
- /// assert!(cx.remove_param("temp"));
- /// assert!(!cx.remove_param("temp")); // ya no existe
- /// ```
- pub fn remove_param(&mut self, key: &'static str) -> bool {
- self.params.remove(key).is_some()
- }
-}
-
-/// Permite a [`Context`](crate::html::Context) actuar como proveedor de idioma.
-///
-/// Devuelve un [`LanguageIdentifier`] siguiendo este orden de prioridad:
-///
-/// 1. Un idioma válido establecido explícitamente con [`Context::with_langid`].
-/// 2. El idioma por defecto configurado para la aplicación.
-/// 3. Un idioma válido extraído de la cabecera `Accept-Language` del navegador.
-/// 4. Y si ninguna de las opciones anteriores aplica, se usa el idioma de respaldo (`"en-US"`).
-///
-/// Resulta útil para usar un contexto ([`Context`]) como fuente de traducción en
-/// [`L10n::lookup()`](crate::locale::L10n::lookup) o [`L10n::using()`](crate::locale::L10n::using).
-impl LangId for Context {
- fn langid(&self) -> &'static LanguageIdentifier {
- self.langid
- }
-}
-
-impl Contextual for Context {
- // Contextual BUILDER **************************************************************************
+ // Context BUILDER *****************************************************************************
+ /// Modifica la fuente de idioma del documento.
#[builder_fn]
- fn with_request(mut self, request: Option) -> Self {
- self.request = request;
- self
- }
-
- #[builder_fn]
- fn with_langid(mut self, language: &impl LangId) -> Self {
+ pub fn with_langid(mut self, language: &impl LangId) -> Self {
self.langid = language.langid();
self
}
- /// Asigna el tema para renderizar el documento.
+ /// Modifica el tema que se usará para renderizar el documento.
///
/// Localiza el tema por su [`short_name()`](crate::core::AnyInfo::short_name), y si no aplica
/// ninguno entonces usará el tema por defecto.
#[builder_fn]
- fn with_theme(mut self, theme_name: &'static str) -> Self {
+ pub fn with_theme(mut self, theme_name: &'static str) -> Self {
self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME);
self
}
+ /// Modifica la composición para renderizar el documento.
#[builder_fn]
- fn with_layout(mut self, layout_name: &'static str) -> Self {
+ pub fn with_layout(mut self, layout_name: &'static str) -> Self {
self.layout = layout_name;
self
}
- /// Añade o modifica un parámetro dinámico del contexto.
- ///
- /// El valor se guarda conservando el *nombre del tipo* real para mejorar los mensajes de error
- /// posteriores.
- ///
- /// # Ejemplos
- ///
- /// ```rust
- /// use pagetop::prelude::*;
- ///
- /// let cx = Context::new(None)
- /// .with_param("usuario_id", 42_i32)
- /// .with_param("titulo", "Hola".to_string())
- /// .with_param("flags", vec!["a", "b"]);
- /// ```
+ /// Define los recursos del contexto usando [`AssetsOp`].
#[builder_fn]
- fn with_param(mut self, key: &'static str, value: T) -> Self {
- let type_name = TypeInfo::FullName.of::();
- self.params.insert(key, (Box::new(value), type_name));
- self
- }
-
- #[builder_fn]
- fn with_assets(mut self, op: AssetsOp) -> Self {
+ pub fn with_assets(mut self, op: AssetsOp) -> Self {
match op {
// Favicon.
AssetsOp::SetFavicon(favicon) => {
@@ -471,74 +209,69 @@ impl Contextual for Context {
self
}
- // Contextual GETTERS **************************************************************************
+ // Context GETTERS *****************************************************************************
- fn request(&self) -> Option<&HttpRequest> {
+ /// Devuelve una referencia a la solicitud HTTP asociada, si existe.
+ pub fn request(&self) -> Option<&HttpRequest> {
self.request.as_ref()
}
- fn theme(&self) -> ThemeRef {
+ /// Devuelve el tema que se usará para renderizar el documento.
+ pub fn theme(&self) -> ThemeRef {
self.theme
}
- fn layout(&self) -> &str {
+ /// Devuelve la composición para renderizar el documento. Por defecto es `"default"`.
+ pub fn layout(&self) -> &str {
self.layout
}
- /// Recupera un parámetro como [`Option`], simplificando el acceso.
- ///
- /// A diferencia de [`get_param`](Self::get_param), que devuelve un [`Result`] con información
- /// detallada de error, este método devuelve `None` tanto si la clave no existe como si el valor
- /// guardado no coincide con el tipo solicitado.
- ///
- /// Resulta útil en escenarios donde sólo interesa saber si el valor existe y es del tipo
- /// correcto, sin necesidad de diferenciar entre error de ausencia o de tipo.
- ///
- /// # Ejemplo
- ///
- /// ```rust
- /// use pagetop::prelude::*;
- ///
- /// let cx = Context::new(None).with_param("username", "Alice".to_string());
- ///
- /// // Devuelve Some(&String) si existe y coincide el tipo.
- /// assert_eq!(cx.param::("username").map(|s| s.as_str()), Some("Alice"));
- ///
- /// // Devuelve None si no existe o si el tipo no coincide.
- /// assert!(cx.param::("username").is_none());
- /// assert!(cx.param::("missing").is_none());
- ///
- /// // Acceso con valor por defecto.
- /// let user = cx.param::("missing")
- /// .cloned()
- /// .unwrap_or_else(|| "visitor".to_string());
- /// assert_eq!(user, "visitor");
- /// ```
- fn param(&self, key: &'static str) -> Option<&T> {
- self.get_param::(key).ok()
+ // Context RENDER ******************************************************************************
+
+ /// Renderiza los recursos del contexto.
+ pub fn render_assets(&self) -> Markup {
+ html! {
+ @if let Some(favicon) = &self.favicon {
+ (favicon)
+ }
+ (self.stylesheets)
+ (self.javascripts)
+ }
}
- fn favicon(&self) -> Option<&Favicon> {
- self.favicon.as_ref()
+ // Context PARAMS ******************************************************************************
+
+ /// Añade o modifica un parámetro del contexto almacenando el valor como [`String`].
+ #[builder_fn]
+ pub fn with_param(mut self, key: &'static str, value: T) -> Self {
+ self.params.insert(key, value.to_string());
+ self
}
- fn stylesheets(&self) -> &Assets {
- &self.stylesheets
+ /// Recupera un parámetro del contexto convertido al tipo especificado.
+ ///
+ /// Devuelve un error si el parámetro no existe ([`ErrorParam::NotFound`]) o la conversión falla
+ /// ([`ErrorParam::ParseError`]).
+ pub fn get_param(&self, key: &'static str) -> Result {
+ self.params
+ .get(key)
+ .ok_or(ErrorParam::NotFound)
+ .and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone())))
}
- fn javascripts(&self) -> &Assets {
- &self.javascripts
+ /// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó.
+ pub fn remove_param(&mut self, key: &'static str) -> bool {
+ self.params.remove(key).is_some()
}
- // Contextual HELPERS **************************************************************************
+ // Context EXTRAS ******************************************************************************
- /// Devuelve un identificador único dentro del contexto para el tipo `T`, si no se proporciona
- /// un `id` explícito.
+ /// Genera un identificador único si no se proporciona uno explícito.
///
/// Si no se proporciona un `id`, se genera un identificador único en la forma `-`
/// donde `` es el nombre corto del tipo en minúsculas (sin espacios) y `` es un
/// contador interno incremental.
- fn required_id(&mut self, id: Option) -> String {
+ pub fn required_id(&mut self, id: Option) -> String {
if let Some(id) = id {
id
} else {
@@ -548,7 +281,7 @@ impl Contextual for Context {
.replace(' ', "_")
.to_lowercase();
let prefix = if prefix.is_empty() {
- "prefix".to_string()
+ "prefix".to_owned()
} else {
prefix
};
@@ -557,3 +290,21 @@ impl Contextual for Context {
}
}
}
+
+/// Permite a [`Context`](crate::html::Context) actuar como proveedor de idioma.
+///
+/// Devuelve un [`LanguageIdentifier`] siguiendo este orden de prioridad:
+///
+/// 1. Un idioma válido establecido explícitamente con [`Context::with_langid`].
+/// 2. El idioma por defecto configurado para la aplicación.
+/// 3. Un idioma válido extraído de la cabecera `Accept-Language` del navegador.
+/// 4. Y si ninguna de las opciones anteriores aplica, se usa el idioma de respaldo (`"en-US"`).
+///
+/// Resulta útil para usar un contexto ([`Context`]) como fuente de traducción en
+/// [`L10n::using()`](crate::locale::L10n::using) o
+/// [`L10n::to_markup()`](crate::locale::L10n::to_markup).
+impl LangId for Context {
+ fn langid(&self) -> &'static LanguageIdentifier {
+ self.langid
+ }
+}
diff --git a/src/html/maud.rs b/src/html/maud.rs
index 6536036..9bf179e 100644
--- a/src/html/maud.rs
+++ b/src/html/maud.rs
@@ -69,6 +69,23 @@ impl fmt::Write for Escaper<'_> {
/// `.render()` or `.render_to()`. Since the default definitions of
/// these methods call each other, not doing this will result in
/// infinite recursion.
+///
+/// # Example
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// /// Provides a shorthand for linking to a CSS stylesheet.
+/// pub struct Stylesheet(&'static str);
+///
+/// impl Render for Stylesheet {
+/// fn render(&self) -> Markup {
+/// html! {
+/// link rel="stylesheet" type="text/css" href=(self.0);
+/// }
+/// }
+/// }
+/// ```
pub trait Render {
/// Renders `self` as a block of `Markup`.
fn render(&self) -> Markup {
@@ -221,10 +238,6 @@ impl Markup {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
-
- pub fn as_str(&self) -> &str {
- self.0.as_str()
- }
}
impl> PreEscaped {
diff --git a/src/html/attr_classes.rs b/src/html/opt_classes.rs
similarity index 84%
rename from src/html/attr_classes.rs
rename to src/html/opt_classes.rs
index 098c26c..abb3ba4 100644
--- a/src/html/attr_classes.rs
+++ b/src/html/opt_classes.rs
@@ -1,6 +1,6 @@
use crate::{builder_fn, AutoDefault};
-/// Operaciones disponibles sobre la lista de clases en [`AttrClasses`].
+/// Operaciones disponibles sobre la lista de clases en [`OptionClasses`].
pub enum ClassesOp {
/// Añade al final (si no existe).
Add,
@@ -25,7 +25,6 @@ pub enum ClassesOp {
///
/// - El [orden de las clases no es relevante](https://stackoverflow.com/a/1321712) en CSS.
/// - No se permiten clases duplicadas.
-/// - Las clases se convierten a minúsculas.
/// - Las clases vacías se ignoran.
///
/// # Ejemplo
@@ -33,26 +32,26 @@ pub enum ClassesOp {
/// ```rust
/// use pagetop::prelude::*;
///
-/// let classes = AttrClasses::new("Btn btn-primary")
-/// .with_value(ClassesOp::Add, "Active")
+/// let classes = OptionClasses::new("btn btn-primary")
+/// .with_value(ClassesOp::Add, "active")
/// .with_value(ClassesOp::Remove, "btn-primary");
///
-/// assert_eq!(classes.get(), Some("btn active".to_string()));
+/// assert_eq!(classes.get(), Some(String::from("btn active")));
/// assert!(classes.contains("active"));
/// ```
#[derive(AutoDefault, Clone, Debug)]
-pub struct AttrClasses(Vec);
+pub struct OptionClasses(Vec);
-impl AttrClasses {
+impl OptionClasses {
pub fn new(classes: impl AsRef) -> Self {
- AttrClasses::default().with_value(ClassesOp::Prepend, classes)
+ OptionClasses::default().with_value(ClassesOp::Prepend, classes)
}
- // AttrClasses BUILDER *************************************************************************
+ // OptionClasses BUILDER ***********************************************************************
#[builder_fn]
pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef) -> Self {
- let classes = classes.as_ref().to_ascii_lowercase();
+ let classes: &str = classes.as_ref();
let classes: Vec<&str> = classes.split_ascii_whitespace().collect();
if classes.is_empty() {
@@ -114,9 +113,9 @@ impl AttrClasses {
}
}
- // AttrClasses GETTERS *************************************************************************
+ // OptionClasses GETTERS ***********************************************************************
- /// Devuelve la cadena de clases, si existe.
+ /// Devuele la cadena de clases, si existe.
pub fn get(&self) -> Option {
if self.0.is_empty() {
None
diff --git a/src/html/opt_component.rs b/src/html/opt_component.rs
new file mode 100644
index 0000000..39106d9
--- /dev/null
+++ b/src/html/opt_component.rs
@@ -0,0 +1,68 @@
+use crate::builder_fn;
+use crate::core::component::{Component, Typed};
+use crate::html::{html, Context, Markup};
+
+/// Contenedor de componente para incluir en otros componentes.
+///
+/// Este tipo encapsula `Option>` para incluir un componente de manera segura en otros
+/// componentes, útil para representar estructuras complejas.
+///
+/// # Ejemplo
+///
+/// ```rust,ignore
+/// use pagetop::prelude::*;
+///
+/// let comp = MyComponent::new();
+/// let opt = OptionComponent::new(comp);
+/// assert!(opt.get().is_some());
+/// ```
+pub struct OptionComponent(Option>);
+
+impl Default for OptionComponent {
+ fn default() -> Self {
+ OptionComponent(None)
+ }
+}
+
+impl OptionComponent {
+ /// Crea un nuevo [`OptionComponent`].
+ ///
+ /// El componente se envuelve automáticamente en un [`Typed`] y se almacena.
+ pub fn new(component: C) -> Self {
+ OptionComponent::default().with_value(Some(component))
+ }
+
+ // OptionComponent BUILDER *********************************************************************
+
+ /// Establece un componente nuevo, o lo vacía.
+ ///
+ /// Si se proporciona `Some(component)`, se guarda en [`Typed`]; y si es `None`, se limpia.
+ #[builder_fn]
+ pub fn with_value(mut self, component: Option) -> Self {
+ if let Some(component) = component {
+ self.0 = Some(Typed::with(component));
+ } else {
+ self.0 = None;
+ }
+ self
+ }
+
+ // OptionComponent GETTERS *********************************************************************
+
+ /// Devuelve el componente, si existe.
+ pub fn get(&self) -> Option> {
+ if let Some(value) = &self.0 {
+ return Some(value.clone());
+ }
+ None
+ }
+
+ /// Renderiza el componente, si existe.
+ pub fn render(&self, cx: &mut Context) -> Markup {
+ if let Some(component) = &self.0 {
+ component.render(cx)
+ } else {
+ html! {}
+ }
+ }
+}
diff --git a/src/html/opt_id.rs b/src/html/opt_id.rs
new file mode 100644
index 0000000..893ac6d
--- /dev/null
+++ b/src/html/opt_id.rs
@@ -0,0 +1,58 @@
+use crate::{builder_fn, AutoDefault};
+
+/// Identificador normalizado para el atributo `id` o similar de HTML.
+///
+/// Este tipo encapsula `Option` garantizando un valor normalizado para su uso.
+///
+/// # Normalización
+///
+/// - Se eliminan los espacios al principio y al final.
+/// - Se sustituyen los espacios intermedios por guiones bajos (`_`).
+/// - Si el resultado es una cadena vacía, se guarda `None`.
+///
+/// # Ejemplo
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// let id = OptionId::new("main section");
+/// assert_eq!(id.get(), Some(String::from("main_section")));
+///
+/// let empty = OptionId::default();
+/// assert_eq!(empty.get(), None);
+/// ```
+#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
+pub struct OptionId(Option);
+
+impl OptionId {
+ /// Crea un nuevo [`OptionId`].
+ ///
+ /// El valor se normaliza automáticamente.
+ pub fn new(value: impl AsRef) -> Self {
+ OptionId::default().with_value(value)
+ }
+
+ // OptionId BUILDER ****************************************************************************
+
+ /// Establece un identificador nuevo.
+ ///
+ /// El valor se normaliza automáticamente.
+ #[builder_fn]
+ pub fn with_value(mut self, value: impl AsRef) -> Self {
+ let value = value.as_ref().trim().replace(' ', "_");
+ self.0 = (!value.is_empty()).then_some(value);
+ self
+ }
+
+ // OptionId GETTERS ****************************************************************************
+
+ /// Devuelve el identificador, si existe.
+ pub fn get(&self) -> Option {
+ if let Some(value) = &self.0 {
+ if !value.is_empty() {
+ return Some(value.to_owned());
+ }
+ }
+ None
+ }
+}
diff --git a/src/html/opt_name.rs b/src/html/opt_name.rs
new file mode 100644
index 0000000..aa74e3b
--- /dev/null
+++ b/src/html/opt_name.rs
@@ -0,0 +1,58 @@
+use crate::{builder_fn, AutoDefault};
+
+/// Nombre normalizado para el atributo `name` o similar de HTML.
+///
+/// Este tipo encapsula `Option` garantizando un valor normalizado para su uso.
+///
+/// # Normalización
+///
+/// - Se eliminan los espacios al principio y al final.
+/// - Se sustituyen los espacios intermedios por guiones bajos (`_`).
+/// - Si el resultado es una cadena vacía, se guarda `None`.
+///
+/// # Ejemplo
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// let name = OptionName::new(" display name ");
+/// assert_eq!(name.get(), Some(String::from("display_name")));
+///
+/// let empty = OptionName::default();
+/// assert_eq!(empty.get(), None);
+/// ```
+#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
+pub struct OptionName(Option);
+
+impl OptionName {
+ /// Crea un nuevo [`OptionName`].
+ ///
+ /// El valor se normaliza automáticamente.
+ pub fn new(value: impl AsRef) -> Self {
+ OptionName::default().with_value(value)
+ }
+
+ // OptionName BUILDER **************************************************************************
+
+ /// Establece un nombre nuevo.
+ ///
+ /// El valor se normaliza automáticamente.
+ #[builder_fn]
+ pub fn with_value(mut self, value: impl AsRef) -> Self {
+ let value = value.as_ref().trim().replace(' ', "_");
+ self.0 = (!value.is_empty()).then_some(value);
+ self
+ }
+
+ // OptionName GETTERS **************************************************************************
+
+ /// Devuelve el nombre, si existe.
+ pub fn get(&self) -> Option {
+ if let Some(value) = &self.0 {
+ if !value.is_empty() {
+ return Some(value.to_owned());
+ }
+ }
+ None
+ }
+}
diff --git a/src/html/opt_string.rs b/src/html/opt_string.rs
new file mode 100644
index 0000000..5bfd9c7
--- /dev/null
+++ b/src/html/opt_string.rs
@@ -0,0 +1,57 @@
+use crate::{builder_fn, AutoDefault};
+
+/// Cadena normalizada para renderizar en atributos HTML.
+///
+/// Este tipo encapsula `Option` garantizando un valor normalizado para su uso.
+///
+/// # Normalización
+///
+/// - Se eliminan los espacios al principio y al final.
+/// - Si el resultado es una cadena vacía, se guarda `None`.
+///
+/// # Ejemplo
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// let s = OptionString::new(" a new string ");
+/// assert_eq!(s.get(), Some(String::from("a new string")));
+///
+/// let empty = OptionString::default();
+/// assert_eq!(empty.get(), None);
+/// ```
+#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
+pub struct OptionString(Option);
+
+impl OptionString {
+ /// Crea un nuevo [`OptionString`].
+ ///
+ /// El valor se normaliza automáticamente.
+ pub fn new(value: impl AsRef) -> Self {
+ OptionString::default().with_value(value)
+ }
+
+ // OptionString BUILDER ************************************************************************
+
+ /// Establece una cadena nueva.
+ ///
+ /// El valor se normaliza automáticamente.
+ #[builder_fn]
+ pub fn with_value(mut self, value: impl AsRef) -> Self {
+ let value = value.as_ref().trim().to_owned();
+ self.0 = (!value.is_empty()).then_some(value);
+ self
+ }
+
+ // OptionString GETTERS ************************************************************************
+
+ /// Devuelve la cadena, si existe.
+ pub fn get(&self) -> Option {
+ if let Some(value) = &self.0 {
+ if !value.is_empty() {
+ return Some(value.to_owned());
+ }
+ }
+ None
+ }
+}
diff --git a/src/html/opt_translated.rs b/src/html/opt_translated.rs
new file mode 100644
index 0000000..b15ea18
--- /dev/null
+++ b/src/html/opt_translated.rs
@@ -0,0 +1,65 @@
+use crate::html::Markup;
+use crate::locale::{L10n, LangId};
+use crate::{builder_fn, AutoDefault};
+
+/// Cadena para traducir al renderizar ([`locale`](crate::locale)).
+///
+/// Encapsula un tipo [`L10n`] para manejar traducciones de forma segura.
+///
+/// # Ejemplo
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// // Traducción por clave en las locales por defecto de PageTop.
+/// let hello = OptionTranslated::new(L10n::l("test-hello-world"));
+///
+/// // Español disponible.
+/// assert_eq!(
+/// hello.using(&LangMatch::resolve("es-ES")),
+/// Some(String::from("¡Hola mundo!"))
+/// );
+///
+/// // Japonés no disponible, traduce al idioma de respaldo ("en-US").
+/// assert_eq!(
+/// hello.using(&LangMatch::resolve("ja-JP")),
+/// Some(String::from("Hello world!"))
+/// );
+///
+/// // Para incrustar en HTML escapado:
+/// let markup = hello.to_markup(&LangMatch::resolve("es-ES"));
+/// assert_eq!(markup.into_string(), "¡Hola mundo!");
+/// ```
+#[derive(AutoDefault, Clone, Debug)]
+pub struct OptionTranslated(L10n);
+
+impl OptionTranslated {
+ /// Crea una nueva instancia [`OptionTranslated`].
+ pub fn new(value: L10n) -> Self {
+ OptionTranslated(value)
+ }
+
+ // OptionTranslated BUILDER ********************************************************************
+
+ /// Establece una traducción nueva.
+ #[builder_fn]
+ pub fn with_value(mut self, value: L10n) -> Self {
+ self.0 = value;
+ self
+ }
+
+ // OptionTranslated GETTERS ********************************************************************
+
+ /// Devuelve la traducción para `language`, si existe.
+ pub fn using(&self, language: &impl LangId) -> Option {
+ self.0.using(language)
+ }
+
+ /// Devuelve la traducción *escapada* como [`Markup`] para `language`, si existe.
+ ///
+ /// Útil para incrustar el texto directamente en plantillas HTML sin riesgo de inyección de
+ /// contenido.
+ pub fn to_markup(&self, language: &impl LangId) -> Markup {
+ self.0.to_markup(language)
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 1c1ba2c..90ea462 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -15,8 +15,8 @@
-PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
-creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
+`PageTop` reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para
+la creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
según las necesidades de cada proyecto, incluyendo:
@@ -25,14 +25,14 @@ según las necesidades de cada proyecto, incluyendo:
* **Componentes** (*components*): encapsulan HTML, CSS y JavaScript en unidades funcionales,
configurables y reutilizables.
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
- de PageTop o de terceros.
+ de `PageTop` o de terceros.
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
componentes sin comprometer su funcionalidad.
# ⚡️ Guía rápida
-La aplicación más sencilla de PageTop se ve así:
+La aplicación más sencilla de `PageTop` se ve así:
```rust,no_run
use pagetop::prelude::*;
@@ -43,10 +43,10 @@ async fn main() -> std::io::Result<()> {
}
```
-Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
-bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
+Este código arranca el servidor de `PageTop`. Con la configuración por defecto, muestra una página
+de bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
-Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera:
+Para personalizar el servicio, se puede crear una extensión de `PageTop` de la siguiente manera:
```rust,no_run
use pagetop::prelude::*;
@@ -60,8 +60,8 @@ impl Extension for HelloWorld {
}
async fn hello_world(request: HttpRequest) -> ResultPage {
- Page::new(request)
- .add_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
+ Page::new(Some(request))
+ .with_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
.render()
}
@@ -77,11 +77,11 @@ Este programa implementa una extensión llamada `HelloWorld` que sirve una pági
# 🧩 Gestión de Dependencias
-Los proyectos que utilizan PageTop gestionan las dependencias con `cargo`, como cualquier otro
+Los proyectos que utilizan `PageTop` gestionan las dependencias con `cargo`, como cualquier otro
proyecto en Rust.
Sin embargo, es fundamental que cada extensión declare explícitamente sus
-[dependencias](core::extension::Extension::dependencies), si las tiene, para que PageTop pueda
+[dependencias](core::extension::Extension::dependencies), si las tiene, para que `PageTop` pueda
estructurar e inicializar la aplicación de forma modular.
*/
@@ -138,7 +138,7 @@ pub type Weight = i8;
// API *********************************************************************************************
-// Macros y funciones útiles.
+// Funciones y macros útiles.
pub mod util;
// Carga las opciones de configuración.
pub mod config;
diff --git a/src/locale.rs b/src/locale.rs
index 2bf0da9..f23f51e 100644
--- a/src/locale.rs
+++ b/src/locale.rs
@@ -1,6 +1,6 @@
//! Localización (L10n).
//!
-//! PageTop utiliza las especificaciones de [Fluent](https://www.projectfluent.org/) para la
+//! `PageTop` utiliza las especificaciones de [Fluent](https://www.projectfluent.org/) para la
//! localización de aplicaciones, y aprovecha [fluent-templates](https://docs.rs/fluent-templates/)
//! para integrar los recursos de traducción directamente en el binario de la aplicación.
//!
@@ -13,7 +13,7 @@
//!
//! # Recursos Fluent
//!
-//! Por defecto, las traducciones están en el directorio `src/locale`, con subdirectorios para cada
+//! Por defecto las traducciones están en el directorio `src/locale`, con subdirectorios para cada
//! [Identificador de Idioma Unicode](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
//! válido. Podríamos tener una estructura como esta:
//!
@@ -34,7 +34,7 @@
//! └── main.ftl
//! ```
//!
-//! Ejemplo de un archivo en `src/locale/en-US/main.ftl`:
+//! Ejemplo de un archivo en `src/locale/en-US/main.ftl`
//!
//! ```text
//! hello-world = Hello world!
@@ -53,7 +53,7 @@
//! Y su archivo equivalente para español en `src/locale/es-ES/main.ftl`:
//!
//! ```text
-//! hello-world = ¡Hola, mundo!
+//! hello-world = Hola mundo!
//! hello-user = ¡Hola, {$userName}!
//! shared-photos =
//! {$userName} {$photoCount ->
@@ -81,13 +81,13 @@
//! include_locales!(LOCALES_SAMPLE);
//! ```
//!
-//! Si están ubicados en otro directorio, se puede usar la forma:
+//! Si están ubicados en otro directorio se puede usar la forma:
//!
//! ```rust,ignore
//! include_locales!(LOCALES_SAMPLE from "ruta/a/las/traducciones");
//! ```
//!
-//! Y *voilà*, sólo queda operar con los idiomas soportados por PageTop usando [`LangMatch`] y
+//! Y *voilà*, sólo queda operar con los idiomas soportados por `PageTop` usando [`LangMatch`] y
//! traducir textos con [`L10n`].
use crate::html::{Markup, PreEscaped};
@@ -129,7 +129,7 @@ pub(crate) static FALLBACK_LANGID: LazyLock =
// Identificador de idioma **por defecto** para la aplicación.
//
// Se resuelve a partir de [`global::SETTINGS.app.language`](global::SETTINGS). Si el identificador
-// de idioma no es válido o no está disponible, se usa [`FALLBACK_LANGID`].
+// de idioma no es válido o no está disponible entonces resuelve como [`FALLBACK_LANGID`].
pub(crate) static DEFAULT_LANGID: LazyLock