Compare commits
35 commits
bf2c298d18
...
36e2d9bec8
Author | SHA1 | Date | |
---|---|---|---|
36e2d9bec8 | |||
aae6c7df15 | |||
accab251d8 | |||
940e6aaf18 | |||
e3ca6079ff | |||
ddf78c2de8 | |||
2a4d6a7890 | |||
3ba71dbe45 | |||
824024b96d | |||
0127d17459 | |||
1af3776a50 | |||
f182eb3178 | |||
3bf058b8a5 | |||
8274519405 | |||
fe3bbcb131 | |||
0e4f10237d | |||
19c16d962f | |||
71c7793131 | |||
d7fcd6ccc4 | |||
d43b699a32 | |||
c2a8a58057 | |||
d4d55146d3 | |||
4311e9f335 | |||
0c1b12aacd | |||
c6c8c66a97 | |||
0496b9dc5d | |||
282b903eaf | |||
7ebd7b0e49 | |||
bb34ba5887 | |||
75eec8bebc | |||
512a406ede | |||
3e76c656eb | |||
8de3f0b7a5 | |||
97581659bf | |||
8b06b1752a |
13
CREDITS.md
|
@ -1,8 +1,7 @@
|
||||||
# 🔃 Dependencias
|
# 🔃 Dependencias
|
||||||
|
|
||||||
`PageTop` está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes
|
PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
|
||||||
aprovechando algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs)
|
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs) como son:
|
||||||
como son:
|
|
||||||
|
|
||||||
* [Actix Web](https://actix.rs/) para los servicios web.
|
* [Actix Web](https://actix.rs/) para los servicios web.
|
||||||
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
|
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
|
||||||
|
@ -11,14 +10,14 @@ como son:
|
||||||
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
|
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
|
||||||
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
|
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
|
||||||
* Además de otros *crates* adicionales que se pueden explorar en los archivos `Cargo.toml` de
|
* 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
|
# 🗚 FIGfonts
|
||||||
|
|
||||||
`PageTop` usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por
|
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
|
||||||
*yuanbohan* para mostrar un banner de presentación en el terminal con el nombre de la aplicación en
|
para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
|
||||||
caracteres [FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
|
[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*
|
* [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*
|
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) de *Glenn Chappell*
|
||||||
|
|
7
Cargo.lock
generated
|
@ -1307,6 +1307,12 @@ dependencies = [
|
||||||
"hashbrown 0.15.4",
|
"hashbrown 0.15.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
@ -1568,6 +1574,7 @@ dependencies = [
|
||||||
"config",
|
"config",
|
||||||
"figlet-rs",
|
"figlet-rs",
|
||||||
"fluent-templates",
|
"fluent-templates",
|
||||||
|
"indoc",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pagetop-build",
|
"pagetop-build",
|
||||||
"pagetop-macros",
|
"pagetop-macros",
|
||||||
|
|
|
@ -20,6 +20,7 @@ colored = "3.0.0"
|
||||||
concat-string = "1.0.1"
|
concat-string = "1.0.1"
|
||||||
config = { version = "0.15.13", default-features = false, features = ["toml"] }
|
config = { version = "0.15.13", default-features = false, features = ["toml"] }
|
||||||
figlet-rs = "0.1.5"
|
figlet-rs = "0.1.5"
|
||||||
|
indoc = "2.0.6"
|
||||||
itoa = "1.0.15"
|
itoa = "1.0.15"
|
||||||
parking_lot = "0.12.4"
|
parking_lot = "0.12.4"
|
||||||
paste = { package = "pastey", version = "0.1.0" }
|
paste = { package = "pastey", version = "0.1.0" }
|
||||||
|
|
28
README.md
|
@ -14,8 +14,8 @@
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
`PageTop` reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para
|
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
||||||
la creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
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
|
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
|
||||||
según las necesidades de cada proyecto, incluyendo:
|
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,
|
* **Componentes** (*components*): encapsulan HTML, CSS y JavaScript en unidades funcionales,
|
||||||
configurables y reutilizables.
|
configurables y reutilizables.
|
||||||
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
|
* **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
|
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
||||||
componentes sin comprometer su funcionalidad.
|
componentes sin comprometer su funcionalidad.
|
||||||
|
|
||||||
|
|
||||||
# ⚡️ Guía rápida
|
# ⚡️ 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
|
```rust,no_run
|
||||||
use pagetop::prelude::*;
|
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
|
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
|
||||||
de bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
|
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
|
```rust,no_run
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
@ -59,8 +59,8 @@ impl Extension for HelloWorld {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
Page::new(Some(request))
|
Page::new(request)
|
||||||
.with_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
|
.add_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
|
||||||
.render()
|
.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)**,
|
* **[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
|
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
|
PageTop para servirlos de forma eficiente, con detección de cambios que optimizan el tiempo de
|
||||||
de compilación.
|
compilación.
|
||||||
|
|
||||||
* **[pagetop-build](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build)**,
|
* **[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
|
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)**,
|
* **[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
|
# 🧪 Pruebas
|
||||||
|
@ -116,7 +116,7 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
|
||||||
|
|
||||||
# 🚧 Advertencia
|
# 🚧 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
|
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**.
|
hasta que se libere la versión **1.0.0**.
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,8 @@ async fn hello_name(
|
||||||
path: service::web::Path<String>,
|
path: service::web::Path<String>,
|
||||||
) -> ResultPage<Markup, ErrorPage> {
|
) -> ResultPage<Markup, ErrorPage> {
|
||||||
let name = path.into_inner();
|
let name = path.into_inner();
|
||||||
Page::new(Some(request))
|
Page::new(request)
|
||||||
.with_component(Html::with(move |_| html! { h1 { "Hello " (name) "!" } }))
|
.add_component(Html::with(move |_| html! { h1 { "Hello " (name) "!" } }))
|
||||||
.render()
|
.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ impl Extension for HelloWorld {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
Page::new(Some(request))
|
Page::new(request)
|
||||||
.with_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
|
.add_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
|
||||||
.render()
|
.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,7 @@ impl Extension for MyExtension {
|
||||||
|
|
||||||
# 🚧 Advertencia
|
# 🚧 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
|
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**.
|
hasta que se libere la versión **1.0.0**.
|
||||||
|
|
||||||
|
|
|
@ -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
|
[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
|
[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
|
necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de
|
||||||
cada proyecto `PageTop`.
|
cada proyecto PageTop.
|
||||||
|
|
||||||
|
|
||||||
# 🚧 Advertencia
|
# 🚧 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
|
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**.
|
hasta que se libere la versión **1.0.0**.
|
||||||
|
|
||||||
|
|
|
@ -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
|
[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
|
[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
|
necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de
|
||||||
cada proyecto `PageTop`.
|
cada proyecto PageTop.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#).
|
/// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)).
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
|
@ -107,119 +107,221 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
|
||||||
/// `alter_...()`, que permitirá más adelante modificar instancias existentes.
|
/// `alter_...()`, que permitirá más adelante modificar instancias existentes.
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
let fn_with = parse_macro_input!(item as ItemFn);
|
use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type};
|
||||||
let fn_with_name = fn_with.sig.ident.clone();
|
|
||||||
let fn_with_name_str = fn_with.sig.ident.to_string();
|
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::<ImplItemFn>(ts.clone()) {
|
||||||
|
Kind::Impl(it)
|
||||||
|
} else if let Ok(tt) = parse2::<TraitItemFn>(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();
|
||||||
|
|
||||||
// Valida el nombre del método.
|
// Valida el nombre del método.
|
||||||
if !fn_with_name_str.starts_with("with_") {
|
if !with_name_str.starts_with("with_") {
|
||||||
let expanded = quote_spanned! {
|
return quote_spanned! {
|
||||||
fn_with.sig.ident.span() =>
|
sig.ident.span() => compile_error!("expected a named `with_...()` method");
|
||||||
compile_error!("expected a \"pub fn with_...(mut self, ...) -> Self\" 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! {},
|
||||||
};
|
};
|
||||||
return expanded.into();
|
|
||||||
}
|
// Validaciones comunes.
|
||||||
// Valida que el método es público.
|
if sig.asyncness.is_some() {
|
||||||
if !matches!(fn_with.vis, syn::Visibility::Public(_)) {
|
|
||||||
return quote_spanned! {
|
return quote_spanned! {
|
||||||
fn_with.sig.ident.span() => compile_error!("expected method to be `pub`");
|
sig.asyncness.span() => compile_error!("`with_...()` cannot be `async`");
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
}
|
}
|
||||||
// Valida que el primer argumento es exactamente `mut self`.
|
if sig.constness.is_some() {
|
||||||
if let Some(syn::FnArg::Receiver(receiver)) = fn_with.sig.inputs.first() {
|
|
||||||
if receiver.mutability.is_none() || receiver.reference.is_some() {
|
|
||||||
return quote_spanned! {
|
return quote_spanned! {
|
||||||
receiver.span() => compile_error!("expected `mut self` as the first argument");
|
sig.constness.span() => compile_error!("`with_...()` cannot be `const`");
|
||||||
}
|
}
|
||||||
.into();
|
.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 {
|
} 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`"
|
||||||
|
};
|
||||||
|
let err = sig
|
||||||
|
.inputs
|
||||||
|
.first()
|
||||||
|
.map(|a| a.span())
|
||||||
|
.unwrap_or(sig.ident.span());
|
||||||
return quote_spanned! {
|
return quote_spanned! {
|
||||||
fn_with.sig.ident.span() => compile_error!("expected `mut self` as the first argument");
|
err => compile_error!(#msg);
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valida que el método devuelve exactamente `Self`.
|
// Valida que el método devuelve exactamente `Self`.
|
||||||
if let syn::ReturnType::Type(_, ty) = &fn_with.sig.output {
|
match &sig.output {
|
||||||
if let syn::Type::Path(type_path) = ty.as_ref() {
|
ReturnType::Type(_, ty) => match ty.as_ref() {
|
||||||
if type_path.qself.is_some() || !type_path.path.is_ident("Self") {
|
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();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return quote_spanned! { ty.span() =>
|
|
||||||
compile_error!("expected return type to be exactly `Self`");
|
|
||||||
}
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return quote_spanned! {
|
return quote_spanned! {
|
||||||
fn_with.sig.output.span() => compile_error!("expected method to return `Self`");
|
ty.span() => compile_error!("expected return type to be exactly `Self`");
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return quote_spanned! {
|
||||||
|
sig.output.span() => compile_error!("expected return type to be exactly `Self`");
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Genera el nombre del método alter_...().
|
// Genera el nombre del método alter_...().
|
||||||
let fn_alter_name_str = fn_with_name_str.replace("with_", "alter_");
|
let stem = with_name_str.strip_prefix("with_").expect("validated");
|
||||||
let fn_alter_name = syn::Ident::new(&fn_alter_name_str, fn_with.sig.ident.span());
|
let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span());
|
||||||
|
|
||||||
// Extrae genéricos y cláusulas where.
|
// Extrae genéricos y cláusulas where.
|
||||||
let fn_generics = &fn_with.sig.generics;
|
let generics = &sig.generics;
|
||||||
let where_clause = &fn_with.sig.generics.where_clause;
|
let where_clause = &sig.generics.where_clause;
|
||||||
|
|
||||||
// Extrae argumentos y parámetros de llamada.
|
// Extrae identificadores de los argumentos para la llamada (sin `mut` ni patrones complejos).
|
||||||
let args: Vec<_> = fn_with.sig.inputs.iter().skip(1).collect();
|
let args: Vec<_> = sig.inputs.iter().skip(1).collect();
|
||||||
let params: Vec<_> = fn_with
|
let call_idents: Vec<Ident> = {
|
||||||
.sig
|
let mut v = Vec::new();
|
||||||
.inputs
|
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
|
||||||
.iter()
|
.iter()
|
||||||
.skip(1)
|
.filter(|&a| !a.path().is_ident("doc"))
|
||||||
.map(|arg| match arg {
|
.cloned()
|
||||||
syn::FnArg::Typed(pat) => &pat.pat,
|
|
||||||
_ => panic!("unexpected argument type"),
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Extrae bloque del método.
|
// Documentación del método alter_...().
|
||||||
let fn_with_block = &fn_with.block;
|
let alter_doc =
|
||||||
|
format!("Equivalente a [`Self::{with_name_str}()`], pero fuera del patrón *builder*.");
|
||||||
// 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.
|
// Genera el código final.
|
||||||
let expanded = quote! {
|
let expanded = match body_opt {
|
||||||
#fn_with
|
None => {
|
||||||
#[inline]
|
quote! {
|
||||||
#fn_alter
|
#(#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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
expanded.into()
|
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
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
@ -240,7 +342,7 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Define funciones de prueba asíncronas para usar con `PageTop`.
|
/// Define funciones de prueba asíncronas para usar con PageTop.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
|
@ -16,7 +16,7 @@ configurables, basadas en HTML, CSS y JavaScript.
|
||||||
|
|
||||||
## Descripción general
|
## 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
|
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
|
||||||
compilación.
|
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
|
[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).
|
[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`.
|
`static-files` manualmente como dependencia en su `Cargo.toml`.
|
||||||
|
|
||||||
|
|
||||||
# 🚧 Advertencia
|
# 🚧 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
|
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**.
|
hasta que se libere la versión **1.0.0**.
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ configurables, basadas en HTML, CSS y JavaScript.
|
||||||
|
|
||||||
## Descripción general
|
## 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
|
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
|
||||||
compilación.
|
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
|
[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).
|
[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`.
|
`static-files` manualmente como dependencia en su `Cargo.toml`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
17
src/app.rs
|
@ -1,8 +1,11 @@
|
||||||
//! Prepara y ejecuta una aplicación creada con `Pagetop`.
|
//! Prepara y ejecuta una aplicación creada con PageTop.
|
||||||
|
|
||||||
mod figfont;
|
mod figfont;
|
||||||
|
|
||||||
use crate::core::{extension, extension::ExtensionRef};
|
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 crate::{global, locale, service, trace};
|
||||||
|
|
||||||
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
|
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
|
||||||
|
@ -14,7 +17,7 @@ use substring::Substring;
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
use std::sync::LazyLock;
|
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
|
/// 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).
|
/// instanciarla se puede usar [`new()`](Application::new) o [`prepare()`](Application::prepare).
|
||||||
|
@ -81,7 +84,7 @@ impl Application {
|
||||||
if let Some((Width(term_width), _)) = terminal_size() {
|
if let Some((Width(term_width), _)) = terminal_size() {
|
||||||
if term_width >= 80 {
|
if term_width >= 80 {
|
||||||
let maxlen: usize = ((term_width / 10) - 2).into();
|
let maxlen: usize = ((term_width / 10) - 2).into();
|
||||||
let mut app = app_name.substring(0, maxlen).to_owned();
|
let mut app = app_name.substring(0, maxlen).to_string();
|
||||||
if app_name.len() > maxlen {
|
if app_name.len() > maxlen {
|
||||||
app = format!("{app}...");
|
app = format!("{app}...");
|
||||||
}
|
}
|
||||||
|
@ -170,6 +173,12 @@ impl Application {
|
||||||
InitError = (),
|
InitError = (),
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
service::App::new().configure(extension::all::configure_services)
|
service::App::new()
|
||||||
|
.configure(extension::all::configure_services)
|
||||||
|
.default_service(service::web::route().to(service_not_found))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn service_not_found(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
|
Err(ErrorPage::NotFound(request))
|
||||||
|
}
|
||||||
|
|
|
@ -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::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::base::action::FnActionWithComponent;
|
||||||
pub struct AfterRender<C: Component> {
|
pub struct AfterRender<C: Component> {
|
||||||
f: FnActionWithComponent<C>,
|
f: FnActionWithComponent<C>,
|
||||||
referer_type_id: Option<UniqueId>,
|
referer_type_id: Option<UniqueId>,
|
||||||
referer_id: OptionId,
|
referer_id: AttrId,
|
||||||
weight: Weight,
|
weight: Weight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ impl<C: Component> AfterRender<C> {
|
||||||
AfterRender {
|
AfterRender {
|
||||||
f,
|
f,
|
||||||
referer_type_id: Some(UniqueId::of::<C>()),
|
referer_type_id: Some(UniqueId::of::<C>()),
|
||||||
referer_id: OptionId::default(),
|
referer_id: AttrId::default(),
|
||||||
weight: 0,
|
weight: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::base::action::FnActionWithComponent;
|
||||||
pub struct BeforeRender<C: Component> {
|
pub struct BeforeRender<C: Component> {
|
||||||
f: FnActionWithComponent<C>,
|
f: FnActionWithComponent<C>,
|
||||||
referer_type_id: Option<UniqueId>,
|
referer_type_id: Option<UniqueId>,
|
||||||
referer_id: OptionId,
|
referer_id: AttrId,
|
||||||
weight: Weight,
|
weight: Weight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ impl<C: Component> BeforeRender<C> {
|
||||||
BeforeRender {
|
BeforeRender {
|
||||||
f,
|
f,
|
||||||
referer_type_id: Some(UniqueId::of::<C>()),
|
referer_type_id: Some(UniqueId::of::<C>()),
|
||||||
referer_id: OptionId::default(),
|
referer_id: AttrId::default(),
|
||||||
weight: 0,
|
weight: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ pub type FnIsRenderable<C> = fn(component: &C, cx: &Context) -> bool;
|
||||||
pub struct IsRenderable<C: Component> {
|
pub struct IsRenderable<C: Component> {
|
||||||
f: FnIsRenderable<C>,
|
f: FnIsRenderable<C>,
|
||||||
referer_type_id: Option<UniqueId>,
|
referer_type_id: Option<UniqueId>,
|
||||||
referer_id: OptionId,
|
referer_id: AttrId,
|
||||||
weight: Weight,
|
weight: Weight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ impl<C: Component> IsRenderable<C> {
|
||||||
IsRenderable {
|
IsRenderable {
|
||||||
f,
|
f,
|
||||||
referer_type_id: Some(UniqueId::of::<C>()),
|
referer_type_id: Some(UniqueId::of::<C>()),
|
||||||
referer_id: OptionId::default(),
|
referer_id: AttrId::default(),
|
||||||
weight: 0,
|
weight: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
//! Componentes nativos proporcionados por `PageTop`.
|
//! Componentes nativos proporcionados por PageTop.
|
||||||
|
|
||||||
mod html;
|
mod html;
|
||||||
pub use html::Html;
|
pub use html::Html;
|
||||||
|
|
||||||
|
mod block;
|
||||||
|
pub use block::Block;
|
||||||
|
|
||||||
|
mod poweredby;
|
||||||
|
pub use poweredby::PoweredBy;
|
||||||
|
|
103
src/base/component/block.rs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
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<String> {
|
||||||
|
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::<Block>(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<str>) -> 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<str>) -> 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ use crate::prelude::*;
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// let component = Html::with(|cx| {
|
/// let component = Html::with(|cx| {
|
||||||
/// let user = cx.get_param::<String>("username").unwrap_or(String::from("visitor"));
|
/// let user = cx.param::<String>("username").cloned().unwrap_or("visitor".to_string());
|
||||||
/// html! {
|
/// html! {
|
||||||
/// h1 { "Hello, " (user) }
|
/// h1 { "Hello, " (user) }
|
||||||
/// }
|
/// }
|
||||||
|
@ -44,11 +44,13 @@ impl Component for Html {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
PrepareMarkup::With((self.0)(cx))
|
PrepareMarkup::With(self.html(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Html {
|
impl Html {
|
||||||
|
// Html BUILDER ********************************************************************************
|
||||||
|
|
||||||
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
|
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
|
||||||
///
|
///
|
||||||
/// El método [`prepare_component()`](crate::core::component::Component::prepare_component)
|
/// El método [`prepare_component()`](crate::core::component::Component::prepare_component)
|
||||||
|
@ -66,11 +68,24 @@ impl Html {
|
||||||
/// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando
|
/// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando
|
||||||
/// [`prepare_component()`](crate::core::component::Component::prepare_component) invoque esta
|
/// [`prepare_component()`](crate::core::component::Component::prepare_component) invoque esta
|
||||||
/// instancia. La nueva función también recibe una referencia al contexto ([`Context`]).
|
/// instancia. La nueva función también recibe una referencia al contexto ([`Context`]).
|
||||||
pub fn alter_html<F>(&mut self, f: F) -> &mut Self
|
#[builder_fn]
|
||||||
|
pub fn with_fn<F>(mut self, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
|
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
self.0 = Box::new(f);
|
self.0 = Box::new(f);
|
||||||
self
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
67
src/base/component/poweredby.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
// Enlace a la página oficial de PageTop.
|
||||||
|
const LINK: &str = "<a href=\"https://pagetop.cillero.es\" rel=\"noreferrer\">PageTop</a>";
|
||||||
|
|
||||||
|
/// 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<String>);
|
||||||
|
/// ```
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_copyright(mut self, copyright: Option<impl Into<String>>) -> 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
//! Extensiones para funcionalidades avanzadas de `PageTop`.
|
//! Extensiones para funcionalidades avanzadas de PageTop.
|
||||||
|
|
||||||
mod welcome;
|
mod welcome;
|
||||||
pub use welcome::Welcome;
|
pub use welcome::Welcome;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::prelude::*;
|
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
|
/// 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á
|
/// configurado ninguna página de inicio personalizada. Permite confirmar que el servidor está
|
||||||
|
@ -24,93 +24,32 @@ impl Extension for Welcome {
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
let app = &global::SETTINGS.app.name;
|
let app = &global::SETTINGS.app.name;
|
||||||
|
|
||||||
Page::new(Some(request))
|
Page::new(request)
|
||||||
.with_title(L10n::l("welcome_page"))
|
|
||||||
.with_theme("Basic")
|
.with_theme("Basic")
|
||||||
.with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css")))
|
.with_layout("PageTopIntro")
|
||||||
.with_component(Html::with(move |cx| html! {
|
.with_title(L10n::l("welcome_title"))
|
||||||
div id="main-header" {
|
.with_description(L10n::l("welcome_intro").with_arg("app", app))
|
||||||
header {
|
.with_param("intro_button_txt", L10n::l("welcome_powered"))
|
||||||
h1
|
.with_param("intro_button_lnk", "https://pagetop.cillero.es".to_string())
|
||||||
id="header-title"
|
.add_component(
|
||||||
aria-label=(L10n::l("welcome_aria").with_arg("app", app).to_markup(cx))
|
Block::new()
|
||||||
{
|
.with_title(L10n::l("welcome_status_title"))
|
||||||
span { (L10n::l("welcome_title").to_markup(cx)) }
|
.add_component(Html::with(move |cx| {
|
||||||
(L10n::l("welcome_intro").with_arg("app", app).to_markup(cx))
|
html! {
|
||||||
|
p { (L10n::l("welcome_status_1").using(cx)) }
|
||||||
|
p { (L10n::l("welcome_status_2").using(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()
|
.render()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! Temas básicos soportados por `PageTop`.
|
//! Temas básicos soportados por PageTop.
|
||||||
|
|
||||||
mod basic;
|
mod basic;
|
||||||
pub use basic::Basic;
|
pub use basic::Basic;
|
||||||
|
|
|
@ -1,8 +1,33 @@
|
||||||
//! Es el tema básico que incluye `PageTop` por defecto.
|
/// Es el tema básico que incluye PageTop por defecto.
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
/// Tema básico por defecto.
|
/// 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<String>`) – 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;
|
pub struct Basic;
|
||||||
|
|
||||||
impl Extension for Basic {
|
impl Extension for Basic {
|
||||||
|
@ -12,11 +37,152 @@ impl Extension for Basic {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme 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),
|
||||||
|
_ => <Self as ThemePage>::render_body(self, page, self.page_regions()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn after_render_page_body(&self, page: &mut Page) {
|
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(
|
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/css/normalize.css")
|
StyleSheet::from("/css/normalize.css")
|
||||||
.with_version("8.0.1")
|
.with_version("8.0.1")
|
||||||
.with_weight(-99),
|
.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)
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
//! Estos ajustes se obtienen de archivos [TOML](https://toml.io) como pares `clave = valor` que se
|
//! Estos ajustes se obtienen de archivos [TOML](https://toml.io) como pares `clave = valor` que se
|
||||||
//! mapean a estructuras **fuertemente tipadas** y valores predefinidos.
|
//! 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
|
//! **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.
|
//! 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
|
//! 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.
|
//! 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
|
//! 1. **config/common.toml**, para ajustes comunes a todos los entornos. Este enfoque simplifica el
|
||||||
//! mantenimiento al centralizar los valores de configuración comunes.
|
//! 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`:
|
//! 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.
|
//! cargar *config/default.toml* si el archivo existe.
|
||||||
//!
|
//!
|
||||||
//! * Útil para definir configuraciones específicas por entorno, garantizando que cada uno (p.ej.
|
//! * Útil para definir configuraciones específicas por entorno, garantizando que cada uno (p.ej.
|
||||||
|
|
|
@ -117,7 +117,7 @@ impl TypeInfo {
|
||||||
///
|
///
|
||||||
/// Este *trait* se implementa automáticamente para **todos** los tipos que implementen [`Any`], de
|
/// 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
|
/// 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
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
|
@ -7,3 +7,6 @@ mod children;
|
||||||
pub use children::Children;
|
pub use children::Children;
|
||||||
pub use children::{Child, ChildOp};
|
pub use children::{Child, ChildOp};
|
||||||
pub use children::{Typed, TypedOp};
|
pub use children::{Typed, TypedOp};
|
||||||
|
|
||||||
|
mod slot;
|
||||||
|
pub use slot::TypedSlot;
|
||||||
|
|
|
@ -9,13 +9,13 @@ use std::vec::IntoIter;
|
||||||
|
|
||||||
/// Representa un componente encapsulado de forma segura y compartida.
|
/// Representa un componente encapsulado de forma segura y compartida.
|
||||||
///
|
///
|
||||||
/// Esta estructura permite manipular y renderizar cualquier tipo que implemente [`Component`],
|
/// Esta estructura permite manipular y renderizar un componente que implemente [`Component`], y
|
||||||
/// garantizando acceso concurrente a través de [`Arc<RwLock<_>>`].
|
/// habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Child(Arc<RwLock<dyn Component>>);
|
pub struct Child(Arc<RwLock<dyn Component>>);
|
||||||
|
|
||||||
impl Child {
|
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 {
|
pub fn with(component: impl Component) -> Self {
|
||||||
Child(Arc::new(RwLock::new(component)))
|
Child(Arc::new(RwLock::new(component)))
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,8 @@ impl Child {
|
||||||
|
|
||||||
/// Variante tipada de [`Child`] para evitar conversiones durante el uso.
|
/// Variante tipada de [`Child`] para evitar conversiones durante el uso.
|
||||||
///
|
///
|
||||||
/// Facilita el acceso a componentes del mismo tipo sin necesidad de hacer `downcast`.
|
/// Esta estructura permite manipular y renderizar un componente concreto que implemente
|
||||||
|
/// [`Component`], y habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
|
||||||
pub struct Typed<C: Component>(Arc<RwLock<C>>);
|
pub struct Typed<C: Component>(Arc<RwLock<C>>);
|
||||||
|
|
||||||
impl<C: Component> Clone for Typed<C> {
|
impl<C: Component> Clone for Typed<C> {
|
||||||
|
@ -56,7 +57,7 @@ impl<C: Component> Clone for Typed<C> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Component> Typed<C> {
|
impl<C: Component> Typed<C> {
|
||||||
/// Crea un nuevo [`Typed`] a partir de un componente.
|
/// Crea un nuevo `Typed` a partir de un componente.
|
||||||
pub fn with(component: C) -> Self {
|
pub fn with(component: C) -> Self {
|
||||||
Typed(Arc::new(RwLock::new(component)))
|
Typed(Arc::new(RwLock::new(component)))
|
||||||
}
|
}
|
||||||
|
@ -284,7 +285,7 @@ impl IntoIterator for Children {
|
||||||
///
|
///
|
||||||
/// # Ejemplo de uso:
|
/// # Ejemplo de uso:
|
||||||
///
|
///
|
||||||
/// ```rust#ignore
|
/// ```rust,ignore
|
||||||
/// let children = Children::new().with(child1).with(child2);
|
/// let children = Children::new().with(child1).with(child2);
|
||||||
/// for child in children {
|
/// for child in children {
|
||||||
/// println!("{:?}", child.id());
|
/// println!("{:?}", child.id());
|
||||||
|
@ -303,7 +304,7 @@ impl<'a> IntoIterator for &'a Children {
|
||||||
///
|
///
|
||||||
/// # Ejemplo de uso:
|
/// # Ejemplo de uso:
|
||||||
///
|
///
|
||||||
/// ```rust#ignore
|
/// ```rust,ignore
|
||||||
/// let children = Children::new().with(child1).with(child2);
|
/// let children = Children::new().with(child1).with(child2);
|
||||||
/// for child in &children {
|
/// for child in &children {
|
||||||
/// println!("{:?}", child.id());
|
/// println!("{:?}", child.id());
|
||||||
|
@ -322,7 +323,7 @@ impl<'a> IntoIterator for &'a mut Children {
|
||||||
///
|
///
|
||||||
/// # Ejemplo de uso:
|
/// # Ejemplo de uso:
|
||||||
///
|
///
|
||||||
/// ```rust#ignore
|
/// ```rust,ignore
|
||||||
/// let mut children = Children::new().with(child1).with(child2);
|
/// let mut children = Children::new().with(child1).with(child2);
|
||||||
/// for child in &mut children {
|
/// for child in &mut children {
|
||||||
/// child.render(&mut context);
|
/// child.render(&mut context);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
use crate::core::{AnyInfo, TypeInfo};
|
use crate::core::{AnyInfo, TypeInfo};
|
||||||
use crate::html::{html, Context, Markup, PrepareMarkup, Render};
|
use crate::html::{html, Context, Markup, PrepareMarkup};
|
||||||
|
|
||||||
/// Define la función de renderizado para todos los componentes.
|
/// 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;
|
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
|
/// 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
|
/// 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::<Self>()
|
TypeInfo::ShortName.of::<Self>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve una descripción opcional del componente.
|
/// Devuelve una descripción del componente, si existe.
|
||||||
///
|
///
|
||||||
/// Por defecto, no se proporciona ninguna descripción (`None`).
|
/// Por defecto, no se proporciona ninguna descripción (`None`).
|
||||||
fn description(&self) -> Option<String> {
|
fn description(&self) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve un identificador opcional para el componente.
|
/// Devuelve el identificador del componente, si existe.
|
||||||
///
|
///
|
||||||
/// Este identificador puede usarse para referenciar el componente en el HTML. Por defecto, no
|
/// Este identificador puede usarse para referenciar el componente en el HTML. Por defecto, no
|
||||||
/// tiene ningún identificador (`None`).
|
/// tiene ningún identificador (`None`).
|
||||||
|
@ -51,12 +51,17 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn setup_before_prepare(&mut self, cx: &mut Context) {}
|
fn setup_before_prepare(&mut self, cx: &mut Context) {}
|
||||||
|
|
||||||
/// Devuelve una representación estructurada del componente lista para renderizar.
|
/// Devuelve una representación renderizada del componente.
|
||||||
///
|
///
|
||||||
/// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente
|
/// 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
|
/// durante el proceso de construcción del documento. Puede sobrescribirse para generar
|
||||||
/// dinámicamente el contenido HTML con acceso al contexto de renderizado.
|
/// 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`].
|
/// Por defecto, devuelve [`PrepareMarkup::None`].
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
|
64
src/core/component/slot.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
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<Typed<C>>`, 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<C: Component>(Option<Typed<C>>);
|
||||||
|
|
||||||
|
impl<C: Component> Default for TypedSlot<C> {
|
||||||
|
fn default() -> Self {
|
||||||
|
TypedSlot(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component> TypedSlot<C> {
|
||||||
|
/// 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<C>) -> Self {
|
||||||
|
self.0 = component.map(Typed::with);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypedSlot GETTERS *********************************************************************
|
||||||
|
|
||||||
|
/// Devuelve un clon (incrementa el contador `Arc`) de [`Typed<C>`], si existe.
|
||||||
|
pub fn get(&self) -> Option<Typed<C>> {
|
||||||
|
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! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
//! API para añadir nuevas funcionalidades usando extensiones.
|
//! 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`].
|
//! como una **extensión**. Todas comparten la misma interfaz declarada en [`Extension`].
|
||||||
|
|
||||||
mod definition;
|
mod definition;
|
||||||
|
|
|
@ -10,7 +10,7 @@ use crate::{actions_boxed, service};
|
||||||
/// cualquier hilo de la ejecución sin necesidad de sincronización adicional.
|
/// cualquier hilo de la ejecución sin necesidad de sincronización adicional.
|
||||||
pub type ExtensionRef = &'static dyn Extension;
|
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
|
/// 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.
|
/// 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 {
|
pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// Nombre legible para el usuario.
|
/// Nombre localizado de la extensión legible para el usuario.
|
||||||
///
|
///
|
||||||
/// Predeterminado por el [`short_name()`](AnyInfo::short_name) del tipo asociado a la
|
/// Predeterminado por el [`short_name()`](AnyInfo::short_name) del tipo asociado a la
|
||||||
/// extensión.
|
/// extensión.
|
||||||
|
@ -34,18 +34,15 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
L10n::n(self.short_name())
|
L10n::n(self.short_name())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Descripción corta para paneles, listados, etc.
|
/// Descripción corta localizada de la extensión para paneles, listados, etc.
|
||||||
fn description(&self) -> L10n {
|
fn description(&self) -> L10n {
|
||||||
L10n::default()
|
L10n::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Los temas son extensiones que implementan [`Extension`] y también
|
/// Devuelve una referencia a esta misma extensión cuando se trata de un tema.
|
||||||
/// [`Theme`](crate::core::theme::Theme).
|
|
||||||
///
|
///
|
||||||
/// Si la extensión no es un tema, este método devuelve `None` por defecto.
|
/// 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.
|
||||||
/// En caso contrario, este método debe implementarse para devolver una referencia de sí mismo
|
|
||||||
/// como tema. Por ejemplo:
|
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
|
@ -66,7 +63,7 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
|
|
||||||
/// Otras extensiones que deben habilitarse **antes** de esta.
|
/// 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.
|
/// aplicación.
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![]
|
vec![]
|
||||||
|
@ -81,7 +78,7 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
actions_boxed![]
|
actions_boxed![]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inicializa la extensión durante la lógica de arranque de la aplicación.
|
/// Inicializa la extensión durante la fase de arranque de la aplicación.
|
||||||
///
|
///
|
||||||
/// Se llama una sola vez, después de que todas las dependencias se han inicializado y antes de
|
/// Se llama una sola vez, después de que todas las dependencias se han inicializado y antes de
|
||||||
/// aceptar cualquier petición HTTP.
|
/// aceptar cualquier petición HTTP.
|
||||||
|
@ -104,8 +101,8 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
|
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
|
||||||
|
|
||||||
/// Permite crear extensiones para deshabilitar y desinstalar los recursos de otras extensiones
|
/// Permite crear extensiones para deshabilitar y desinstalar recursos de otras de versiones
|
||||||
/// utilizadas en versiones anteriores de la aplicación.
|
/// anteriores de la aplicación.
|
||||||
///
|
///
|
||||||
/// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones.
|
/// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones.
|
||||||
fn drop_extensions(&self) -> Vec<ExtensionRef> {
|
fn drop_extensions(&self) -> Vec<ExtensionRef> {
|
||||||
|
|
|
@ -1,26 +1,24 @@
|
||||||
//! API para añadir y gestionar nuevos temas.
|
//! 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
|
//! especialmente las páginas de contenido ([`Page`](crate::response::page::Page)), sin alterar la
|
||||||
//! lógica interna de sus componentes.
|
//! lógica interna de sus componentes.
|
||||||
//!
|
//!
|
||||||
//! Un tema **declara las regiones** (*cabecera*, *barra lateral*, *pie*, etc.) que estarán
|
//! 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,
|
//! 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,
|
//! tipografías, espaciados y cualquier otro detalle visual o de comportamiento (como animaciones,
|
||||||
//! *scripts* de interfaz, etc.).
|
//! scripts de interfaz, etc.).
|
||||||
//!
|
//!
|
||||||
//! Es una extensión más (implementando [`Extension`](crate::core::extension::Extension)). Se
|
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension); por
|
||||||
//! instala, activa y declara dependencias igual que el resto de extensiones; y se señala a sí misma
|
//! lo que se instancian, declaran sus dependencias y se inician igual que el resto de extensiones;
|
||||||
//! como tema (implementando [`theme()`](crate::core::extension::Extension::theme) y [`Theme`]).
|
//! pero serán temas si además implementan [`theme()`](crate::core::extension::Extension::theme) y
|
||||||
|
//! [`Theme`].
|
||||||
|
|
||||||
mod definition;
|
mod definition;
|
||||||
pub use definition::{Theme, ThemeRef};
|
pub use definition::{Theme, ThemePage, ThemeRef};
|
||||||
|
|
||||||
mod regions;
|
mod regions;
|
||||||
pub(crate) use regions::ChildrenInRegions;
|
pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT};
|
||||||
pub use regions::InRegion;
|
pub use regions::{InRegion, Region};
|
||||||
|
|
||||||
pub(crate) mod all;
|
pub(crate) mod all;
|
||||||
|
|
||||||
/// Nombre de la región por defecto: `content`.
|
|
||||||
pub const CONTENT_REGION_NAME: &str = "content";
|
|
||||||
|
|
|
@ -1,52 +1,49 @@
|
||||||
use crate::core::extension::Extension;
|
use crate::core::extension::Extension;
|
||||||
use crate::core::theme::CONTENT_REGION_NAME;
|
use crate::core::theme::Region;
|
||||||
use crate::global;
|
use crate::global;
|
||||||
use crate::html::{html, Markup};
|
use crate::html::{html, Markup};
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
use crate::response::page::Page;
|
use crate::response::page::Page;
|
||||||
|
|
||||||
/// Representa una referencia a un tema.
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
/// Referencia estática a un tema.
|
||||||
///
|
///
|
||||||
/// Los temas son también extensiones. Por tanto se deben definir igual, es decir, como instancias
|
/// Los temas son también extensiones. Por tanto, deben declararse como **instancias estáticas** que
|
||||||
/// estáticas globales que implementan [`Theme`], pero también [`Extension`].
|
/// implementen [`Theme`] y, a su vez, [`Extension`].
|
||||||
pub type ThemeRef = &'static dyn Theme;
|
pub type ThemeRef = &'static dyn Theme;
|
||||||
|
|
||||||
/// Interfaz común que debe implementar cualquier tema de `PageTop`.
|
/// Métodos predefinidos de renderizado para las páginas de un tema.
|
||||||
///
|
///
|
||||||
/// Un tema implementará [`Theme`] y los métodos que sean necesarios de [`Extension`], aunque el
|
/// Contiene las implementaciones base de las **secciones** `<head>` y `<body>`. Se implementa
|
||||||
/// único obligatorio es [`theme()`](Extension::theme).
|
/// automáticamente para cualquier tipo que implemente [`Theme`], por lo que normalmente no requiere
|
||||||
|
/// implementación explícita.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// Si un tema **sobrescribe** [`render_page_head()`](Theme::render_page_head) o
|
||||||
/// use pagetop::prelude::*;
|
/// [`render_page_body()`](Theme::render_page_body), se puede volver al comportamiento por defecto
|
||||||
|
/// cuando se necesite usando FQS (*Fully Qualified Syntax*):
|
||||||
///
|
///
|
||||||
/// pub struct MyTheme;
|
/// - `<Self as ThemePage>::render_body(self, page, self.page_regions())`
|
||||||
///
|
/// - `<Self as ThemePage>::render_head(self, page)`
|
||||||
/// impl Extension for MyTheme {
|
pub trait ThemePage {
|
||||||
/// fn name(&self) -> L10n { L10n::n("My theme") }
|
/// Renderiza el contenido del `<body>` de la página.
|
||||||
/// fn description(&self) -> L10n { L10n::n("Un tema personal") }
|
///
|
||||||
///
|
/// Recorre `regions` en el **orden declarado** y, para cada región con contenido, genera un
|
||||||
/// fn theme(&self) -> Option<ThemeRef> {
|
/// contenedor con `role="region"` y un `aria-label` localizado. Se asume que cada identificador
|
||||||
/// Some(&Self)
|
/// de región es **único** dentro de la página.
|
||||||
/// }
|
fn render_body(&self, page: &mut Page, regions: &[(Region, L10n)]) -> Markup {
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// 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! {
|
html! {
|
||||||
body id=[page.body_id().get()] class=[page.body_classes().get()] {
|
body id=[page.body_id().get()] class=[page.body_classes().get()] {
|
||||||
@for (region_name, _) in self.regions() {
|
@for (region, region_label) in regions {
|
||||||
@let output = page.render_region(region_name);
|
@let output = page.render_region(region.key());
|
||||||
@if !output.is_empty() {
|
@if !output.is_empty() {
|
||||||
div id=(region_name) class={ "region-container region-" (region_name) } {
|
@let region_name = region.name();
|
||||||
|
div
|
||||||
|
id=(region_name)
|
||||||
|
class={ "region region--" (region_name) }
|
||||||
|
role="region"
|
||||||
|
aria-label=[region_label.lookup(page)]
|
||||||
|
{
|
||||||
(output)
|
(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,10 +52,12 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
/// Renderiza el contenido del `<head>` de la página.
|
||||||
fn after_render_page_body(&self, page: &mut Page) {}
|
///
|
||||||
|
/// Por defecto incluye las etiquetas básicas (`charset`, `title`, `description`, `viewport`,
|
||||||
fn render_page_head(&self, page: &mut Page) -> Markup {
|
/// `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 {
|
||||||
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
|
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
|
||||||
html! {
|
html! {
|
||||||
head {
|
head {
|
||||||
|
@ -88,12 +87,115 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn error403(&self, _page: &mut Page) -> Markup {
|
/// Interfaz común que debe implementar cualquier tema de PageTop.
|
||||||
html! { div { h1 { ("FORBIDDEN ACCESS") } } }
|
///
|
||||||
|
/// 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<ThemeRef> {
|
||||||
|
/// 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 error404(&self, _page: &mut Page) -> Markup {
|
/// Declaración ordenada de las regiones disponibles en la página.
|
||||||
html! { div { h1 { ("RESOURCE NOT FOUND") } } }
|
///
|
||||||
|
/// 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 `<body>` 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 `<body>` de la página.
|
||||||
|
///
|
||||||
|
/// Si se sobrescribe este método, se puede volver al comportamiento base con:
|
||||||
|
/// `<Self as ThemePage>::render_body(self, page, self.page_regions())`.
|
||||||
|
#[inline]
|
||||||
|
fn render_page_body(&self, page: &mut Page) -> Markup {
|
||||||
|
<Self as ThemePage>::render_body(self, page, self.page_regions())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acciones específicas del tema después de renderizar el `<body>` 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 `<head>` de la página.
|
||||||
|
///
|
||||||
|
/// Si se sobrescribe este método, se puede volver al comportamiento base con:
|
||||||
|
/// `<Self as ThemePage>::render_head(self, page)`.
|
||||||
|
#[inline]
|
||||||
|
fn render_page_head(&self, page: &mut Page) -> Markup {
|
||||||
|
<Self as ThemePage>::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)) } } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Se implementa automáticamente `ThemePage` para cualquier tema.
|
||||||
|
impl<T: Theme> ThemePage for T {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::core::component::{Child, ChildOp, Children};
|
use crate::core::component::{Child, ChildOp, Children};
|
||||||
use crate::core::theme::{ThemeRef, CONTENT_REGION_NAME};
|
use crate::core::theme::ThemeRef;
|
||||||
use crate::{builder_fn, AutoDefault, UniqueId};
|
use crate::{builder_fn, AutoDefault, UniqueId};
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
@ -7,25 +7,81 @@ use parking_lot::RwLock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
// Regiones globales con componentes para un tema dado.
|
// Conjunto de regiones globales asociadas a un tema específico.
|
||||||
static THEME_REGIONS: LazyLock<RwLock<HashMap<UniqueId, ChildrenInRegions>>> =
|
static THEME_REGIONS: LazyLock<RwLock<HashMap<UniqueId, ChildrenInRegions>>> =
|
||||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
// Regiones globales con componentes para cualquier tema.
|
// Conjunto de regiones globales comunes a todos los temas.
|
||||||
static COMMON_REGIONS: LazyLock<RwLock<ChildrenInRegions>> =
|
static COMMON_REGIONS: LazyLock<RwLock<ChildrenInRegions>> =
|
||||||
LazyLock::new(|| RwLock::new(ChildrenInRegions::default()));
|
LazyLock::new(|| RwLock::new(ChildrenInRegions::default()));
|
||||||
|
|
||||||
// Estructura interna para mantener los componentes de una región.
|
/// 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.
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub struct ChildrenInRegions(HashMap<&'static str, Children>);
|
pub struct ChildrenInRegions(HashMap<&'static str, Children>);
|
||||||
|
|
||||||
impl ChildrenInRegions {
|
impl ChildrenInRegions {
|
||||||
pub fn with(region_name: &'static str, child: Child) -> Self {
|
pub fn with(region_name: &'static str, child: Child) -> Self {
|
||||||
ChildrenInRegions::default().with_child_in_region(region_name, ChildOp::Add(child))
|
ChildrenInRegions::default().with_child_in(region_name, ChildOp::Add(child))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child_in_region(mut self, region_name: &'static str, op: ChildOp) -> Self {
|
pub fn with_child_in(mut self, region_name: &'static str, op: ChildOp) -> Self {
|
||||||
if let Some(region) = self.0.get_mut(region_name) {
|
if let Some(region) = self.0.get_mut(region_name) {
|
||||||
region.alter_child(op);
|
region.alter_child(op);
|
||||||
} else {
|
} else {
|
||||||
|
@ -48,25 +104,24 @@ impl ChildrenInRegions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Permite añadir componentes a regiones globales o regiones de temas concretos.
|
/// Punto de acceso para añadir componentes a regiones globales o específicas de un tema.
|
||||||
///
|
///
|
||||||
/// Dada una región, según la variante seleccionada, se le podrán añadir ([`add()`](Self::add))
|
/// Según la variante, se pueden añadir componentes ([`add()`](Self::add)) que permanecerán
|
||||||
/// componentes que se mantendrán durante la ejecución de la aplicación.
|
/// disponibles durante toda la ejecución.
|
||||||
///
|
///
|
||||||
/// Estas estructuras de componentes se renderizarán automáticamente al procesar los documentos HTML
|
/// Estos componentes se renderizarán automáticamente al procesar los documentos HTML que incluyen
|
||||||
/// que las usan, como las páginas de contenido ([`Page`](crate::response::page::Page)), por
|
/// estas regiones, como las páginas de contenido ([`Page`](crate::response::page::Page)).
|
||||||
/// ejemplo.
|
|
||||||
pub enum InRegion {
|
pub enum InRegion {
|
||||||
/// Representa la región por defecto en la que se pueden añadir componentes.
|
/// Región de contenido por defecto.
|
||||||
Content,
|
Content,
|
||||||
/// Representa la región con el nombre del argumento.
|
/// Región identificada por el nombre proporcionado.
|
||||||
Named(&'static str),
|
Named(&'static str),
|
||||||
/// Representa la región con el nombre y del tema especificado en los argumentos.
|
/// Región identificada por un nombre y asociada a un tema concreto.
|
||||||
OfTheme(&'static str, ThemeRef),
|
OfTheme(&'static str, ThemeRef),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InRegion {
|
impl InRegion {
|
||||||
/// Permite añadir un componente en la región de la variante seleccionada.
|
/// Añade un componente a la región indicada por la variante.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
@ -88,17 +143,17 @@ impl InRegion {
|
||||||
InRegion::Content => {
|
InRegion::Content => {
|
||||||
COMMON_REGIONS
|
COMMON_REGIONS
|
||||||
.write()
|
.write()
|
||||||
.alter_child_in_region(CONTENT_REGION_NAME, ChildOp::Add(child));
|
.alter_child_in(REGION_CONTENT, ChildOp::Add(child));
|
||||||
}
|
}
|
||||||
InRegion::Named(name) => {
|
InRegion::Named(region_name) => {
|
||||||
COMMON_REGIONS
|
COMMON_REGIONS
|
||||||
.write()
|
.write()
|
||||||
.alter_child_in_region(name, ChildOp::Add(child));
|
.alter_child_in(region_name, ChildOp::Add(child));
|
||||||
}
|
}
|
||||||
InRegion::OfTheme(region_name, theme_ref) => {
|
InRegion::OfTheme(region_name, theme_ref) => {
|
||||||
let mut regions = THEME_REGIONS.write();
|
let mut regions = THEME_REGIONS.write();
|
||||||
if let Some(r) = regions.get_mut(&theme_ref.type_id()) {
|
if let Some(r) = regions.get_mut(&theme_ref.type_id()) {
|
||||||
r.alter_child_in_region(region_name, ChildOp::Add(child));
|
r.alter_child_in(region_name, ChildOp::Add(child));
|
||||||
} else {
|
} else {
|
||||||
regions.insert(
|
regions.insert(
|
||||||
theme_ref.type_id(),
|
theme_ref.type_id(),
|
||||||
|
|
|
@ -50,12 +50,11 @@ pub struct App {
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
/// Idioma por defecto para la aplicación.
|
/// Idioma por defecto para la aplicación.
|
||||||
///
|
///
|
||||||
/// Si no se especifica un valor válido, normalmente se usará el idioma devuelto por la
|
/// Si no está definido o no es válido, el idioma efectivo para el renderizado se resolverá
|
||||||
/// implementación de [`LangId`](crate::locale::LangId) para [`Context`](crate::html::Context),
|
/// según la implementación de [`LangId`](crate::locale::LangId) en este orden: primero intenta
|
||||||
/// en el siguiente orden: primero, el idioma establecido explícitamente con
|
/// con el establecido en [`Contextual::with_langid()`](crate::html::Contextual::with_langid);
|
||||||
/// [`Context::with_langid()`](crate::html::Context::with_langid); si no se ha definido, se
|
/// pero si no se ha definido explícitamente, usará el indicado en la cabecera `Accept-Language`
|
||||||
/// usará el indicado en la cabecera `Accept-Language` del navegador; y, si ninguno aplica, se
|
/// del navegador; y, si ninguno aplica, se empleará el idioma de respaldo ("en-US").
|
||||||
/// empleará el idioma de respaldo ("en-US").
|
|
||||||
pub language: String,
|
pub language: String,
|
||||||
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
|
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
|
||||||
/// *"Starwars"*.
|
/// *"Starwars"*.
|
||||||
|
@ -68,7 +67,7 @@ pub struct App {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
/// Sección `[Dev]` de la configuración. Forma parte de [`Settings`].
|
/// Sección `[Dev]` de la configuración. Forma parte de [`Settings`].
|
||||||
pub struct Dev {
|
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
|
/// 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
|
/// ruta válida, ya sea absoluta o relativa al directorio del proyecto o del binario en
|
||||||
|
|
100
src/html.rs
|
@ -1,54 +1,84 @@
|
||||||
//! HTML en código.
|
//! HTML en código.
|
||||||
|
|
||||||
mod maud;
|
mod maud;
|
||||||
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE};
|
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE};
|
||||||
|
|
||||||
|
// HTML DOCUMENT ASSETS ****************************************************************************
|
||||||
|
|
||||||
mod assets;
|
mod assets;
|
||||||
pub use assets::favicon::Favicon;
|
pub use assets::favicon::Favicon;
|
||||||
pub use assets::javascript::JavaScript;
|
pub use assets::javascript::JavaScript;
|
||||||
pub use assets::stylesheet::{StyleSheet, TargetMedia};
|
pub use assets::stylesheet::{StyleSheet, TargetMedia};
|
||||||
pub(crate) use assets::Assets;
|
pub use assets::{Asset, Assets};
|
||||||
|
|
||||||
|
// HTML DOCUMENT CONTEXT ***************************************************************************
|
||||||
|
|
||||||
mod context;
|
mod context;
|
||||||
pub use context::{AssetsOp, Context, ErrorParam};
|
pub use context::{AssetsOp, Context, Contextual, ErrorParam};
|
||||||
|
|
||||||
mod opt_id;
|
// HTML ATTRIBUTES *********************************************************************************
|
||||||
pub use opt_id::OptionId;
|
|
||||||
|
|
||||||
mod opt_name;
|
mod attr_id;
|
||||||
pub use opt_name::OptionName;
|
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_string;
|
mod attr_name;
|
||||||
pub use opt_string::OptionString;
|
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_translated;
|
mod attr_value;
|
||||||
pub use opt_translated::OptionTranslated;
|
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_classes;
|
mod attr_l10n;
|
||||||
pub use opt_classes::{ClassesOp, OptionClasses};
|
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_component;
|
mod attr_classes;
|
||||||
pub use opt_component::OptionComponent;
|
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;
|
||||||
|
|
||||||
use crate::AutoDefault;
|
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<C: core::component::Component> = core::component::TypedSlot<C>;
|
||||||
|
|
||||||
/// Prepara contenido HTML para su conversión a [`Markup`].
|
/// Prepara contenido HTML para su conversión a [`Markup`].
|
||||||
///
|
///
|
||||||
/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML escapado o marcado
|
/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML sin escapar o
|
||||||
/// ya procesado) para renderizar de forma homogénea en plantillas sin interferir con el uso
|
/// fragmentos ya procesados) para renderizarlos de forma homogénea en plantillas, sin interferir
|
||||||
/// estándar de [`Markup`].
|
/// con el uso estándar de [`Markup`].
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// let fragment = PrepareMarkup::Text(String::from("Hola <b>mundo</b>"));
|
/// // Texto normal, se escapa automáticamente para evitar inyección de HTML.
|
||||||
|
/// let fragment = PrepareMarkup::Escaped("Hola <b>mundo</b>".to_string());
|
||||||
/// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>");
|
/// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>");
|
||||||
///
|
///
|
||||||
/// let raw_html = PrepareMarkup::Escaped(String::from("<b>negrita</b>"));
|
/// // HTML literal, se inserta directamente, sin escapado adicional.
|
||||||
|
/// let raw_html = PrepareMarkup::Raw("<b>negrita</b>".to_string());
|
||||||
/// assert_eq!(raw_html.render().into_string(), "<b>negrita</b>");
|
/// assert_eq!(raw_html.render().into_string(), "<b>negrita</b>");
|
||||||
///
|
///
|
||||||
|
/// // Fragmento ya preparado con la macro `html!`.
|
||||||
/// let prepared = PrepareMarkup::With(html! {
|
/// let prepared = PrepareMarkup::With(html! {
|
||||||
/// h2 { "Título de ejemplo" }
|
/// h2 { "Título de ejemplo" }
|
||||||
/// p { "Este es un párrafo con contenido dinámico." }
|
/// p { "Este es un párrafo con contenido dinámico." }
|
||||||
|
@ -60,14 +90,22 @@ use crate::AutoDefault;
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub enum PrepareMarkup {
|
pub enum PrepareMarkup {
|
||||||
/// No se genera contenido HTML (devuelve `html! {}`).
|
/// No se genera contenido HTML (equivale a `html! {}`).
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
/// Texto estático que se escapará automáticamente para no ser interpretado como HTML.
|
/// Texto plano que se **escapará automáticamente** para que no sea interpretado como HTML.
|
||||||
Text(String),
|
///
|
||||||
/// Contenido sin escapado adicional, útil para HTML generado externamente.
|
/// Úsalo con textos que provengan de usuarios u otras fuentes externas para garantizar la
|
||||||
|
/// seguridad contra inyección de código.
|
||||||
Escaped(String),
|
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.
|
/// Fragmento HTML ya preparado como [`Markup`], listo para insertarse directamente.
|
||||||
|
///
|
||||||
|
/// Normalmente proviene de expresiones `html! { ... }`.
|
||||||
With(Markup),
|
With(Markup),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,20 +114,18 @@ impl PrepareMarkup {
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
PrepareMarkup::None => true,
|
PrepareMarkup::None => true,
|
||||||
PrepareMarkup::Text(text) => text.is_empty(),
|
PrepareMarkup::Escaped(text) => text.is_empty(),
|
||||||
PrepareMarkup::Escaped(string) => string.is_empty(),
|
PrepareMarkup::Raw(string) => string.is_empty(),
|
||||||
PrepareMarkup::With(markup) => markup.is_empty(),
|
PrepareMarkup::With(markup) => markup.is_empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for PrepareMarkup {
|
|
||||||
/// Integra el renderizado fácilmente en la macro [`html!`].
|
/// Integra el renderizado fácilmente en la macro [`html!`].
|
||||||
fn render(&self) -> Markup {
|
pub fn render(&self) -> Markup {
|
||||||
match self {
|
match self {
|
||||||
PrepareMarkup::None => html! {},
|
PrepareMarkup::None => html! {},
|
||||||
PrepareMarkup::Text(text) => html! { (text) },
|
PrepareMarkup::Escaped(text) => html! { (text) },
|
||||||
PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) },
|
PrepareMarkup::Raw(string) => html! { (PreEscaped(string)) },
|
||||||
PrepareMarkup::With(markup) => html! { (markup) },
|
PrepareMarkup::With(markup) => html! { (markup) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,25 +2,55 @@ pub mod favicon;
|
||||||
pub mod javascript;
|
pub mod javascript;
|
||||||
pub mod stylesheet;
|
pub mod stylesheet;
|
||||||
|
|
||||||
use crate::html::{html, Markup, Render};
|
use crate::html::{html, Context, Markup};
|
||||||
use crate::{AutoDefault, Weight};
|
use crate::{AutoDefault, Weight};
|
||||||
|
|
||||||
pub trait AssetsTrait: Render {
|
/// Representación genérica de un script [`JavaScript`](crate::html::JavaScript) o una hoja de
|
||||||
// Devuelve el nombre del recurso, utilizado como clave única.
|
/// 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.
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
// Devuelve el peso del recurso, durante el renderizado se procesan de menor a mayor peso.
|
/// Devuelve el peso del recurso, usado para ordenar el renderizado de menor a mayor peso.
|
||||||
fn weight(&self) -> Weight;
|
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)]
|
#[derive(AutoDefault)]
|
||||||
pub(crate) struct Assets<T>(Vec<T>);
|
pub struct Assets<T>(Vec<T>);
|
||||||
|
|
||||||
impl<T: AssetsTrait> Assets<T> {
|
impl<T: Asset> Assets<T> {
|
||||||
|
/// 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.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Assets::<T>(Vec::<T>::new())
|
Self(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 {
|
pub fn add(&mut self, asset: T) -> bool {
|
||||||
match self.0.iter().position(|x| x.name() == asset.name()) {
|
match self.0.iter().position(|x| x.name() == asset.name()) {
|
||||||
Some(index) => {
|
Some(index) => {
|
||||||
|
@ -39,6 +69,9 @@ impl<T: AssetsTrait> Assets<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Elimina un recurso por nombre.
|
||||||
|
///
|
||||||
|
/// Devuelve `true` si el recurso existía y fue eliminado.
|
||||||
pub fn remove(&mut self, name: impl AsRef<str>) -> bool {
|
pub fn remove(&mut self, name: impl AsRef<str>) -> bool {
|
||||||
if let Some(index) = self.0.iter().position(|x| x.name() == name.as_ref()) {
|
if let Some(index) = self.0.iter().position(|x| x.name() == name.as_ref()) {
|
||||||
self.0.remove(index);
|
self.0.remove(index);
|
||||||
|
@ -47,16 +80,13 @@ impl<T: AssetsTrait> Assets<T> {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AssetsTrait> Render for Assets<T> {
|
pub fn render(&self, cx: &mut Context) -> Markup {
|
||||||
fn render(&self) -> Markup {
|
|
||||||
let mut assets = self.0.iter().collect::<Vec<_>>();
|
let mut assets = self.0.iter().collect::<Vec<_>>();
|
||||||
assets.sort_by_key(|a| a.weight());
|
assets.sort_by_key(|a| a.weight());
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
@for a in assets {
|
@for a in assets {
|
||||||
(a.render())
|
(a.render(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::html::{html, Markup, Render};
|
use crate::html::{html, Context, Markup};
|
||||||
use crate::AutoDefault;
|
use crate::AutoDefault;
|
||||||
|
|
||||||
/// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio.
|
/// 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<String>,
|
icon_color: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let icon_type = match icon_source.rfind('.') {
|
let icon_type = match icon_source.rfind('.') {
|
||||||
Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() {
|
Some(i) => match icon_source[i..].to_string().to_lowercase().as_str() {
|
||||||
".avif" => Some("image/avif"),
|
".avif" => Some("image/avif"),
|
||||||
".gif" => Some("image/gif"),
|
".gif" => Some("image/gif"),
|
||||||
".ico" => Some("image/x-icon"),
|
".ico" => Some("image/x-icon"),
|
||||||
|
@ -151,10 +151,12 @@ impl Favicon {
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Favicon {
|
/// Renderiza el **Favicon** completo con todas las etiquetas declaradas.
|
||||||
fn render(&self) -> Markup {
|
///
|
||||||
|
/// 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 {
|
||||||
html! {
|
html! {
|
||||||
@for item in &self.0 {
|
@for item in &self.0 {
|
||||||
(item)
|
(item)
|
||||||
|
|
|
@ -1,35 +1,45 @@
|
||||||
use crate::html::assets::AssetsTrait;
|
use crate::html::assets::Asset;
|
||||||
use crate::html::{html, Markup, Render};
|
use crate::html::{html, Context, Markup, PreEscaped};
|
||||||
use crate::{join, join_pair, AutoDefault, Weight};
|
use crate::{join, join_pair, AutoDefault, Weight};
|
||||||
|
|
||||||
// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador.
|
// 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
|
// Los distintos modos de carga permiten optimizar el rendimiento y controlar el comportamiento del
|
||||||
// script.
|
// script en relación con el análisis del documento HTML y la ejecución del resto de scripts.
|
||||||
//
|
//
|
||||||
// - [`From`] – Carga el script de forma estándar con la etiqueta `<script src="...">`.
|
// - [`From`] – Carga estándar con la etiqueta `<script src="...">`.
|
||||||
// - [`Defer`] – Igual que [`From`], pero con el atributo `defer`.
|
// - [`Defer`] – Igual que [`From`], pero con el atributo `defer`, descarga en paralelo y se
|
||||||
// - [`Async`] – Igual que [`From`], pero con el atributo `async`.
|
// ejecuta tras el análisis del documento HTML, respetando el orden de
|
||||||
|
// aparición.
|
||||||
|
// - [`Async`] – Igual que [`From`], pero con el atributo `async`, descarga en paralelo y se
|
||||||
|
// ejecuta en cuanto esté listo, **sin garantizar** el orden relativo respecto a
|
||||||
|
// otros scripts.
|
||||||
// - [`Inline`] – Inserta el código directamente en la etiqueta `<script>`.
|
// - [`Inline`] – Inserta el código directamente en la etiqueta `<script>`.
|
||||||
// - [`OnLoad`] – Inserta el código JavaScript y lo ejecuta tras el evento `DOMContentLoaded`.
|
// - [`OnLoad`] – Inserta el código JavaScript y lo ejecuta tras el evento `DOMContentLoaded`.
|
||||||
|
// - [`OnLoadAsync`] – Igual que [`OnLoad`], pero con manejador asíncrono (`async`), útil si dentro
|
||||||
|
// del código JavaScript se utiliza `await`.
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
enum Source {
|
enum Source {
|
||||||
#[default]
|
#[default]
|
||||||
From(String),
|
From(String),
|
||||||
Defer(String),
|
Defer(String),
|
||||||
Async(String),
|
Async(String),
|
||||||
Inline(String, String),
|
// `name`, `closure(Context) -> String`.
|
||||||
OnLoad(String, String),
|
Inline(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||||
|
// `name`, `closure(Context) -> String` (se ejecuta tras `DOMContentLoaded`).
|
||||||
|
OnLoad(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||||
|
// `name`, `closure(Context) -> String` (manejador `async` tras `DOMContentLoaded`).
|
||||||
|
OnLoadAsync(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Define un recurso **JavaScript** para incluir en un documento HTML.
|
/// Define un recurso **JavaScript** para incluir en un documento HTML.
|
||||||
///
|
///
|
||||||
/// Este tipo permite añadir *scripts* externos o embebidos con distintas estrategias de carga
|
/// Este tipo permite añadir scripts externos o embebidos con distintas estrategias de carga
|
||||||
/// (`defer`, `async`, *inline*, etc.) y [pesos](crate::Weight) para controlar el orden de inserción
|
/// (`defer`, `async`, *inline*, etc.) y [pesos](crate::Weight) para controlar el orden de inserción
|
||||||
/// en el documento.
|
/// en el documento.
|
||||||
///
|
///
|
||||||
/// > **Nota**
|
/// > **Nota**
|
||||||
/// > Los archivos de los *scripts* deben estar disponibles en el servidor web de la aplicación.
|
/// > Los archivos de los scripts deben estar disponibles en el servidor web de la aplicación.
|
||||||
/// > Pueden servirse usando [`static_files_service!`](crate::static_files_service).
|
/// > Pueden servirse usando [`static_files_service!`](crate::static_files_service).
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
|
@ -37,23 +47,37 @@ enum Source {
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// // Script externo con carga diferida, versión para control de caché y prioriza el renderizado.
|
/// // Script externo con carga diferida, versión de caché y prioridad en el renderizado.
|
||||||
/// let script = JavaScript::defer("/assets/js/app.js")
|
/// let script = JavaScript::defer("/assets/js/app.js")
|
||||||
/// .with_version("1.2.3")
|
/// .with_version("1.2.3")
|
||||||
/// .with_weight(-10);
|
/// .with_weight(-10);
|
||||||
///
|
///
|
||||||
/// // Script embebido que se ejecuta tras la carga del documento.
|
/// // Script embebido que se ejecuta tras la carga del documento.
|
||||||
/// let script = JavaScript::on_load("init_tooltips", r#"
|
/// let script = JavaScript::on_load("init_tooltips", |_| r#"
|
||||||
/// const tooltips = document.querySelectorAll('[data-tooltip]');
|
/// const tooltips = document.querySelectorAll('[data-tooltip]');
|
||||||
/// for (const el of tooltips) {
|
/// for (const el of tooltips) {
|
||||||
/// el.addEventListener('mouseenter', showTooltip);
|
/// el.addEventListener('mouseenter', showTooltip);
|
||||||
/// }
|
/// }
|
||||||
/// "#);
|
/// "#.to_string());
|
||||||
|
///
|
||||||
|
/// // Script embebido con manejador asíncrono (`async`) que puede usar `await`.
|
||||||
|
/// let mut cx = Context::new(None).with_param("user_id", 7u32);
|
||||||
|
///
|
||||||
|
/// let js = JavaScript::on_load_async("hydrate", |cx| {
|
||||||
|
/// // Ejemplo: lectura de un parámetro del contexto para inyectarlo en el código.
|
||||||
|
/// let uid: u32 = cx.param_or_default("user_id");
|
||||||
|
/// format!(r#"
|
||||||
|
/// const USER_ID = {};
|
||||||
|
/// await Promise.resolve(USER_ID);
|
||||||
|
/// // Aquí se podría hidratar la interfaz o cargar módulos dinámicos:
|
||||||
|
/// // await import('/assets/js/hydrate.js');
|
||||||
|
/// "#, uid)
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub struct JavaScript {
|
pub struct JavaScript {
|
||||||
source : Source, // Fuente y modo de carga del script.
|
source : Source, // Fuente y estrategia de carga del script.
|
||||||
version: String, // Versión del recurso para la caché del navegador.
|
version: String, // Versión del recurso para la caché del navegador.
|
||||||
weight : Weight, // Peso que determina el orden.
|
weight : Weight, // Peso que determina el orden.
|
||||||
}
|
}
|
||||||
|
@ -70,11 +94,11 @@ impl JavaScript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un **script externo** con el atributo `defer`, que se carga en segundo plano y se
|
/// Crea un **script externo** con el atributo `defer`, que se descarga en paralelo y se ejecuta
|
||||||
/// ejecuta tras analizar completamente el documento HTML.
|
/// tras analizar completamente el documento HTML, **respetando el orden** de inserción.
|
||||||
///
|
///
|
||||||
/// Equivale a `<script src="..." defer>`. Útil para mantener el orden de ejecución y evitar
|
/// Equivale a `<script src="..." defer>`. Suele ser la opción recomendada para scripts no
|
||||||
/// bloquear el análisis del documento HTML.
|
/// críticos.
|
||||||
pub fn defer(path: impl Into<String>) -> Self {
|
pub fn defer(path: impl Into<String>) -> Self {
|
||||||
JavaScript {
|
JavaScript {
|
||||||
source: Source::Defer(path.into()),
|
source: Source::Defer(path.into()),
|
||||||
|
@ -82,11 +106,10 @@ impl JavaScript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un **script externo** con el atributo `async`, que se carga y ejecuta de forma
|
/// Crea un **script externo** con el atributo `async`, que se descarga en paralelo y se ejecuta
|
||||||
/// asíncrona tan pronto como esté disponible.
|
/// tan pronto como esté disponible.
|
||||||
///
|
///
|
||||||
/// Equivale a `<script src="..." async>`. La ejecución puede producirse fuera de orden respecto
|
/// Equivale a `<script src="..." async>`. **No garantiza** el orden relativo con otros scripts.
|
||||||
/// a otros *scripts*.
|
|
||||||
pub fn asynchronous(path: impl Into<String>) -> Self {
|
pub fn asynchronous(path: impl Into<String>) -> Self {
|
||||||
JavaScript {
|
JavaScript {
|
||||||
source: Source::Async(path.into()),
|
source: Source::Async(path.into()),
|
||||||
|
@ -97,37 +120,68 @@ impl JavaScript {
|
||||||
/// Crea un **script embebido** directamente en el documento HTML.
|
/// Crea un **script embebido** directamente en el documento HTML.
|
||||||
///
|
///
|
||||||
/// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del
|
/// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del
|
||||||
/// *script*.
|
/// script.
|
||||||
pub fn inline(name: impl Into<String>, script: impl Into<String>) -> Self {
|
///
|
||||||
|
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||||
|
pub fn inline<F>(name: impl Into<String>, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
{
|
||||||
JavaScript {
|
JavaScript {
|
||||||
source: Source::Inline(name.into(), script.into()),
|
source: Source::Inline(name.into(), Box::new(f)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un **script embebido** que se ejecuta automáticamente al terminar de cargarse el
|
/// Crea un **script embebido** que se ejecuta cuando **el DOM está listo**.
|
||||||
/// documento HTML.
|
|
||||||
///
|
///
|
||||||
/// El código se envuelve automáticamente en un `addEventListener('DOMContentLoaded', ...)`. El
|
/// El código se envuelve en un `addEventListener('DOMContentLoaded',function(){...})` que lo
|
||||||
/// parámetro `name` se usa como identificador interno del *script*.
|
/// ejecuta tras analizar el documento HTML, **no** espera imágenes ni otros recursos externos.
|
||||||
pub fn on_load(name: impl Into<String>, script: impl Into<String>) -> Self {
|
/// Ú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<F>(name: impl Into<String>, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
{
|
||||||
JavaScript {
|
JavaScript {
|
||||||
source: Source::OnLoad(name.into(), script.into()),
|
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<F>(name: impl Into<String>, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
JavaScript {
|
||||||
|
source: Source::OnLoadAsync(name.into(), Box::new(f)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JavaScript BUILDER **************************************************************************
|
// 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<String>) -> Self {
|
pub fn with_version(mut self, version: impl Into<String>) -> Self {
|
||||||
self.version = version.into();
|
self.version = version.into();
|
||||||
self
|
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
|
/// Los recursos se renderizan de menor a mayor peso. Por defecto es `0`, que respeta el orden
|
||||||
/// de creación.
|
/// de creación.
|
||||||
|
@ -137,8 +191,10 @@ impl JavaScript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssetsTrait for JavaScript {
|
impl Asset for JavaScript {
|
||||||
// Para *scripts* externos es la ruta; para *scripts* embebidos, un identificador.
|
/// Devuelve el nombre del recurso, utilizado como clave única.
|
||||||
|
///
|
||||||
|
/// Para scripts externos es la ruta del recurso; para scripts embebidos, un identificador.
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
match &self.source {
|
match &self.source {
|
||||||
Source::From(path) => path,
|
Source::From(path) => path,
|
||||||
|
@ -146,16 +202,15 @@ impl AssetsTrait for JavaScript {
|
||||||
Source::Async(path) => path,
|
Source::Async(path) => path,
|
||||||
Source::Inline(name, _) => name,
|
Source::Inline(name, _) => name,
|
||||||
Source::OnLoad(name, _) => name,
|
Source::OnLoad(name, _) => name,
|
||||||
|
Source::OnLoadAsync(name, _) => name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn weight(&self) -> Weight {
|
fn weight(&self) -> Weight {
|
||||||
self.weight
|
self.weight
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for JavaScript {
|
fn render(&self, cx: &mut Context) -> Markup {
|
||||||
fn render(&self) -> Markup {
|
|
||||||
match &self.source {
|
match &self.source {
|
||||||
Source::From(path) => html! {
|
Source::From(path) => html! {
|
||||||
script src=(join_pair!(path, "?v=", self.version.as_str())) {};
|
script src=(join_pair!(path, "?v=", self.version.as_str())) {};
|
||||||
|
@ -166,12 +221,15 @@ impl Render for JavaScript {
|
||||||
Source::Async(path) => html! {
|
Source::Async(path) => html! {
|
||||||
script src=(join_pair!(path, "?v=", self.version.as_str())) async {};
|
script src=(join_pair!(path, "?v=", self.version.as_str())) async {};
|
||||||
},
|
},
|
||||||
Source::Inline(_, code) => html! {
|
Source::Inline(_, f) => html! {
|
||||||
script { (code) };
|
script { (PreEscaped((f)(cx))) };
|
||||||
},
|
},
|
||||||
Source::OnLoad(_, code) => html! { (join!(
|
Source::OnLoad(_, f) => html! { script { (PreEscaped(join!(
|
||||||
"document.addEventListener('DOMContentLoaded',function(){", code, "});"
|
"document.addEventListener(\"DOMContentLoaded\",function(){", (f)(cx), "});"
|
||||||
)) },
|
))) } },
|
||||||
|
Source::OnLoadAsync(_, f) => html! { script { (PreEscaped(join!(
|
||||||
|
"document.addEventListener(\"DOMContentLoaded\",async()=>{", (f)(cx), "});"
|
||||||
|
))) } },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::html::assets::AssetsTrait;
|
use crate::html::assets::Asset;
|
||||||
use crate::html::{html, Markup, PreEscaped, Render};
|
use crate::html::{html, Context, Markup, PreEscaped};
|
||||||
use crate::{join_pair, AutoDefault, Weight};
|
use crate::{join_pair, AutoDefault, Weight};
|
||||||
|
|
||||||
// Define el origen del recurso CSS y cómo se incluye en el documento.
|
// Define el origen del recurso CSS y cómo se incluye en el documento.
|
||||||
|
@ -14,7 +14,8 @@ use crate::{join_pair, AutoDefault, Weight};
|
||||||
enum Source {
|
enum Source {
|
||||||
#[default]
|
#[default]
|
||||||
From(String),
|
From(String),
|
||||||
Inline(String, String),
|
// `name`, `closure(Context) -> String`.
|
||||||
|
Inline(String, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Define el medio objetivo para la hoja de estilos.
|
/// Define el medio objetivo para la hoja de estilos.
|
||||||
|
@ -34,7 +35,7 @@ pub enum TargetMedia {
|
||||||
Speech,
|
Speech,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve el texto asociado al punto de interrupción usado por Bootstrap.
|
/// Devuelve el valor para el atributo `media` (`Some(...)`) o `None` para `Default`.
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
impl TargetMedia {
|
impl TargetMedia {
|
||||||
fn as_str_opt(&self) -> Option<&str> {
|
fn as_str_opt(&self) -> Option<&str> {
|
||||||
|
@ -69,12 +70,12 @@ impl TargetMedia {
|
||||||
/// .with_weight(-10);
|
/// .with_weight(-10);
|
||||||
///
|
///
|
||||||
/// // Crea una hoja de estilos embebida en el documento HTML.
|
/// // 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 {
|
/// body {
|
||||||
/// background-color: #f5f5f5;
|
/// background-color: #f5f5f5;
|
||||||
/// font-family: 'Segoe UI', sans-serif;
|
/// font-family: 'Segoe UI', sans-serif;
|
||||||
/// }
|
/// }
|
||||||
/// "#);
|
/// "#.to_string());
|
||||||
/// ```
|
/// ```
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
|
@ -100,9 +101,14 @@ impl StyleSheet {
|
||||||
///
|
///
|
||||||
/// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del
|
/// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del
|
||||||
/// recurso.
|
/// recurso.
|
||||||
pub fn inline(name: impl Into<String>, styles: impl Into<String>) -> Self {
|
///
|
||||||
|
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||||
|
pub fn inline<F>(name: impl Into<String>, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
{
|
||||||
StyleSheet {
|
StyleSheet {
|
||||||
source: Source::Inline(name.into(), styles.into()),
|
source: Source::Inline(name.into(), Box::new(f)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,17 +139,19 @@ impl StyleSheet {
|
||||||
/// Según el argumento `media`:
|
/// Según el argumento `media`:
|
||||||
///
|
///
|
||||||
/// - `TargetMedia::Default` - Se aplica en todos los casos (medio por defecto).
|
/// - `TargetMedia::Default` - Se aplica en todos los casos (medio por defecto).
|
||||||
/// - `TargetMedia::Print` - Se aplican cuando el documento se imprime.
|
/// - `TargetMedia::Print` - Se aplica cuando el documento se imprime.
|
||||||
/// - `TargetMedia::Screen` - Se aplican en pantallas.
|
/// - `TargetMedia::Screen` - Se aplica en pantallas.
|
||||||
/// - `TargetMedia::Speech` - Se aplican en dispositivos que convierten el texto a voz.
|
/// - `TargetMedia::Speech` - Se aplica en dispositivos que convierten el texto a voz.
|
||||||
pub fn for_media(mut self, media: TargetMedia) -> Self {
|
pub fn for_media(mut self, media: TargetMedia) -> Self {
|
||||||
self.media = media;
|
self.media = media;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssetsTrait for StyleSheet {
|
impl Asset for StyleSheet {
|
||||||
// Para hojas de estilos externas es la ruta; para las embebidas, un identificador.
|
/// Devuelve el nombre del recurso, utilizado como clave única.
|
||||||
|
///
|
||||||
|
/// Para hojas de estilos externas es la ruta del recurso; para las embebidas, un identificador.
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
match &self.source {
|
match &self.source {
|
||||||
Source::From(path) => path,
|
Source::From(path) => path,
|
||||||
|
@ -154,10 +162,8 @@ impl AssetsTrait for StyleSheet {
|
||||||
fn weight(&self) -> Weight {
|
fn weight(&self) -> Weight {
|
||||||
self.weight
|
self.weight
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for StyleSheet {
|
fn render(&self, cx: &mut Context) -> Markup {
|
||||||
fn render(&self) -> Markup {
|
|
||||||
match &self.source {
|
match &self.source {
|
||||||
Source::From(path) => html! {
|
Source::From(path) => html! {
|
||||||
link
|
link
|
||||||
|
@ -165,8 +171,8 @@ impl Render for StyleSheet {
|
||||||
href=(join_pair!(path, "?v=", self.version.as_str()))
|
href=(join_pair!(path, "?v=", self.version.as_str()))
|
||||||
media=[self.media.as_str_opt()];
|
media=[self.media.as_str_opt()];
|
||||||
},
|
},
|
||||||
Source::Inline(_, code) => html! {
|
Source::Inline(_, f) => html! {
|
||||||
style { (PreEscaped(code)) };
|
style { (PreEscaped((f)(cx))) };
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{builder_fn, AutoDefault};
|
use crate::{builder_fn, AutoDefault};
|
||||||
|
|
||||||
/// Operaciones disponibles sobre la lista de clases en [`OptionClasses`].
|
/// Operaciones disponibles sobre la lista de clases en [`AttrClasses`].
|
||||||
pub enum ClassesOp {
|
pub enum ClassesOp {
|
||||||
/// Añade al final (si no existe).
|
/// Añade al final (si no existe).
|
||||||
Add,
|
Add,
|
||||||
|
@ -25,6 +25,7 @@ pub enum ClassesOp {
|
||||||
///
|
///
|
||||||
/// - El [orden de las clases no es relevante](https://stackoverflow.com/a/1321712) en CSS.
|
/// - El [orden de las clases no es relevante](https://stackoverflow.com/a/1321712) en CSS.
|
||||||
/// - No se permiten clases duplicadas.
|
/// - No se permiten clases duplicadas.
|
||||||
|
/// - Las clases se convierten a minúsculas.
|
||||||
/// - Las clases vacías se ignoran.
|
/// - Las clases vacías se ignoran.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
|
@ -32,26 +33,26 @@ pub enum ClassesOp {
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// let classes = OptionClasses::new("btn btn-primary")
|
/// let classes = AttrClasses::new("Btn btn-primary")
|
||||||
/// .with_value(ClassesOp::Add, "active")
|
/// .with_value(ClassesOp::Add, "Active")
|
||||||
/// .with_value(ClassesOp::Remove, "btn-primary");
|
/// .with_value(ClassesOp::Remove, "btn-primary");
|
||||||
///
|
///
|
||||||
/// assert_eq!(classes.get(), Some(String::from("btn active")));
|
/// assert_eq!(classes.get(), Some("btn active".to_string()));
|
||||||
/// assert!(classes.contains("active"));
|
/// assert!(classes.contains("active"));
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
pub struct OptionClasses(Vec<String>);
|
pub struct AttrClasses(Vec<String>);
|
||||||
|
|
||||||
impl OptionClasses {
|
impl AttrClasses {
|
||||||
pub fn new(classes: impl AsRef<str>) -> Self {
|
pub fn new(classes: impl AsRef<str>) -> Self {
|
||||||
OptionClasses::default().with_value(ClassesOp::Prepend, classes)
|
AttrClasses::default().with_value(ClassesOp::Prepend, classes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionClasses BUILDER ***********************************************************************
|
// AttrClasses BUILDER *************************************************************************
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||||
let classes: &str = classes.as_ref();
|
let classes = classes.as_ref().to_ascii_lowercase();
|
||||||
let classes: Vec<&str> = classes.split_ascii_whitespace().collect();
|
let classes: Vec<&str> = classes.split_ascii_whitespace().collect();
|
||||||
|
|
||||||
if classes.is_empty() {
|
if classes.is_empty() {
|
||||||
|
@ -113,9 +114,9 @@ impl OptionClasses {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionClasses GETTERS ***********************************************************************
|
// AttrClasses GETTERS *************************************************************************
|
||||||
|
|
||||||
/// Devuele la cadena de clases, si existe.
|
/// Devuelve la cadena de clases, si existe.
|
||||||
pub fn get(&self) -> Option<String> {
|
pub fn get(&self) -> Option<String> {
|
||||||
if self.0.is_empty() {
|
if self.0.is_empty() {
|
||||||
None
|
None
|
63
src/html/attr_id.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::{builder_fn, AutoDefault};
|
||||||
|
|
||||||
|
/// Identificador normalizado para el atributo `id` o similar de HTML.
|
||||||
|
///
|
||||||
|
/// Este tipo encapsula `Option<String>` 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<String>);
|
||||||
|
|
||||||
|
impl AttrId {
|
||||||
|
/// Crea un nuevo `AttrId` normalizando el valor.
|
||||||
|
pub fn new(value: impl AsRef<str>) -> 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<str>) -> 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<String> {
|
||||||
|
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<String> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` si no hay valor.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_none()
|
||||||
|
}
|
||||||
|
}
|
68
src/html/attr_l10n.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
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<String> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
63
src/html/attr_name.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::{builder_fn, AutoDefault};
|
||||||
|
|
||||||
|
/// Nombre normalizado para el atributo `name` o similar de HTML.
|
||||||
|
///
|
||||||
|
/// Este tipo encapsula `Option<String>` 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<String>);
|
||||||
|
|
||||||
|
impl AttrName {
|
||||||
|
/// Crea un nuevo `AttrName` normalizando el valor.
|
||||||
|
pub fn new(value: impl AsRef<str>) -> 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<str>) -> 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<String> {
|
||||||
|
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<String> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` si no hay valor.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_none()
|
||||||
|
}
|
||||||
|
}
|
65
src/html/attr_value.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use crate::{builder_fn, AutoDefault};
|
||||||
|
|
||||||
|
/// Cadena normalizada para renderizar en atributos HTML.
|
||||||
|
///
|
||||||
|
/// Este tipo encapsula `Option<String>` 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<String>);
|
||||||
|
|
||||||
|
impl AttrValue {
|
||||||
|
/// Crea un nuevo `AttrValue` normalizando el valor.
|
||||||
|
pub fn new(value: impl AsRef<str>) -> 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<str>) -> 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<String> {
|
||||||
|
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<String> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` si no hay valor.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_none()
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,13 +7,10 @@ use crate::locale::{LangId, LangMatch, LanguageIdentifier, DEFAULT_LANGID, FALLB
|
||||||
use crate::service::HttpRequest;
|
use crate::service::HttpRequest;
|
||||||
use crate::{builder_fn, join};
|
use crate::{builder_fn, join};
|
||||||
|
|
||||||
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use std::fmt;
|
/// Operaciones para modificar el contexto ([`Context`]) de un documento.
|
||||||
|
|
||||||
/// Operaciones para modificar el contexto ([`Context`]) del documento.
|
|
||||||
pub enum AssetsOp {
|
pub enum AssetsOp {
|
||||||
// Favicon.
|
// Favicon.
|
||||||
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
|
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
|
||||||
|
@ -28,38 +25,138 @@ pub enum AssetsOp {
|
||||||
RemoveStyleSheet(&'static str),
|
RemoveStyleSheet(&'static str),
|
||||||
|
|
||||||
// JavaScripts.
|
// JavaScripts.
|
||||||
/// Añade un *script* JavaScript al documento.
|
/// Añade un script JavaScript al documento.
|
||||||
AddJavaScript(JavaScript),
|
AddJavaScript(JavaScript),
|
||||||
/// Elimina un *script* por su ruta o identificador.
|
/// Elimina un script por su ruta o identificador.
|
||||||
RemoveJavaScript(&'static str),
|
RemoveJavaScript(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errores de lectura o conversión de parámetros almacenados en el contexto.
|
/// 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.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ErrorParam {
|
pub enum ErrorParam {
|
||||||
/// El parámetro solicitado no existe.
|
|
||||||
NotFound,
|
NotFound,
|
||||||
/// El valor del parámetro no pudo convertirse al tipo requerido.
|
TypeMismatch {
|
||||||
ParseError(String),
|
key: &'static str,
|
||||||
|
expected: &'static str,
|
||||||
|
saved: &'static str,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ErrorParam {
|
/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
|
||||||
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}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for ErrorParam {}
|
|
||||||
|
|
||||||
/// Representa el contexto de un documento HTML.
|
|
||||||
///
|
///
|
||||||
/// Se crea internamente para manejar información relevante del documento, como la solicitud HTTP de
|
/// `Contextual` extiende [`LangId`] y define los métodos para:
|
||||||
/// 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
|
/// - Establecer el **idioma** del documento.
|
||||||
/// definidos en tiempo de ejecución.
|
/// - 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<C: Contextual>(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<HttpRequest>) -> 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<T: 'static>(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<T: 'static>(&self, key: &'static str) -> Option<&T>;
|
||||||
|
|
||||||
|
/// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
|
||||||
|
fn param_or_default<T: Default + Clone + 'static>(&self, key: &'static str) -> T {
|
||||||
|
self.param::<T>(key).cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve el parámetro clonado o un **valor por defecto** si no existe.
|
||||||
|
fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
|
||||||
|
self.param::<T>(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: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T {
|
||||||
|
self.param::<T>(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<StyleSheet>;
|
||||||
|
|
||||||
|
/// Devuelve los scripts JavaScript de los recursos del contexto.
|
||||||
|
fn javascripts(&self) -> &Assets<JavaScript>;
|
||||||
|
|
||||||
|
// Contextual HELPERS **************************************************************************
|
||||||
|
|
||||||
|
/// Genera un identificador único por tipo (`<tipo>-<n>`) 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<T>(&mut self, id: Option<String>) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementa un **contexto de renderizado** para 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.
|
||||||
///
|
///
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
|
@ -96,7 +193,7 @@ impl Error for ErrorParam {}
|
||||||
/// assert_eq!(active_theme.short_name(), "aliner");
|
/// assert_eq!(active_theme.short_name(), "aliner");
|
||||||
///
|
///
|
||||||
/// // Recupera el parámetro a su tipo original.
|
/// // Recupera el parámetro a su tipo original.
|
||||||
/// let id: i32 = cx.get_param("usuario_id").unwrap();
|
/// let id: i32 = *cx.get_param::<i32>("usuario_id").unwrap();
|
||||||
/// assert_eq!(id, 42);
|
/// assert_eq!(id, 42);
|
||||||
///
|
///
|
||||||
/// // Genera un identificador para un componente de tipo `Menu`.
|
/// // Genera un identificador para un componente de tipo `Menu`.
|
||||||
|
@ -114,10 +211,16 @@ pub struct Context {
|
||||||
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
||||||
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
|
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||||
javascripts: Assets<JavaScript>, // Scripts JavaScript.
|
javascripts: Assets<JavaScript>, // Scripts JavaScript.
|
||||||
params : HashMap<&'static str, String>, // Parámetros definidos en tiempo de ejecución.
|
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
|
||||||
id_counter : usize, // Contador para generar identificadores únicos.
|
id_counter : usize, // Contador para generar identificadores únicos.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Context {
|
||||||
|
fn default() -> Self {
|
||||||
|
Context::new(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// Crea un nuevo contexto asociado a una solicitud HTTP.
|
/// Crea un nuevo contexto asociado a una solicitud HTTP.
|
||||||
///
|
///
|
||||||
|
@ -147,40 +250,199 @@ impl Context {
|
||||||
favicon : None,
|
favicon : None,
|
||||||
stylesheets: Assets::<StyleSheet>::new(),
|
stylesheets: Assets::<StyleSheet>::new(),
|
||||||
javascripts: Assets::<JavaScript>::new(),
|
javascripts: Assets::<JavaScript>::new(),
|
||||||
params : HashMap::<&str, String>::new(),
|
params : HashMap::default(),
|
||||||
id_counter : 0,
|
id_counter : 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context BUILDER *****************************************************************************
|
// 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<StyleSheet>::default() en self.
|
||||||
|
let javascripts = mem_take(&mut self.javascripts); // Assets<JavaScript>::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::<String>("usuario_id").is_err());
|
||||||
|
/// ```
|
||||||
|
pub fn get_param<T: 'static>(&self, key: &'static str) -> Result<&T, ErrorParam> {
|
||||||
|
let (any, type_name) = self.params.get(key).ok_or(ErrorParam::NotFound)?;
|
||||||
|
any.downcast_ref::<T>()
|
||||||
|
.ok_or_else(|| ErrorParam::TypeMismatch {
|
||||||
|
key,
|
||||||
|
expected: TypeInfo::FullName.of::<T>(),
|
||||||
|
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::<i32>("contador").is_err()); // ya no está
|
||||||
|
///
|
||||||
|
/// // Error de tipo:
|
||||||
|
/// assert!(cx.take_param::<i32>("titulo").is_err());
|
||||||
|
/// ```
|
||||||
|
pub fn take_param<T: 'static>(&mut self, key: &'static str) -> Result<T, ErrorParam> {
|
||||||
|
let (boxed, saved) = self.params.remove(key).ok_or(ErrorParam::NotFound)?;
|
||||||
|
boxed
|
||||||
|
.downcast::<T>()
|
||||||
|
.map(|b| *b)
|
||||||
|
.map_err(|_| ErrorParam::TypeMismatch {
|
||||||
|
key,
|
||||||
|
expected: TypeInfo::FullName.of::<T>(),
|
||||||
|
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 **************************************************************************
|
||||||
|
|
||||||
/// Modifica la fuente de idioma del documento.
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_langid(mut self, language: &impl LangId) -> Self {
|
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
|
||||||
|
self.request = request;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_langid(mut self, language: &impl LangId) -> Self {
|
||||||
self.langid = language.langid();
|
self.langid = language.langid();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifica el tema que se usará para renderizar el documento.
|
/// Asigna el tema para renderizar el documento.
|
||||||
///
|
///
|
||||||
/// Localiza el tema por su [`short_name()`](crate::core::AnyInfo::short_name), y si no aplica
|
/// Localiza el tema por su [`short_name()`](crate::core::AnyInfo::short_name), y si no aplica
|
||||||
/// ninguno entonces usará el tema por defecto.
|
/// ninguno entonces usará el tema por defecto.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_theme(mut self, theme_name: &'static str) -> Self {
|
fn with_theme(mut self, theme_name: &'static str) -> Self {
|
||||||
self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME);
|
self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifica la composición para renderizar el documento.
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_layout(mut self, layout_name: &'static str) -> Self {
|
fn with_layout(mut self, layout_name: &'static str) -> Self {
|
||||||
self.layout = layout_name;
|
self.layout = layout_name;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Define los recursos del contexto usando [`AssetsOp`].
|
/// 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"]);
|
||||||
|
/// ```
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_assets(mut self, op: AssetsOp) -> Self {
|
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
|
||||||
|
let type_name = TypeInfo::FullName.of::<T>();
|
||||||
|
self.params.insert(key, (Box::new(value), type_name));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_assets(mut self, op: AssetsOp) -> Self {
|
||||||
match op {
|
match op {
|
||||||
// Favicon.
|
// Favicon.
|
||||||
AssetsOp::SetFavicon(favicon) => {
|
AssetsOp::SetFavicon(favicon) => {
|
||||||
|
@ -209,69 +471,74 @@ impl Context {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context GETTERS *****************************************************************************
|
// Contextual GETTERS **************************************************************************
|
||||||
|
|
||||||
/// Devuelve una referencia a la solicitud HTTP asociada, si existe.
|
fn request(&self) -> Option<&HttpRequest> {
|
||||||
pub fn request(&self) -> Option<&HttpRequest> {
|
|
||||||
self.request.as_ref()
|
self.request.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve el tema que se usará para renderizar el documento.
|
fn theme(&self) -> ThemeRef {
|
||||||
pub fn theme(&self) -> ThemeRef {
|
|
||||||
self.theme
|
self.theme
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la composición para renderizar el documento. Por defecto es `"default"`.
|
fn layout(&self) -> &str {
|
||||||
pub fn layout(&self) -> &str {
|
|
||||||
self.layout
|
self.layout
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context RENDER ******************************************************************************
|
/// Recupera un parámetro como [`Option`], simplificando el acceso.
|
||||||
|
|
||||||
/// Renderiza los recursos del contexto.
|
|
||||||
pub fn render_assets(&self) -> Markup {
|
|
||||||
html! {
|
|
||||||
@if let Some(favicon) = &self.favicon {
|
|
||||||
(favicon)
|
|
||||||
}
|
|
||||||
(self.stylesheets)
|
|
||||||
(self.javascripts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context PARAMS ******************************************************************************
|
|
||||||
|
|
||||||
/// Añade o modifica un parámetro del contexto almacenando el valor como [`String`].
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_param<T: ToString>(mut self, key: &'static str, value: T) -> Self {
|
|
||||||
self.params.insert(key, value.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
/// A diferencia de [`get_param`](Self::get_param), que devuelve un [`Result`] con información
|
||||||
/// ([`ErrorParam::ParseError`]).
|
/// detallada de error, este método devuelve `None` tanto si la clave no existe como si el valor
|
||||||
pub fn get_param<T: FromStr>(&self, key: &'static str) -> Result<T, ErrorParam> {
|
/// guardado no coincide con el tipo solicitado.
|
||||||
self.params
|
///
|
||||||
.get(key)
|
/// Resulta útil en escenarios donde sólo interesa saber si el valor existe y es del tipo
|
||||||
.ok_or(ErrorParam::NotFound)
|
/// correcto, sin necesidad de diferenciar entre error de ausencia o de tipo.
|
||||||
.and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone())))
|
///
|
||||||
|
/// # 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::<String>("username").map(|s| s.as_str()), Some("Alice"));
|
||||||
|
///
|
||||||
|
/// // Devuelve None si no existe o si el tipo no coincide.
|
||||||
|
/// assert!(cx.param::<i32>("username").is_none());
|
||||||
|
/// assert!(cx.param::<String>("missing").is_none());
|
||||||
|
///
|
||||||
|
/// // Acceso con valor por defecto.
|
||||||
|
/// let user = cx.param::<String>("missing")
|
||||||
|
/// .cloned()
|
||||||
|
/// .unwrap_or_else(|| "visitor".to_string());
|
||||||
|
/// assert_eq!(user, "visitor");
|
||||||
|
/// ```
|
||||||
|
fn param<T: 'static>(&self, key: &'static str) -> Option<&T> {
|
||||||
|
self.get_param::<T>(key).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó.
|
fn favicon(&self) -> Option<&Favicon> {
|
||||||
pub fn remove_param(&mut self, key: &'static str) -> bool {
|
self.favicon.as_ref()
|
||||||
self.params.remove(key).is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context EXTRAS ******************************************************************************
|
fn stylesheets(&self) -> &Assets<StyleSheet> {
|
||||||
|
&self.stylesheets
|
||||||
|
}
|
||||||
|
|
||||||
/// Genera un identificador único si no se proporciona uno explícito.
|
fn javascripts(&self) -> &Assets<JavaScript> {
|
||||||
|
&self.javascripts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contextual HELPERS **************************************************************************
|
||||||
|
|
||||||
|
/// Devuelve un identificador único dentro del contexto para el tipo `T`, si no se proporciona
|
||||||
|
/// un `id` explícito.
|
||||||
///
|
///
|
||||||
/// Si no se proporciona un `id`, se genera un identificador único en la forma `<tipo>-<número>`
|
/// Si no se proporciona un `id`, se genera un identificador único en la forma `<tipo>-<número>`
|
||||||
/// donde `<tipo>` es el nombre corto del tipo en minúsculas (sin espacios) y `<número>` es un
|
/// donde `<tipo>` es el nombre corto del tipo en minúsculas (sin espacios) y `<número>` es un
|
||||||
/// contador interno incremental.
|
/// contador interno incremental.
|
||||||
pub fn required_id<T>(&mut self, id: Option<String>) -> String {
|
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
id
|
id
|
||||||
} else {
|
} else {
|
||||||
|
@ -281,7 +548,7 @@ impl Context {
|
||||||
.replace(' ', "_")
|
.replace(' ', "_")
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
let prefix = if prefix.is_empty() {
|
let prefix = if prefix.is_empty() {
|
||||||
"prefix".to_owned()
|
"prefix".to_string()
|
||||||
} else {
|
} else {
|
||||||
prefix
|
prefix
|
||||||
};
|
};
|
||||||
|
@ -290,21 +557,3 @@ impl 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -69,23 +69,6 @@ impl fmt::Write for Escaper<'_> {
|
||||||
/// `.render()` or `.render_to()`. Since the default definitions of
|
/// `.render()` or `.render_to()`. Since the default definitions of
|
||||||
/// these methods call each other, not doing this will result in
|
/// these methods call each other, not doing this will result in
|
||||||
/// infinite recursion.
|
/// 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 {
|
pub trait Render {
|
||||||
/// Renders `self` as a block of `Markup`.
|
/// Renders `self` as a block of `Markup`.
|
||||||
fn render(&self) -> Markup {
|
fn render(&self) -> Markup {
|
||||||
|
@ -238,6 +221,10 @@ impl Markup {
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.0.is_empty()
|
self.0.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Into<String>> PreEscaped<T> {
|
impl<T: Into<String>> PreEscaped<T> {
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
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<Typed<C>>` 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<C: Component>(Option<Typed<C>>);
|
|
||||||
|
|
||||||
impl<C: Component> Default for OptionComponent<C> {
|
|
||||||
fn default() -> Self {
|
|
||||||
OptionComponent(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Component> OptionComponent<C> {
|
|
||||||
/// 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<C>) -> 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<Typed<C>> {
|
|
||||||
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! {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
use crate::{builder_fn, AutoDefault};
|
|
||||||
|
|
||||||
/// Identificador normalizado para el atributo `id` o similar de HTML.
|
|
||||||
///
|
|
||||||
/// Este tipo encapsula `Option<String>` 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<String>);
|
|
||||||
|
|
||||||
impl OptionId {
|
|
||||||
/// Crea un nuevo [`OptionId`].
|
|
||||||
///
|
|
||||||
/// El valor se normaliza automáticamente.
|
|
||||||
pub fn new(value: impl AsRef<str>) -> 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<str>) -> 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<String> {
|
|
||||||
if let Some(value) = &self.0 {
|
|
||||||
if !value.is_empty() {
|
|
||||||
return Some(value.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
use crate::{builder_fn, AutoDefault};
|
|
||||||
|
|
||||||
/// Nombre normalizado para el atributo `name` o similar de HTML.
|
|
||||||
///
|
|
||||||
/// Este tipo encapsula `Option<String>` 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<String>);
|
|
||||||
|
|
||||||
impl OptionName {
|
|
||||||
/// Crea un nuevo [`OptionName`].
|
|
||||||
///
|
|
||||||
/// El valor se normaliza automáticamente.
|
|
||||||
pub fn new(value: impl AsRef<str>) -> 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<str>) -> 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<String> {
|
|
||||||
if let Some(value) = &self.0 {
|
|
||||||
if !value.is_empty() {
|
|
||||||
return Some(value.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
use crate::{builder_fn, AutoDefault};
|
|
||||||
|
|
||||||
/// Cadena normalizada para renderizar en atributos HTML.
|
|
||||||
///
|
|
||||||
/// Este tipo encapsula `Option<String>` 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<String>);
|
|
||||||
|
|
||||||
impl OptionString {
|
|
||||||
/// Crea un nuevo [`OptionString`].
|
|
||||||
///
|
|
||||||
/// El valor se normaliza automáticamente.
|
|
||||||
pub fn new(value: impl AsRef<str>) -> 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<str>) -> 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<String> {
|
|
||||||
if let Some(value) = &self.0 {
|
|
||||||
if !value.is_empty() {
|
|
||||||
return Some(value.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
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<String> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
24
src/lib.rs
|
@ -15,8 +15,8 @@
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
`PageTop` reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para
|
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
||||||
la creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
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
|
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
|
||||||
según las necesidades de cada proyecto, incluyendo:
|
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,
|
* **Componentes** (*components*): encapsulan HTML, CSS y JavaScript en unidades funcionales,
|
||||||
configurables y reutilizables.
|
configurables y reutilizables.
|
||||||
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
|
* **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
|
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
||||||
componentes sin comprometer su funcionalidad.
|
componentes sin comprometer su funcionalidad.
|
||||||
|
|
||||||
|
|
||||||
# ⚡️ Guía rápida
|
# ⚡️ 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
|
```rust,no_run
|
||||||
use pagetop::prelude::*;
|
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
|
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
|
||||||
de bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
|
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
|
```rust,no_run
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
@ -60,8 +60,8 @@ impl Extension for HelloWorld {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
Page::new(Some(request))
|
Page::new(request)
|
||||||
.with_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
|
.add_component(Html::with(move |_| html! { h1 { "Hello World!" } }))
|
||||||
.render()
|
.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,11 +77,11 @@ Este programa implementa una extensión llamada `HelloWorld` que sirve una pági
|
||||||
|
|
||||||
# 🧩 Gestión de Dependencias
|
# 🧩 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.
|
proyecto en Rust.
|
||||||
|
|
||||||
Sin embargo, es fundamental que cada extensión declare explícitamente sus
|
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.
|
estructurar e inicializar la aplicación de forma modular.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ pub type Weight = i8;
|
||||||
|
|
||||||
// API *********************************************************************************************
|
// API *********************************************************************************************
|
||||||
|
|
||||||
// Funciones y macros útiles.
|
// Macros y funciones útiles.
|
||||||
pub mod util;
|
pub mod util;
|
||||||
// Carga las opciones de configuración.
|
// Carga las opciones de configuración.
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
102
src/locale.rs
|
@ -1,6 +1,6 @@
|
||||||
//! Localización (L10n).
|
//! 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/)
|
//! 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.
|
//! para integrar los recursos de traducción directamente en el binario de la aplicación.
|
||||||
//!
|
//!
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
//!
|
//!
|
||||||
//! # Recursos Fluent
|
//! # 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)
|
//! [Identificador de Idioma Unicode](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
|
||||||
//! válido. Podríamos tener una estructura como esta:
|
//! válido. Podríamos tener una estructura como esta:
|
||||||
//!
|
//!
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
//! └── main.ftl
|
//! └── 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
|
//! ```text
|
||||||
//! hello-world = Hello world!
|
//! hello-world = Hello world!
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
//! Y su archivo equivalente para español en `src/locale/es-ES/main.ftl`:
|
//! Y su archivo equivalente para español en `src/locale/es-ES/main.ftl`:
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! hello-world = Hola mundo!
|
//! hello-world = ¡Hola, mundo!
|
||||||
//! hello-user = ¡Hola, {$userName}!
|
//! hello-user = ¡Hola, {$userName}!
|
||||||
//! shared-photos =
|
//! shared-photos =
|
||||||
//! {$userName} {$photoCount ->
|
//! {$userName} {$photoCount ->
|
||||||
|
@ -81,13 +81,13 @@
|
||||||
//! include_locales!(LOCALES_SAMPLE);
|
//! 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
|
//! ```rust,ignore
|
||||||
//! include_locales!(LOCALES_SAMPLE from "ruta/a/las/traducciones");
|
//! 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`].
|
//! traducir textos con [`L10n`].
|
||||||
|
|
||||||
use crate::html::{Markup, PreEscaped};
|
use crate::html::{Markup, PreEscaped};
|
||||||
|
@ -129,7 +129,7 @@ pub(crate) static FALLBACK_LANGID: LazyLock<LanguageIdentifier> =
|
||||||
// Identificador de idioma **por defecto** para la aplicación.
|
// Identificador de idioma **por defecto** para la aplicación.
|
||||||
//
|
//
|
||||||
// Se resuelve a partir de [`global::SETTINGS.app.language`](global::SETTINGS). Si el identificador
|
// Se resuelve a partir de [`global::SETTINGS.app.language`](global::SETTINGS). Si el identificador
|
||||||
// de idioma no es válido o no está disponible entonces resuelve como [`FALLBACK_LANGID`].
|
// de idioma no es válido o no está disponible, se usa [`FALLBACK_LANGID`].
|
||||||
pub(crate) static DEFAULT_LANGID: LazyLock<Option<&LanguageIdentifier>> =
|
pub(crate) static DEFAULT_LANGID: LazyLock<Option<&LanguageIdentifier>> =
|
||||||
LazyLock::new(|| LangMatch::resolve(&global::SETTINGS.app.language).as_option());
|
LazyLock::new(|| LangMatch::resolve(&global::SETTINGS.app.language).as_option());
|
||||||
|
|
||||||
|
@ -141,10 +141,10 @@ pub trait LangId {
|
||||||
fn langid(&self) -> &'static LanguageIdentifier;
|
fn langid(&self) -> &'static LanguageIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Operaciones con los idiomas soportados por `PageTop`.
|
/// Operaciones con los idiomas soportados por PageTop.
|
||||||
///
|
///
|
||||||
/// Utiliza [`LangMatch`] para transformar un identificador de idioma en un [`LanguageIdentifier`]
|
/// Utiliza [`LangMatch`] para transformar un identificador de idioma en un [`LanguageIdentifier`]
|
||||||
/// soportado por `PageTop`.
|
/// soportado por PageTop.
|
||||||
///
|
///
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
|
@ -155,7 +155,7 @@ pub trait LangId {
|
||||||
/// let lang = LangMatch::resolve("es-ES");
|
/// let lang = LangMatch::resolve("es-ES");
|
||||||
/// assert_eq!(lang.langid().to_string(), "es-ES");
|
/// assert_eq!(lang.langid().to_string(), "es-ES");
|
||||||
///
|
///
|
||||||
/// // Coincidencia parcial (con el idioma base).
|
/// // Coincidencia parcial (retrocede al idioma base si no hay variante regional).
|
||||||
/// let lang = LangMatch::resolve("es-EC");
|
/// let lang = LangMatch::resolve("es-EC");
|
||||||
/// assert_eq!(lang.langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado.
|
/// assert_eq!(lang.langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado.
|
||||||
///
|
///
|
||||||
|
@ -165,7 +165,7 @@ pub trait LangId {
|
||||||
///
|
///
|
||||||
/// // Idioma no soportado.
|
/// // Idioma no soportado.
|
||||||
/// let lang = LangMatch::resolve("ja-JP");
|
/// let lang = LangMatch::resolve("ja-JP");
|
||||||
/// assert_eq!(lang, LangMatch::Unsupported(String::from("ja-JP")));
|
/// assert_eq!(lang, LangMatch::Unsupported("ja-JP".to_string()));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Con la siguiente instrucción siempre se obtiene un [`LanguageIdentifier`] válido, ya sea porque
|
/// Con la siguiente instrucción siempre se obtiene un [`LanguageIdentifier`] válido, ya sea porque
|
||||||
|
@ -183,11 +183,11 @@ pub trait LangId {
|
||||||
pub enum LangMatch {
|
pub enum LangMatch {
|
||||||
/// Cuando el identificador de idioma es una cadena vacía.
|
/// Cuando el identificador de idioma es una cadena vacía.
|
||||||
Unspecified,
|
Unspecified,
|
||||||
/// Si encuentra un [`LanguageIdentifier`] en la lista de idiomas soportados por `PageTop` que
|
/// Si encuentra un [`LanguageIdentifier`] en la lista de idiomas soportados por PageTop que
|
||||||
/// coincide exactamente con el identificador de idioma (p.ej. "es-ES"), o con el identificador
|
/// coincide exactamente con el identificador de idioma (p.ej. "es-ES"), o con el identificador
|
||||||
/// del idioma base (p.ej. "es").
|
/// del idioma base (p.ej. "es").
|
||||||
Found(&'static LanguageIdentifier),
|
Found(&'static LanguageIdentifier),
|
||||||
/// Si el identificador de idioma no está entre los soportados por `PageTop`.
|
/// Si el identificador de idioma no está entre los soportados por PageTop.
|
||||||
Unsupported(String),
|
Unsupported(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,8 +221,8 @@ impl LangMatch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// En otro caso indica que el idioma no está soportado.
|
// En caso contrario, indica que el idioma no está soportado.
|
||||||
Self::Unsupported(String::from(language))
|
Self::Unsupported(language.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido.
|
/// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido.
|
||||||
|
@ -241,7 +241,7 @@ impl LangMatch {
|
||||||
/// let lang = LangMatch::resolve("es-ES").as_option();
|
/// let lang = LangMatch::resolve("es-ES").as_option();
|
||||||
/// assert_eq!(lang.unwrap().to_string(), "es-ES");
|
/// assert_eq!(lang.unwrap().to_string(), "es-ES");
|
||||||
///
|
///
|
||||||
/// let lang = LangMatch::resolve("jp-JP").as_option();
|
/// let lang = LangMatch::resolve("ja-JP").as_option();
|
||||||
/// assert!(lang.is_none());
|
/// assert!(lang.is_none());
|
||||||
/// ```
|
/// ```
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -259,8 +259,8 @@ impl LangMatch {
|
||||||
/// devuelve el idioma por defecto de la aplicación y, si tampoco está disponible, el idioma de
|
/// devuelve el idioma por defecto de la aplicación y, si tampoco está disponible, el idioma de
|
||||||
/// respaldo ("en-US").
|
/// respaldo ("en-US").
|
||||||
///
|
///
|
||||||
/// Resulta útil para usar un valor de [`LangMatch`] como fuente de traducción en [`L10n::using()`]
|
/// Resulta útil para usar un valor de [`LangMatch`] como fuente de traducción en [`L10n::lookup()`]
|
||||||
/// o [`L10n::to_markup()`].
|
/// o [`L10n::using()`].
|
||||||
impl LangId for LangMatch {
|
impl LangId for LangMatch {
|
||||||
fn langid(&self) -> &'static LanguageIdentifier {
|
fn langid(&self) -> &'static LanguageIdentifier {
|
||||||
match self {
|
match self {
|
||||||
|
@ -271,10 +271,10 @@ impl LangId for LangMatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
/// Define un conjunto de elementos de localización y textos de traducción local.
|
/// Incluye un conjunto de recursos **Fluent** y textos de traducción propios.
|
||||||
macro_rules! include_locales {
|
macro_rules! include_locales {
|
||||||
// Se eliminan las marcas de aislamiento Unicode en los argumentos para mejorar la legibilidad y
|
// Se desactiva la inserción de marcas de aislamiento Unicode (FSI/PDI) en los argumentos para
|
||||||
// la compatibilidad en ciertos contextos de renderizado.
|
// mejorar la legibilidad y la compatibilidad en ciertos contextos de renderizado.
|
||||||
( $LOCALES:ident $(, $core_locales:literal)? ) => {
|
( $LOCALES:ident $(, $core_locales:literal)? ) => {
|
||||||
$crate::locale::fluent_templates::static_loader! {
|
$crate::locale::fluent_templates::static_loader! {
|
||||||
static $LOCALES = {
|
static $LOCALES = {
|
||||||
|
@ -310,8 +310,8 @@ include_locales!(LOCALES_PAGETOP);
|
||||||
enum L10nOp {
|
enum L10nOp {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
Text(String),
|
Text(Cow<'static, str>),
|
||||||
Translate(String),
|
Translate(Cow<'static, str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea instancias para traducir textos localizados.
|
/// Crea instancias para traducir textos localizados.
|
||||||
|
@ -319,12 +319,12 @@ enum L10nOp {
|
||||||
/// Cada instancia puede representar:
|
/// Cada instancia puede representar:
|
||||||
///
|
///
|
||||||
/// - Un texto puro (`n()`) que no requiere traducción.
|
/// - Un texto puro (`n()`) que no requiere traducción.
|
||||||
/// - Una clave para traducir un texto de las traducciones predefinidas de `PageTop` (`l()`).
|
/// - Una clave para traducir un texto de las traducciones predefinidas de PageTop (`l()`).
|
||||||
/// - Una clave para traducir de un conjunto concreto de traducciones (`t()`).
|
/// - Una clave para traducir de un conjunto concreto de traducciones (`t()`).
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// Los argumentos dinámicos se añaden usando `with_arg()` o `with_args()`.
|
/// Los argumentos dinámicos se añaden con `with_arg()` o `with_args()`.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
|
@ -338,11 +338,11 @@ enum L10nOp {
|
||||||
/// .get();
|
/// .get();
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// También para traducciones a idiomas concretos.
|
/// También sirve para traducciones contra un conjunto de recursos concreto.
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// // Traducción con clave, conjunto de traducciones y fuente de idioma.
|
/// // Traducción con clave, conjunto de traducciones y fuente de idioma.
|
||||||
/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(&LangMatch::resolve("it"));
|
/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).lookup(&LangMatch::resolve("it"));
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone)]
|
#[derive(AutoDefault, Clone)]
|
||||||
pub struct L10n {
|
pub struct L10n {
|
||||||
|
@ -354,7 +354,7 @@ pub struct L10n {
|
||||||
|
|
||||||
impl L10n {
|
impl L10n {
|
||||||
/// **n** = *“native”*. Crea una instancia con una cadena literal sin traducción.
|
/// **n** = *“native”*. Crea una instancia con una cadena literal sin traducción.
|
||||||
pub fn n(text: impl Into<String>) -> Self {
|
pub fn n(text: impl Into<Cow<'static, str>>) -> Self {
|
||||||
L10n {
|
L10n {
|
||||||
op: L10nOp::Text(text.into()),
|
op: L10nOp::Text(text.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -363,7 +363,7 @@ impl L10n {
|
||||||
|
|
||||||
/// **l** = *“lookup”*. Crea una instancia para traducir usando una clave del conjunto de
|
/// **l** = *“lookup”*. Crea una instancia para traducir usando una clave del conjunto de
|
||||||
/// traducciones predefinidas.
|
/// traducciones predefinidas.
|
||||||
pub fn l(key: impl Into<String>) -> Self {
|
pub fn l(key: impl Into<Cow<'static, str>>) -> Self {
|
||||||
L10n {
|
L10n {
|
||||||
op: L10nOp::Translate(key.into()),
|
op: L10nOp::Translate(key.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -372,7 +372,7 @@ impl L10n {
|
||||||
|
|
||||||
/// **t** = *“translate”*. Crea una instancia para traducir usando una clave de un conjunto de
|
/// **t** = *“translate”*. Crea una instancia para traducir usando una clave de un conjunto de
|
||||||
/// traducciones específico.
|
/// traducciones específico.
|
||||||
pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self {
|
pub fn t(key: impl Into<Cow<'static, str>>, locales: &'static Locales) -> Self {
|
||||||
L10n {
|
L10n {
|
||||||
op: L10nOp::Translate(key.into()),
|
op: L10nOp::Translate(key.into()),
|
||||||
locales,
|
locales,
|
||||||
|
@ -399,7 +399,8 @@ impl L10n {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resuelve la traducción usando el idioma por defecto o de respaldo de la aplicación.
|
/// Resuelve la traducción usando el idioma por defecto o, si no procede, el de respaldo de la
|
||||||
|
/// aplicación.
|
||||||
///
|
///
|
||||||
/// Devuelve `None` si no aplica o no encuentra una traducción válida.
|
/// Devuelve `None` si no aplica o no encuentra una traducción válida.
|
||||||
///
|
///
|
||||||
|
@ -411,7 +412,7 @@ impl L10n {
|
||||||
/// let text = L10n::l("greeting").with_arg("name", "Manuel").get();
|
/// let text = L10n::l("greeting").with_arg("name", "Manuel").get();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn get(&self) -> Option<String> {
|
pub fn get(&self) -> Option<String> {
|
||||||
self.using(&LangMatch::default())
|
self.lookup(&LangMatch::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resuelve la traducción usando la fuente de idioma proporcionada.
|
/// Resuelve la traducción usando la fuente de idioma proporcionada.
|
||||||
|
@ -432,20 +433,27 @@ impl L10n {
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// let r = ResourceLang;
|
/// let r = ResourceLang;
|
||||||
/// let text = L10n::l("greeting").with_arg("name", "Usuario").using(&r);
|
/// let text = L10n::l("greeting").with_arg("name", "Usuario").lookup(&r);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn using(&self, language: &impl LangId) -> Option<String> {
|
pub fn lookup(&self, language: &impl LangId) -> Option<String> {
|
||||||
match &self.op {
|
match &self.op {
|
||||||
L10nOp::None => None,
|
L10nOp::None => None,
|
||||||
L10nOp::Text(text) => Some(text.to_owned()),
|
L10nOp::Text(text) => Some(text.clone().into_owned()),
|
||||||
L10nOp::Translate(key) => self.locales.try_lookup_with_args(
|
L10nOp::Translate(key) => {
|
||||||
|
if self.args.is_empty() {
|
||||||
|
self.locales.try_lookup(language.langid(), key.as_ref())
|
||||||
|
} else {
|
||||||
|
self.locales.try_lookup_with_args(
|
||||||
language.langid(),
|
language.langid(),
|
||||||
key,
|
key.as_ref(),
|
||||||
&self.args.iter().fold(HashMap::new(), |mut arg, (k, v)| {
|
&self
|
||||||
arg.insert(Cow::Owned(k.clone()), v.to_owned().into());
|
.args
|
||||||
arg
|
.iter()
|
||||||
}),
|
.map(|(k, v)| (Cow::Owned(k.clone()), v.clone().into()))
|
||||||
),
|
.collect::<HashMap<_, _>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,10 +466,16 @@ impl L10n {
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// let html = L10n::l("welcome.message").to_markup(&LangMatch::resolve("es"));
|
/// let html = L10n::l("welcome.message").using(&LangMatch::resolve("es"));
|
||||||
/// ```
|
/// ```
|
||||||
|
pub fn using(&self, language: &impl LangId) -> Markup {
|
||||||
|
PreEscaped(self.lookup(language).unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Obsoleto desde la versión 0.4.0**: usar [`using()`](Self::using) en su lugar.
|
||||||
|
#[deprecated(since = "0.4.0", note = "Use `using()` instead")]
|
||||||
pub fn to_markup(&self, language: &impl LangId) -> Markup {
|
pub fn to_markup(&self, language: &impl LangId) -> Markup {
|
||||||
PreEscaped(self.using(language).unwrap_or_default())
|
self.using(language)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
13
src/locale/en-US/base.ftl
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Basic theme, intro layout.
|
||||||
|
intro_pagetop_label = PageTop version on Crates.io
|
||||||
|
intro_release_label = Release date
|
||||||
|
intro_license_label = License
|
||||||
|
|
||||||
|
intro_text1 = PageTop is <strong>an opinionated Rust web development framework</strong> designed to build modular, extensible, and configurable web solutions.
|
||||||
|
intro_text2 = PageTop brings back the essence of the classic web, renders on the server (SSR) and uses <em>HTML-first</em> components, CSS and JavaScript, <strong>with the performance and security of Rust</strong>.
|
||||||
|
|
||||||
|
intro_code = Code
|
||||||
|
intro_have_fun = Coding is creating
|
||||||
|
|
||||||
|
# PoweredBy component.
|
||||||
|
poweredby_pagetop = Powered by { $pagetop_link }
|
|
@ -1,2 +1,9 @@
|
||||||
content = Content
|
# Regions.
|
||||||
|
region_header = Header
|
||||||
|
region_content = Content
|
||||||
|
region_footer = Footer
|
||||||
|
|
||||||
|
error403_notice = FORBIDDEN ACCESS
|
||||||
|
error404_notice = RESOURCE NOT FOUND
|
||||||
|
|
||||||
pagetop_logo = PageTop Logo
|
pagetop_logo = PageTop Logo
|
||||||
|
|
|
@ -1,21 +1,16 @@
|
||||||
welcome_extension_name = Default homepage
|
welcome_extension_name = Default Homepage
|
||||||
welcome_extension_description = Displays a landing page when none is configured.
|
welcome_extension_description = Displays a default homepage when none is configured.
|
||||||
|
|
||||||
welcome_page = Welcome Page
|
welcome_page = Welcome page
|
||||||
welcome_title = Hello world!
|
welcome_title = Hello, world!
|
||||||
welcome_aria = Say hello to your { $app } installation
|
|
||||||
|
|
||||||
welcome_intro = Discover⚡{ $app }
|
welcome_intro = Discover⚡{ $app }
|
||||||
welcome_powered = A web solution powered by <strong>PageTop!</strong>
|
welcome_powered = A web solution powered by <strong>PageTop</strong>
|
||||||
|
|
||||||
welcome_text1 = If you can read this page, it means that the <strong>PageTop</strong> server is running correctly but has not yet been fully configured. This usually means the site is either experiencing temporary issues or is undergoing routine maintenance.
|
welcome_status_title = Status
|
||||||
welcome_text2 = If the issue persists, please <strong>contact your system administrator</strong> for assistance.
|
welcome_status_1 = If you can see this page, it means the <strong>PageTop</strong> server is running correctly, but the application is not fully configured. This may be due to routine maintenance or a temporary issue.
|
||||||
|
welcome_status_2 = If the issue persists, please <strong>contact the system administrator</strong>.
|
||||||
|
|
||||||
welcome_about = About
|
welcome_support_title = Support
|
||||||
welcome_pagetop = <strong>PageTop</strong> is a <a href="https://www.rust-lang.org" target="_blank">Rust</a>-based web development framework for building modular, extensible, and configurable web solutions.
|
welcome_support_1 = To report issues with the <strong>PageTop</strong> framework, use <a href="https://git.cillero.es/manuelcillero/pagetop/issues" target="_blank" rel="noreferrer">SoloGit</a>. Remember, before opening a new issue, review the existing ones to avoid duplicates.
|
||||||
|
welcome_support_2 = For issues specific to the application (<strong>{ $app }</strong>), please use its official repository or support channel.
|
||||||
welcome_issues1 = To report issues related to the <strong>PageTop</strong> framework, please use <a href="https://git.cillero.es/manuelcillero/pagetop/issues" target="_blank">SoloGit</a>. Before opening a new issue, check existing reports to avoid duplicates.
|
|
||||||
welcome_issues2 = For issues related specifically to <strong>{ $app }</strong>, please refer to its official repository or support channel, rather than directly to <strong>PageTop</strong>.
|
|
||||||
|
|
||||||
welcome_code = Code
|
|
||||||
welcome_have_fun = Coding is creating
|
|
||||||
|
|
13
src/locale/es-ES/base.ftl
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Basic theme, intro layout.
|
||||||
|
intro_pagetop_label = Versión de PageTop en Crates.io
|
||||||
|
intro_release_label = Lanzamiento
|
||||||
|
intro_license_label = Licencia
|
||||||
|
|
||||||
|
intro_text1 = PageTop es un <strong>entorno de desarrollo web basado en Rust</strong>, pensado para construir soluciones web modulares, extensibles y configurables.
|
||||||
|
intro_text2 = PageTop reivindica la esencia de la web clásica, renderiza en el servidor (SSR) utilizando componentes <em>HTML-first</em>, CSS y JavaScript, <strong>con el rendimiento y la seguridad de Rust</strong>.
|
||||||
|
|
||||||
|
intro_code = Código
|
||||||
|
intro_have_fun = Programar es crear
|
||||||
|
|
||||||
|
# PoweredBy component.
|
||||||
|
poweredby_pagetop = Funciona con { $pagetop_link }
|
|
@ -1,2 +1,9 @@
|
||||||
content = Contenido
|
# Regions.
|
||||||
|
region_header = Cabecera
|
||||||
|
region_content = Contenido
|
||||||
|
region_footer = Pie de página
|
||||||
|
|
||||||
|
error403_notice = ACCESO NO PERMITIDO
|
||||||
|
error404_notice = RECURSO NO ENCONTRADO
|
||||||
|
|
||||||
pagetop_logo = Logotipo de PageTop
|
pagetop_logo = Logotipo de PageTop
|
||||||
|
|
|
@ -1,21 +1,16 @@
|
||||||
welcome_extension_name = Página de inicio predeterminada
|
welcome_extension_name = Página de inicio predeterminada
|
||||||
welcome_extension_description = Muestra una página de inicio predeterminada cuando no hay ninguna configurada.
|
welcome_extension_description = Muestra una página de inicio predeterminada cuando no hay ninguna configurada.
|
||||||
|
|
||||||
welcome_page = Página de Bienvenida
|
welcome_page = Página de bienvenida
|
||||||
welcome_title = ¡Hola mundo!
|
welcome_title = ¡Hola, mundo!
|
||||||
welcome_aria = Saluda a tu instalación { $app }
|
|
||||||
|
|
||||||
welcome_intro = Descubre⚡{ $app }
|
welcome_intro = Descubre⚡{ $app }
|
||||||
welcome_powered = Una solución web creada con <strong>PageTop!</strong>
|
welcome_powered = Una solución web creada con <strong>PageTop</strong>
|
||||||
|
|
||||||
welcome_text1 = Si puedes leer esta página, significa que el servidor de <strong>PageTop</strong> funciona correctamente, pero aún no ha sido completamente configurado. Esto suele indicar que el sitio está experimentando problemas temporales o está pasando por un mantenimiento de rutina.
|
welcome_status_title = Estado
|
||||||
welcome_text2 = Si el problema persiste, por favor <strong>contacta con el administrador del sistema</strong> para recibir asistencia técnica.
|
welcome_status_1 = Si puedes ver esta página, es porque el servidor de <strong>PageTop</strong> está funcionando correctamente, pero la aplicación no está completamente configurada. Esto puede deberse a tareas de mantenimiento o a una incidencia temporal.
|
||||||
|
welcome_status_2 = Si el problema persiste, por favor, <strong>contacta con el administrador del sistema</strong>.
|
||||||
|
|
||||||
welcome_about = Acerca de
|
welcome_support_title = Soporte
|
||||||
welcome_pagetop = <strong>PageTop</strong> es un entorno de desarrollo web basado en <a href="https://www.rust-lang.org/es" target="_blank">Rust</a>, diseñado para crear soluciones web modulares, extensibles y configurables.
|
welcome_support_1 = Para comunicar incidencias del propio entorno <strong>PageTop</strong>, utiliza <a href="https://git.cillero.es/manuelcillero/pagetop/issues" target="_blank" rel="noreferrer">SoloGit</a>. Recuerda, antes de abrir una nueva incidencia, revisa las existentes para evitar duplicados.
|
||||||
|
welcome_support_2 = Para fallos específicos de la aplicación (<strong>{ $app }</strong>), utiliza su repositorio oficial o su canal de soporte.
|
||||||
welcome_issues1 = Para comunicar cualquier problema con <strong>PageTop</strong>, utiliza <a href="https://git.cillero.es/manuelcillero/pagetop/issues" target="_blank">SoloGit</a>. Antes de informar de una incidencia, revisa los informes ya existentes para evitar duplicados.
|
|
||||||
welcome_issues2 = Si se trata de fallos específicos de <strong>{ $app }</strong>, por favor acude a su repositorio oficial o canal de soporte, y no al de <strong>PageTop</strong> directamente.
|
|
||||||
|
|
||||||
welcome_code = Código
|
|
||||||
welcome_have_fun = Programar es crear
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! *Prelude* de `PageTop`.
|
//! *Prelude* de PageTop.
|
||||||
|
|
||||||
// RE-EXPORTED.
|
// RE-EXPORTED.
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,16 @@ pub use error::ErrorPage;
|
||||||
pub use actix_web::Result as ResultPage;
|
pub use actix_web::Result as ResultPage;
|
||||||
|
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
use crate::builder_fn;
|
|
||||||
use crate::core::component::{Child, ChildOp, Component};
|
use crate::core::component::{Child, ChildOp, Component};
|
||||||
use crate::core::theme::{ChildrenInRegions, ThemeRef, CONTENT_REGION_NAME};
|
use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT};
|
||||||
use crate::html::{html, AssetsOp, Context, Markup, DOCTYPE};
|
use crate::html::{html, Markup, DOCTYPE};
|
||||||
use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
|
use crate::html::{AssetsOp, Context, Contextual};
|
||||||
|
use crate::html::{AttrClasses, ClassesOp};
|
||||||
|
use crate::html::{AttrId, AttrL10n};
|
||||||
use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
|
use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
|
||||||
use crate::service::HttpRequest;
|
use crate::service::HttpRequest;
|
||||||
|
use crate::{builder_fn, AutoDefault};
|
||||||
|
|
||||||
/// Representa una página HTML completa lista para renderizar.
|
/// Representa una página HTML completa lista para renderizar.
|
||||||
///
|
///
|
||||||
|
@ -18,32 +21,33 @@ use crate::service::HttpRequest;
|
||||||
/// regiones donde disponer los componentes, atributos de `<body>` y otros aspectos del contexto de
|
/// regiones donde disponer los componentes, atributos de `<body>` y otros aspectos del contexto de
|
||||||
/// renderizado.
|
/// renderizado.
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
title : OptionTranslated,
|
title : AttrL10n,
|
||||||
description : OptionTranslated,
|
description : AttrL10n,
|
||||||
metadata : Vec<(&'static str, &'static str)>,
|
metadata : Vec<(&'static str, &'static str)>,
|
||||||
properties : Vec<(&'static str, &'static str)>,
|
properties : Vec<(&'static str, &'static str)>,
|
||||||
|
body_id : AttrId,
|
||||||
|
body_classes: AttrClasses,
|
||||||
context : Context,
|
context : Context,
|
||||||
body_id : OptionId,
|
|
||||||
body_classes: OptionClasses,
|
|
||||||
regions : ChildrenInRegions,
|
regions : ChildrenInRegions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
/// Crea una nueva instancia de página.
|
/// Crea una nueva instancia de página.
|
||||||
///
|
///
|
||||||
/// Si se proporciona la solicitud HTTP, se guardará en el contexto de renderizado de la página
|
/// La solicitud HTTP se guardará en el contexto de renderizado de la página para poder ser
|
||||||
/// para poder ser recuperada por los componentes si es necesario.
|
/// recuperada por los componentes si es necesario.
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub fn new(request: Option<HttpRequest>) -> Self {
|
pub fn new(request: HttpRequest) -> Self {
|
||||||
Page {
|
Page {
|
||||||
title : OptionTranslated::default(),
|
title : AttrL10n::default(),
|
||||||
description : OptionTranslated::default(),
|
description : AttrL10n::default(),
|
||||||
metadata : Vec::default(),
|
metadata : Vec::default(),
|
||||||
properties : Vec::default(),
|
properties : Vec::default(),
|
||||||
context : Context::new(request),
|
body_id : AttrId::default(),
|
||||||
body_id : OptionId::default(),
|
body_classes: AttrClasses::default(),
|
||||||
body_classes: OptionClasses::default(),
|
context : Context::new(Some(request)),
|
||||||
regions : ChildrenInRegions::default(),
|
regions : ChildrenInRegions::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,34 +82,6 @@ impl Page {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifica la fuente de idioma de la página ([`Context::with_langid()`]).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_langid(mut self, language: &impl LangId) -> Self {
|
|
||||||
self.context.alter_langid(language);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica el tema que se usará para renderizar la página ([`Context::with_theme()`]).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_theme(mut self, theme_name: &'static str) -> Self {
|
|
||||||
self.context.alter_theme(theme_name);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la composición para renderizar la página ([`Context::with_layout()`]).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_layout(mut self, layout_name: &'static str) -> Self {
|
|
||||||
self.context.alter_layout(layout_name);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define los recursos de la página usando [`AssetsOp`].
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_assets(mut self, op: AssetsOp) -> Self {
|
|
||||||
self.context.alter_assets(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el atributo `id` del elemento `<body>`.
|
/// Establece el atributo `id` del elemento `<body>`.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_body_id(mut self, id: impl AsRef<str>) -> Self {
|
pub fn with_body_id(mut self, id: impl AsRef<str>) -> Self {
|
||||||
|
@ -113,35 +89,65 @@ impl Page {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifica las clases CSS del elemento `<body>` con una operación sobre [`OptionClasses`].
|
/// Modifica las clases CSS del elemento `<body>` con una operación sobre [`AttrClasses`].
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_body_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
pub fn with_body_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||||
self.body_classes.alter_value(op, classes);
|
self.body_classes.alter_value(op, classes);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **Obsoleto desde la versión 0.4.0**: usar [`add_component()`](Self::add_component) en su
|
||||||
|
/// lugar.
|
||||||
|
#[deprecated(since = "0.4.0", note = "Use `add_component()` instead")]
|
||||||
|
pub fn with_component(self, component: impl Component) -> Self {
|
||||||
|
self.add_component(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Obsoleto desde la versión 0.4.0**: usar [`add_component_in()`](Self::add_component_in) en
|
||||||
|
/// su lugar.
|
||||||
|
#[deprecated(since = "0.4.0", note = "Use `add_component_in()` instead")]
|
||||||
|
pub fn with_component_in(self, region_name: &'static str, component: impl Component) -> Self {
|
||||||
|
self.add_component_in(region_name, component)
|
||||||
|
}
|
||||||
|
|
||||||
/// Añade un componente a la región de contenido por defecto.
|
/// Añade un componente a la región de contenido por defecto.
|
||||||
pub fn with_component(mut self, component: impl Component) -> Self {
|
pub fn add_component(mut self, component: impl Component) -> Self {
|
||||||
self.regions
|
self.regions
|
||||||
.alter_child_in_region(CONTENT_REGION_NAME, ChildOp::Add(Child::with(component)));
|
.alter_child_in(REGION_CONTENT, ChildOp::Add(Child::with(component)));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un componente en una región (`region_name`) de la página.
|
/// Añade un componente en una región (`region_name`) de la página.
|
||||||
pub fn with_component_in(
|
pub fn add_component_in(
|
||||||
mut self,
|
mut self,
|
||||||
region_name: &'static str,
|
region_name: &'static str,
|
||||||
component: impl Component,
|
component: impl Component,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.regions
|
self.regions
|
||||||
.alter_child_in_region(region_name, ChildOp::Add(Child::with(component)));
|
.alter_child_in(region_name, ChildOp::Add(Child::with(component)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Obsoleto desde la versión 0.4.0**: usar [`with_child_in()`](Self::with_child_in) en su
|
||||||
|
/// lugar.
|
||||||
|
#[deprecated(since = "0.4.0", note = "Use `with_child_in()` instead")]
|
||||||
|
pub fn with_child_in_region(mut self, region_name: &'static str, op: ChildOp) -> Self {
|
||||||
|
self.alter_child_in(region_name, op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Obsoleto desde la versión 0.4.0**: usar [`alter_child_in()`](Self::alter_child_in) en su
|
||||||
|
/// lugar.
|
||||||
|
#[deprecated(since = "0.4.0", note = "Use `alter_child_in()` instead")]
|
||||||
|
pub fn alter_child_in_region(&mut self, region_name: &'static str, op: ChildOp) -> &mut Self {
|
||||||
|
self.alter_child_in(region_name, op);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opera con [`ChildOp`] en una región (`region_name`) de la página.
|
/// Opera con [`ChildOp`] en una región (`region_name`) de la página.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child_in_region(mut self, region_name: &'static str, op: ChildOp) -> Self {
|
pub fn with_child_in(mut self, region_name: &'static str, op: ChildOp) -> Self {
|
||||||
self.regions.alter_child_in_region(region_name, op);
|
self.regions.alter_child_in(region_name, op);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,12 +155,12 @@ impl Page {
|
||||||
|
|
||||||
/// Devuelve el título traducido para el idioma de la página, si existe.
|
/// Devuelve el título traducido para el idioma de la página, si existe.
|
||||||
pub fn title(&mut self) -> Option<String> {
|
pub fn title(&mut self) -> Option<String> {
|
||||||
self.title.using(&self.context)
|
self.title.lookup(&self.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la descripción traducida para el idioma de la página, si existe.
|
/// Devuelve la descripción traducida para el idioma de la página, si existe.
|
||||||
pub fn description(&mut self) -> Option<String> {
|
pub fn description(&mut self) -> Option<String> {
|
||||||
self.description.using(&self.context)
|
self.description.lookup(&self.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la lista de metadatos `<meta name=...>`.
|
/// Devuelve la lista de metadatos `<meta name=...>`.
|
||||||
|
@ -167,39 +173,28 @@ impl Page {
|
||||||
&self.properties
|
&self.properties
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la solicitud HTTP asociada.
|
|
||||||
pub fn request(&self) -> Option<&HttpRequest> {
|
|
||||||
self.context.request()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el identificador de idioma asociado.
|
|
||||||
pub fn langid(&self) -> &LanguageIdentifier {
|
|
||||||
self.context.langid()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el tema que se usará para renderizar la página.
|
|
||||||
pub fn theme(&self) -> ThemeRef {
|
|
||||||
self.context.theme()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la composición para renderizar la página. Por defecto es `"default"`.
|
|
||||||
pub fn layout(&self) -> &str {
|
|
||||||
self.context.layout()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el identificador del elemento `<body>`.
|
/// Devuelve el identificador del elemento `<body>`.
|
||||||
pub fn body_id(&self) -> &OptionId {
|
pub fn body_id(&self) -> &AttrId {
|
||||||
&self.body_id
|
&self.body_id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve las clases CSS del elemento `<body>`.
|
/// Devuelve las clases CSS del elemento `<body>`.
|
||||||
pub fn body_classes(&self) -> &OptionClasses {
|
pub fn body_classes(&self) -> &AttrClasses {
|
||||||
&self.body_classes
|
&self.body_classes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Devuelve una referencia mutable al [`Context`] de la página.
|
||||||
|
///
|
||||||
|
/// El [`Context`] actúa como intermediario para muchos métodos de `Page` (idioma, tema,
|
||||||
|
/// *layout*, recursos, solicitud HTTP, etc.). Resulta especialmente útil cuando un componente
|
||||||
|
/// o un tema necesita recibir el contexto como parámetro.
|
||||||
|
pub fn context(&mut self) -> &mut Context {
|
||||||
|
&mut self.context
|
||||||
|
}
|
||||||
|
|
||||||
// Page RENDER *********************************************************************************
|
// Page RENDER *********************************************************************************
|
||||||
|
|
||||||
/// Renderiza los componentes de una región (`regiona_name`) de la página.
|
/// Renderiza los componentes de una región (`region_name`) de la página.
|
||||||
pub fn render_region(&mut self, region_name: &'static str) -> Markup {
|
pub fn render_region(&mut self, region_name: &'static str) -> Markup {
|
||||||
self.regions
|
self.regions
|
||||||
.merge_all_components(self.context.theme(), region_name)
|
.merge_all_components(self.context.theme(), region_name)
|
||||||
|
@ -207,7 +202,7 @@ impl Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renderiza los recursos de la página.
|
/// Renderiza los recursos de la página.
|
||||||
pub fn render_assets(&self) -> Markup {
|
pub fn render_assets(&mut self) -> Markup {
|
||||||
self.context.render_assets()
|
self.context.render_assets()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,3 +245,85 @@ impl Page {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LangId for Page {
|
||||||
|
fn langid(&self) -> &'static LanguageIdentifier {
|
||||||
|
self.context.langid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Contextual for Page {
|
||||||
|
// Contextual BUILDER **************************************************************************
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
|
||||||
|
self.context.alter_request(request);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_langid(mut self, language: &impl LangId) -> Self {
|
||||||
|
self.context.alter_langid(language);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_theme(mut self, theme_name: &'static str) -> Self {
|
||||||
|
self.context.alter_theme(theme_name);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_layout(mut self, layout_name: &'static str) -> Self {
|
||||||
|
self.context.alter_layout(layout_name);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
|
||||||
|
self.context.alter_param(key, value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_assets(mut self, op: AssetsOp) -> Self {
|
||||||
|
self.context.alter_assets(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contextual GETTERS **************************************************************************
|
||||||
|
|
||||||
|
fn request(&self) -> Option<&HttpRequest> {
|
||||||
|
self.context.request()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme(&self) -> ThemeRef {
|
||||||
|
self.context.theme()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(&self) -> &str {
|
||||||
|
self.context.layout()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn param<T: 'static>(&self, key: &'static str) -> Option<&T> {
|
||||||
|
self.context.param(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn favicon(&self) -> Option<&Favicon> {
|
||||||
|
self.context.favicon()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stylesheets(&self) -> &Assets<StyleSheet> {
|
||||||
|
self.context.stylesheets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn javascripts(&self) -> &Assets<JavaScript> {
|
||||||
|
self.context.javascripts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contextual HELPERS **************************************************************************
|
||||||
|
|
||||||
|
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
||||||
|
self.context.required_id::<T>(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::base::component::Html;
|
use crate::base::component::Html;
|
||||||
|
use crate::html::Contextual;
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
use crate::response::ResponseError;
|
use crate::response::ResponseError;
|
||||||
use crate::service::http::{header::ContentType, StatusCode};
|
use crate::service::http::{header::ContentType, StatusCode};
|
||||||
|
@ -6,7 +7,7 @@ use crate::service::{HttpRequest, HttpResponse};
|
||||||
|
|
||||||
use super::Page;
|
use super::Page;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ErrorPage {
|
pub enum ErrorPage {
|
||||||
|
@ -19,7 +20,7 @@ pub enum ErrorPage {
|
||||||
Timeout(HttpRequest),
|
Timeout(HttpRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ErrorPage {
|
impl Display for ErrorPage {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
// Error 304.
|
// Error 304.
|
||||||
|
@ -28,12 +29,12 @@ impl fmt::Display for ErrorPage {
|
||||||
ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"),
|
ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"),
|
||||||
// Error 403.
|
// Error 403.
|
||||||
ErrorPage::AccessDenied(request) => {
|
ErrorPage::AccessDenied(request) => {
|
||||||
let mut error_page = Page::new(Some(request.clone()));
|
let mut error_page = Page::new(request.clone());
|
||||||
let error403 = error_page.theme().error403(&mut error_page);
|
let error403 = error_page.theme().error403(&mut error_page);
|
||||||
if let Ok(page) = error_page
|
if let Ok(page) = error_page
|
||||||
.with_title(L10n::n("Error FORBIDDEN"))
|
.with_title(L10n::n("Error FORBIDDEN"))
|
||||||
.with_layout("error")
|
.with_layout("error")
|
||||||
.with_component(Html::with(move |_| error403.clone()))
|
.add_component(Html::with(move |_| error403.clone()))
|
||||||
.render()
|
.render()
|
||||||
{
|
{
|
||||||
write!(f, "{}", page.into_string())
|
write!(f, "{}", page.into_string())
|
||||||
|
@ -43,12 +44,12 @@ impl fmt::Display for ErrorPage {
|
||||||
}
|
}
|
||||||
// Error 404.
|
// Error 404.
|
||||||
ErrorPage::NotFound(request) => {
|
ErrorPage::NotFound(request) => {
|
||||||
let mut error_page = Page::new(Some(request.clone()));
|
let mut error_page = Page::new(request.clone());
|
||||||
let error404 = error_page.theme().error404(&mut error_page);
|
let error404 = error_page.theme().error404(&mut error_page);
|
||||||
if let Ok(page) = error_page
|
if let Ok(page) = error_page
|
||||||
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
|
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
|
||||||
.with_layout("error")
|
.with_layout("error")
|
||||||
.with_component(Html::with(move |_| error404.clone()))
|
.add_component(Html::with(move |_| error404.clone()))
|
||||||
.render()
|
.render()
|
||||||
{
|
{
|
||||||
write!(f, "{}", page.into_string())
|
write!(f, "{}", page.into_string())
|
||||||
|
|
|
@ -17,31 +17,6 @@ pub use actix_web::test;
|
||||||
|
|
||||||
/// **Obsoleto desde la versión 0.3.0**: usar [`static_files_service!`](crate::static_files_service)
|
/// **Obsoleto desde la versión 0.3.0**: usar [`static_files_service!`](crate::static_files_service)
|
||||||
/// en su lugar.
|
/// en su lugar.
|
||||||
///
|
|
||||||
/// Incluye en código un conjunto de recursos previamente preparado con `build.rs`.
|
|
||||||
///
|
|
||||||
/// # Formas de uso
|
|
||||||
///
|
|
||||||
/// * `include_files!(media)` - Para incluir un conjunto de recursos llamado `media`. Normalmente se
|
|
||||||
/// usará esta forma.
|
|
||||||
///
|
|
||||||
/// * `include_files!(BLOG => media)` - También se puede asignar el conjunto de recursos a una
|
|
||||||
/// variable global; p.ej. `BLOG`.
|
|
||||||
///
|
|
||||||
/// # Argumentos
|
|
||||||
///
|
|
||||||
/// * `$bundle` – Nombre del conjunto de recursos generado por `build.rs` (consultar
|
|
||||||
/// [`pagetop_build`](https://docs.rs/pagetop-build)).
|
|
||||||
/// * `$STATIC` – Asigna el conjunto de recursos a una variable global de tipo
|
|
||||||
/// [`StaticResources`](crate::StaticResources).
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// include_files!(assets); // Uso habitual.
|
|
||||||
///
|
|
||||||
/// include_files!(STATIC_ASSETS => assets);
|
|
||||||
/// ```
|
|
||||||
#[deprecated(since = "0.3.0", note = "Use `static_files_service!` instead")]
|
#[deprecated(since = "0.3.0", note = "Use `static_files_service!` instead")]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! include_files {
|
macro_rules! include_files {
|
||||||
|
@ -69,48 +44,6 @@ macro_rules! include_files {
|
||||||
|
|
||||||
/// **Obsoleto desde la versión 0.3.0**: usar [`static_files_service!`](crate::static_files_service)
|
/// **Obsoleto desde la versión 0.3.0**: usar [`static_files_service!`](crate::static_files_service)
|
||||||
/// en su lugar.
|
/// en su lugar.
|
||||||
///
|
|
||||||
/// Configura un servicio web para publicar los recursos embebidos con [`include_files!`].
|
|
||||||
///
|
|
||||||
/// El código expandido de la macro decide durante el arranque de la aplicación si debe servir los
|
|
||||||
/// archivos de los recursos embebidos o directamente desde el sistema de ficheros, si se ha
|
|
||||||
/// indicado una ruta válida a un directorio de recursos.
|
|
||||||
///
|
|
||||||
/// # Argumentos
|
|
||||||
///
|
|
||||||
/// * `$scfg` – Instancia de [`ServiceConfig`](crate::service::web::ServiceConfig) donde aplicar la
|
|
||||||
/// configuración del servicio web.
|
|
||||||
/// * `$bundle` – Nombre del conjunto de recursos incluido con [`include_files!`].
|
|
||||||
/// * `$route` – Ruta URL de origen desde la que se servirán los archivos.
|
|
||||||
/// * `[ $root, $relative ]` *(opcional)* – Directorio raíz y ruta relativa para construir la ruta
|
|
||||||
/// absoluta donde buscar los archivos en el sistema de ficheros (ver
|
|
||||||
/// [`absolute_dir()`](crate::util::absolute_dir)). Si no existe, se usarán los recursos
|
|
||||||
/// embebidos.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// include_files!(assets);
|
|
||||||
///
|
|
||||||
/// pub struct MyExtension;
|
|
||||||
///
|
|
||||||
/// impl Extension for MyExtension {
|
|
||||||
/// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
|
||||||
/// include_files_service!(scfg, assets => "/public");
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Y para buscar los recursos en el sistema de ficheros (si existe la ruta absoluta):
|
|
||||||
///
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// include_files_service!(cfg, assets => "/public", ["/var/www", "assets"]);
|
|
||||||
///
|
|
||||||
/// // También desde el directorio actual de ejecución.
|
|
||||||
/// include_files_service!(cfg, assets => "/public", ["", "static"]);
|
|
||||||
/// ```
|
|
||||||
#[deprecated(since = "0.3.0", note = "Use `static_files_service!` instead")]
|
#[deprecated(since = "0.3.0", note = "Use `static_files_service!` instead")]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! include_files_service {
|
macro_rules! include_files_service {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
//! Gestión de trazas y registro de eventos de la aplicación.
|
//! Gestión de trazas y registro de eventos de la aplicación.
|
||||||
//!
|
//!
|
||||||
//! `PageTop` recopila información de diagnóstico de la aplicación de forma estructurada y basada en
|
//! PageTop recopila información de diagnóstico de la aplicación de forma estructurada y basada en
|
||||||
//! eventos.
|
//! eventos.
|
||||||
//!
|
//!
|
||||||
//! En los sistemas asíncronos, interpretar los mensajes de log tradicionales suele volverse
|
//! En los sistemas asíncronos, interpretar los mensajes de log tradicionales suele volverse
|
||||||
//! complicado. Las tareas individuales se multiplexan en el mismo hilo y los eventos y registros
|
//! complicado. Las tareas individuales se multiplexan en el mismo hilo y los eventos y registros
|
||||||
//! asociados se entremezclan, lo que dificulta seguir la secuencia lógica.
|
//! asociados se entremezclan, lo que dificulta seguir la secuencia lógica.
|
||||||
//!
|
//!
|
||||||
//! `PageTop` usa [`tracing`](https://docs.rs/tracing) para registrar eventos estructurados y con
|
//! PageTop usa [`tracing`](https://docs.rs/tracing) para registrar eventos estructurados y con
|
||||||
//! información adicional sobre la *temporalidad* y la *causalidad*. A diferencia de un mensaje de
|
//! información adicional sobre la *temporalidad* y la *causalidad*. A diferencia de un mensaje de
|
||||||
//! log, un *span* (intervalo) tiene un momento de inicio y de fin, puede entrar y salir del flujo
|
//! log, un *span* (intervalo) tiene un momento de inicio y de fin, puede entrar y salir del flujo
|
||||||
//! de ejecución y puede existir dentro de un árbol anidado de *spans* similares. Además, estos
|
//! de ejecución y puede existir dentro de un árbol anidado de *spans* similares. Además, estos
|
||||||
|
|
386
src/util.rs
|
@ -1,4 +1,4 @@
|
||||||
//! Funciones y macros útiles.
|
//! Macros y funciones útiles.
|
||||||
|
|
||||||
use crate::trace;
|
use crate::trace;
|
||||||
|
|
||||||
|
@ -6,6 +6,198 @@ use std::env;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
// MACROS INTEGRADAS *******************************************************************************
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use paste::paste;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use concat_string::concat_string;
|
||||||
|
|
||||||
|
pub use indoc::{concatdoc, formatdoc, indoc};
|
||||||
|
|
||||||
|
// MACROS ÚTILES ***********************************************************************************
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
/// Macro para construir una colección de pares clave-valor.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use pagetop::hm;
|
||||||
|
/// use std::collections::HashMap;
|
||||||
|
///
|
||||||
|
/// let args:HashMap<&str, String> = hm![
|
||||||
|
/// "userName" => "Roberto",
|
||||||
|
/// "photoCount" => "3",
|
||||||
|
/// "userGender" => "male",
|
||||||
|
/// ];
|
||||||
|
/// ```
|
||||||
|
macro_rules! hm {
|
||||||
|
( $($key:expr => $value:expr),* $(,)? ) => {{
|
||||||
|
let mut a = std::collections::HashMap::new();
|
||||||
|
$(
|
||||||
|
a.insert($key.into(), $value.into());
|
||||||
|
)*
|
||||||
|
a
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Concatena eficientemente varios fragmentos en un [`String`].
|
||||||
|
///
|
||||||
|
/// Esta macro exporta [`concat_string!`](https://docs.rs/concat-string). Acepta cualquier número de
|
||||||
|
/// fragmentos que implementen [`AsRef<str>`] y construye un [`String`] con el tamaño óptimo, de
|
||||||
|
/// forma eficiente y evitando el uso de cadenas de formato que penalicen el rendimiento.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// // Concatena todos los fragmentos directamente.
|
||||||
|
/// let result = join!("Hello", " ", "World");
|
||||||
|
/// assert_eq!(result, "Hello World".to_string());
|
||||||
|
///
|
||||||
|
/// // También funciona con valores vacíos.
|
||||||
|
/// let result_with_empty = join!("Hello", "", "World");
|
||||||
|
/// assert_eq!(result_with_empty, "HelloWorld".to_string());
|
||||||
|
///
|
||||||
|
/// // Un único fragmento devuelve el mismo valor.
|
||||||
|
/// let single_result = join!("Hello");
|
||||||
|
/// assert_eq!(single_result, "Hello".to_string());
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! join {
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
$crate::util::concat_string!($($arg)*)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Concatena los fragmentos no vacíos en un [`Option<String>`] con un separador opcional.
|
||||||
|
///
|
||||||
|
/// Esta macro acepta cualquier número de fragmentos que implementen [`AsRef<str>`] para concatenar
|
||||||
|
/// todos los fragmentos no vacíos usando opcionalmente un separador.
|
||||||
|
///
|
||||||
|
/// Si todos los fragmentos están vacíos, devuelve [`None`].
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// // Concatena los fragmentos no vacíos con un espacio como separador.
|
||||||
|
/// let result_with_separator = join_opt!(["Hello", "", "World"]; " ");
|
||||||
|
/// assert_eq!(result_with_separator, Some("Hello World".to_string()));
|
||||||
|
///
|
||||||
|
/// // Concatena los fragmentos no vacíos sin un separador.
|
||||||
|
/// let result_without_separator = join_opt!(["Hello", "", "World"]);
|
||||||
|
/// assert_eq!(result_without_separator, Some("HelloWorld".to_string()));
|
||||||
|
///
|
||||||
|
/// // Devuelve `None` si todos los fragmentos están vacíos.
|
||||||
|
/// let result_empty = join_opt!(["", "", ""]);
|
||||||
|
/// assert_eq!(result_empty, None);
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! join_opt {
|
||||||
|
([$($arg:expr),* $(,)?]) => {{
|
||||||
|
let s = $crate::util::concat_string!($($arg),*);
|
||||||
|
(!s.is_empty()).then_some(s)
|
||||||
|
}};
|
||||||
|
([$($arg:expr),* $(,)?]; $separator:expr) => {{
|
||||||
|
let s = [$($arg),*]
|
||||||
|
.iter()
|
||||||
|
.filter(|&item| !item.is_empty())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join($separator);
|
||||||
|
(!s.is_empty()).then_some(s)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Concatena dos fragmentos en un [`String`] usando un separador.
|
||||||
|
///
|
||||||
|
/// Une los dos fragmentos, que deben implementar [`AsRef<str>`], usando el separador proporcionado.
|
||||||
|
/// Si uno de ellos está vacío, devuelve directamente el otro; y si ambos están vacíos devuelve un
|
||||||
|
/// [`String`] vacío.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// let first = "Hello";
|
||||||
|
/// let separator = "-";
|
||||||
|
/// let second = "World";
|
||||||
|
///
|
||||||
|
/// // Concatena los dos fragmentos cuando ambos no están vacíos.
|
||||||
|
/// let result = join_pair!(first, separator, second);
|
||||||
|
/// assert_eq!(result, "Hello-World".to_string());
|
||||||
|
///
|
||||||
|
/// // Si el primer fragmento está vacío, devuelve el segundo.
|
||||||
|
/// let result_empty_first = join_pair!("", separator, second);
|
||||||
|
/// assert_eq!(result_empty_first, "World".to_string());
|
||||||
|
///
|
||||||
|
/// // Si el segundo fragmento está vacío, devuelve el primero.
|
||||||
|
/// let result_empty_second = join_pair!(first, separator, "");
|
||||||
|
/// assert_eq!(result_empty_second, "Hello".to_string());
|
||||||
|
///
|
||||||
|
/// // Si ambos fragmentos están vacíos, devuelve una cadena vacía.
|
||||||
|
/// let result_both_empty = join_pair!("", separator, "");
|
||||||
|
/// assert_eq!(result_both_empty, "".to_string());
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! join_pair {
|
||||||
|
($first:expr, $separator:expr, $second:expr) => {{
|
||||||
|
if $first.is_empty() {
|
||||||
|
String::from($second)
|
||||||
|
} else if $second.is_empty() {
|
||||||
|
String::from($first)
|
||||||
|
} else {
|
||||||
|
$crate::util::concat_string!($first, $separator, $second)
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Concatena varios fragmentos en un [`Option<String>`] si ninguno está vacío.
|
||||||
|
///
|
||||||
|
/// Si alguno de los fragmentos, que deben implementar [`AsRef<str>`], está vacío, devuelve
|
||||||
|
/// [`None`]. Opcionalmente se puede indicar un separador entre los fragmentos concatenados.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// // Concatena los fragmentos.
|
||||||
|
/// let result = join_strict!(["Hello", "World"]);
|
||||||
|
/// assert_eq!(result, Some("HelloWorld".to_string()));
|
||||||
|
///
|
||||||
|
/// // Concatena los fragmentos con un separador.
|
||||||
|
/// let result_with_separator = join_strict!(["Hello", "World"]; " ");
|
||||||
|
/// assert_eq!(result_with_separator, Some("Hello World".to_string()));
|
||||||
|
///
|
||||||
|
/// // Devuelve `None` si alguno de los fragmentos está vacío.
|
||||||
|
/// let result_with_empty = join_strict!(["Hello", "", "World"]);
|
||||||
|
/// assert_eq!(result_with_empty, None);
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! join_strict {
|
||||||
|
([$($arg:expr),* $(,)?]) => {{
|
||||||
|
let fragments = [$($arg),*];
|
||||||
|
if fragments.iter().any(|&item| item.is_empty()) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(fragments.concat())
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
([$($arg:expr),* $(,)?]; $separator:expr) => {{
|
||||||
|
let fragments = [$($arg),*];
|
||||||
|
if fragments.iter().any(|&item| item.is_empty()) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(fragments.join($separator))
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
// FUNCIONES ÚTILES ********************************************************************************
|
// FUNCIONES ÚTILES ********************************************************************************
|
||||||
|
|
||||||
/// Resuelve y valida la ruta de un directorio existente, devolviendo una ruta absoluta.
|
/// Resuelve y valida la ruta de un directorio existente, devolviendo una ruta absoluta.
|
||||||
|
@ -56,8 +248,8 @@ pub fn resolve_absolute_dir<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la ruta absoluta a un directorio existente.
|
/// **Obsoleto desde la versión 0.3.0**: usar [`resolve_absolute_dir()`] en su lugar.
|
||||||
#[deprecated(since = "0.3.0", note = "Use [`resolve_absolute_dir`] instead")]
|
#[deprecated(since = "0.3.0", note = "Use `resolve_absolute_dir()` instead")]
|
||||||
pub fn absolute_dir<P, Q>(root_path: P, relative_path: Q) -> io::Result<PathBuf>
|
pub fn absolute_dir<P, Q>(root_path: P, relative_path: Q) -> io::Result<PathBuf>
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
|
@ -65,191 +257,3 @@ where
|
||||||
{
|
{
|
||||||
resolve_absolute_dir(root_path.as_ref().join(relative_path.as_ref()))
|
resolve_absolute_dir(root_path.as_ref().join(relative_path.as_ref()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MACROS ÚTILES ***********************************************************************************
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use paste::paste;
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use concat_string::concat_string;
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
/// Macro para construir una colección de pares clave-valor.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use pagetop::hm;
|
|
||||||
/// use std::collections::HashMap;
|
|
||||||
///
|
|
||||||
/// let args:HashMap<&str, String> = hm![
|
|
||||||
/// "userName" => "Roberto",
|
|
||||||
/// "photoCount" => "3",
|
|
||||||
/// "userGender" => "male",
|
|
||||||
/// ];
|
|
||||||
/// ```
|
|
||||||
macro_rules! hm {
|
|
||||||
( $($key:expr => $value:expr),* $(,)? ) => {{
|
|
||||||
let mut a = std::collections::HashMap::new();
|
|
||||||
$(
|
|
||||||
a.insert($key.into(), $value.into());
|
|
||||||
)*
|
|
||||||
a
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Concatena eficientemente varios fragmentos en un [`String`].
|
|
||||||
///
|
|
||||||
/// Esta macro exporta [`concat_string!`](https://docs.rs/concat-string). Acepta cualquier número de
|
|
||||||
/// fragmentos que implementen [`AsRef<str>`] y construye un [`String`] con el tamaño óptimo, de
|
|
||||||
/// forma eficiente y evitando el uso de cadenas de formato que penalicen el rendimiento.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// // Concatena todos los fragmentos directamente.
|
|
||||||
/// let result = join!("Hello", " ", "World");
|
|
||||||
/// assert_eq!(result, String::from("Hello World"));
|
|
||||||
///
|
|
||||||
/// // También funciona con valores vacíos.
|
|
||||||
/// let result_with_empty = join!("Hello", "", "World");
|
|
||||||
/// assert_eq!(result_with_empty, String::from("HelloWorld"));
|
|
||||||
///
|
|
||||||
/// // Un único fragmento devuelve el mismo valor.
|
|
||||||
/// let single_result = join!("Hello");
|
|
||||||
/// assert_eq!(single_result, String::from("Hello"));
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! join {
|
|
||||||
($($arg:tt)*) => {
|
|
||||||
$crate::util::concat_string!($($arg)*)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Concatena los fragmentos no vacíos en un [`Option<String>`] con un separador opcional.
|
|
||||||
///
|
|
||||||
/// Esta macro acepta cualquier número de fragmentos que implementen [`AsRef<str>`] para concatenar
|
|
||||||
/// todos los fragmentos no vacíos usando opcionalmente un separador.
|
|
||||||
///
|
|
||||||
/// Si todos los fragmentos están vacíos, devuelve [`None`].
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// // Concatena los fragmentos no vacíos con un espacio como separador.
|
|
||||||
/// let result_with_separator = join_opt!(["Hello", "", "World"]; " ");
|
|
||||||
/// assert_eq!(result_with_separator, Some(String::from("Hello World")));
|
|
||||||
///
|
|
||||||
/// // Concatena los fragmentos no vacíos sin un separador.
|
|
||||||
/// let result_without_separator = join_opt!(["Hello", "", "World"]);
|
|
||||||
/// assert_eq!(result_without_separator, Some(String::from("HelloWorld")));
|
|
||||||
///
|
|
||||||
/// // Devuelve `None` si todos los fragmentos están vacíos.
|
|
||||||
/// let result_empty = join_opt!(["", "", ""]);
|
|
||||||
/// assert_eq!(result_empty, None);
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! join_opt {
|
|
||||||
([$($arg:expr),* $(,)?]) => {{
|
|
||||||
let s = $crate::util::concat_string!($($arg),*);
|
|
||||||
(!s.is_empty()).then_some(s)
|
|
||||||
}};
|
|
||||||
([$($arg:expr),* $(,)?]; $separator:expr) => {{
|
|
||||||
let s = [$($arg),*]
|
|
||||||
.iter()
|
|
||||||
.filter(|&item| !item.is_empty())
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join($separator);
|
|
||||||
(!s.is_empty()).then_some(s)
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Concatena dos fragmentos en un [`String`] usando un separador.
|
|
||||||
///
|
|
||||||
/// Une los dos fragmentos, que deben implementar [`AsRef<str>`], usando el separador proporcionado.
|
|
||||||
/// Si uno de ellos está vacío, devuelve directamente el otro; y si ambos están vacíos devuelve un
|
|
||||||
/// [`String`] vacío.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// let first = "Hello";
|
|
||||||
/// let separator = "-";
|
|
||||||
/// let second = "World";
|
|
||||||
///
|
|
||||||
/// // Concatena los dos fragmentos cuando ambos no están vacíos.
|
|
||||||
/// let result = join_pair!(first, separator, second);
|
|
||||||
/// assert_eq!(result, String::from("Hello-World"));
|
|
||||||
///
|
|
||||||
/// // Si el primer fragmento está vacío, devuelve el segundo.
|
|
||||||
/// let result_empty_first = join_pair!("", separator, second);
|
|
||||||
/// assert_eq!(result_empty_first, String::from("World"));
|
|
||||||
///
|
|
||||||
/// // Si el segundo fragmento está vacío, devuelve el primero.
|
|
||||||
/// let result_empty_second = join_pair!(first, separator, "");
|
|
||||||
/// assert_eq!(result_empty_second, String::from("Hello"));
|
|
||||||
///
|
|
||||||
/// // Si ambos fragmentos están vacíos, devuelve una cadena vacía.
|
|
||||||
/// let result_both_empty = join_pair!("", separator, "");
|
|
||||||
/// assert_eq!(result_both_empty, String::from(""));
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! join_pair {
|
|
||||||
($first:expr, $separator:expr, $second:expr) => {{
|
|
||||||
if $first.is_empty() {
|
|
||||||
String::from($second)
|
|
||||||
} else if $second.is_empty() {
|
|
||||||
String::from($first)
|
|
||||||
} else {
|
|
||||||
$crate::util::concat_string!($first, $separator, $second)
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Concatena varios fragmentos en un [`Option<String>`] si ninguno está vacío.
|
|
||||||
///
|
|
||||||
/// Si alguno de los fragmentos, que deben implementar [`AsRef<str>`], está vacío, devuelve
|
|
||||||
/// [`None`]. Opcionalmente se puede indicar un separador entre los fragmentos concatenados.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// // Concatena los fragmentos.
|
|
||||||
/// let result = join_strict!(["Hello", "World"]);
|
|
||||||
/// assert_eq!(result, Some(String::from("HelloWorld")));
|
|
||||||
///
|
|
||||||
/// // Concatena los fragmentos con un separador.
|
|
||||||
/// let result_with_separator = join_strict!(["Hello", "World"]; " ");
|
|
||||||
/// assert_eq!(result_with_separator, Some(String::from("Hello World")));
|
|
||||||
///
|
|
||||||
/// // Devuelve `None` si alguno de los fragmentos está vacío.
|
|
||||||
/// let result_with_empty = join_strict!(["Hello", "", "World"]);
|
|
||||||
/// assert_eq!(result_with_empty, None);
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! join_strict {
|
|
||||||
([$($arg:expr),* $(,)?]) => {{
|
|
||||||
let fragments = [$($arg),*];
|
|
||||||
if fragments.iter().any(|&item| item.is_empty()) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(fragments.concat())
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
([$($arg:expr),* $(,)?]; $separator:expr) => {{
|
|
||||||
let fragments = [$($arg),*];
|
|
||||||
if fragments.iter().any(|&item| item.is_empty()) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(fragments.join($separator))
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
11
static/css/basic.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/* Page layout */
|
||||||
|
|
||||||
|
.region--footer {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PoweredBy component */
|
||||||
|
|
||||||
|
.poweredby {
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -1,13 +1,17 @@
|
||||||
:root {
|
:root {
|
||||||
--bg-img: url('/img/welcome-header.jpg');
|
--bg-img: url('/img/intro-header.jpg');
|
||||||
--bg-img-set: image-set(url('/img/welcome-header.avif') type('image/avif'), url('/img/welcome-header.webp') type('image/webp'), var(--bg-img) type('image/jpeg'));
|
--bg-img-set: image-set(url('/img/intro-header.avif') type('image/avif'), url('/img/intro-header.webp') type('image/webp'), var(--bg-img) type('image/jpeg'));
|
||||||
--bg-img-sm: url('/img/welcome-header-sm.jpg');
|
--bg-img-sm: url('/img/intro-header-sm.jpg');
|
||||||
--bg-img-sm-set: image-set(url('/img/welcome-header-sm.avif') type('image/avif'), url('/img/welcome-header-sm.webp') type('image/webp'), var(--bg-img-sm) type('image/jpeg'));
|
--bg-img-sm-set: image-set(url('/img/intro-header-sm.avif') type('image/avif'), url('/img/intro-header-sm.webp') type('image/webp'), var(--bg-img-sm) type('image/jpeg'));
|
||||||
--bg-color: #8c5919;
|
--bg-color: #8c5919;
|
||||||
--color: #1a202c;
|
--color: #1a202c;
|
||||||
--color-red: #fecaca;
|
|
||||||
--color-gray: #e4e4e7;
|
--color-gray: #e4e4e7;
|
||||||
--color-link: #1e4eae;
|
--color-link: #1e4eae;
|
||||||
|
--color-block-1: #b689ff;
|
||||||
|
--color-block-2: #fecaca;
|
||||||
|
--color-block-3: #e6a9e2;
|
||||||
|
--color-block-4: #ffedca;
|
||||||
|
--color-block-5: #ffffff;
|
||||||
--focus-outline: 2px solid var(--color-link);
|
--focus-outline: 2px solid var(--color-link);
|
||||||
--focus-outline-offset: 2px;
|
--focus-outline-offset: 2px;
|
||||||
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
@ -28,9 +32,14 @@ body {
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
header,
|
|
||||||
section {
|
section {
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -50,20 +59,17 @@ a:hover:visited {
|
||||||
text-decoration-color: var(--color-link);
|
text-decoration-color: var(--color-link);
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
/*
|
||||||
width: 100%;
|
* Header
|
||||||
display: flex;
|
*/
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main-header {
|
.intro-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
padding-bottom: 9rem;
|
|
||||||
max-width: 80rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-bottom: 9rem;
|
||||||
background-image: var(--bg-img-sm);
|
background-image: var(--bg-img-sm);
|
||||||
background-image: var(--bg-img-sm-set);
|
background-image: var(--bg-img-sm-set);
|
||||||
background-position: top center;
|
background-position: top center;
|
||||||
|
@ -71,11 +77,11 @@ a:hover:visited {
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
#main-header header {
|
.intro-header__body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
#header-title {
|
.intro-header__title {
|
||||||
margin: 0 0 0 1.5rem;
|
margin: 0 0 0 1.5rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -89,7 +95,7 @@ a:hover:visited {
|
||||||
line-height: 110%;
|
line-height: 110%;
|
||||||
text-shadow: 0 0.125rem 0.1875rem rgba(0, 0, 0, 0.3);
|
text-shadow: 0 0.125rem 0.1875rem rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
#header-title > span {
|
.intro-header__title > span {
|
||||||
background: linear-gradient(180deg, #ddff95 30%, #ffb84b 100%);
|
background: linear-gradient(180deg, #ddff95 30%, #ffb84b 100%);
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
|
@ -100,40 +106,44 @@ a:hover:visited {
|
||||||
line-height: 110%;
|
line-height: 110%;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
#header-image {
|
.intro-header__image {
|
||||||
width: 100%;
|
|
||||||
text-align: right;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
#header-image #monster {
|
.intro-header__monster {
|
||||||
margin-right: 12rem;
|
margin-right: 12rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
@media (min-width: 64rem) {
|
@media (min-width: 64rem) {
|
||||||
#main-header {
|
.intro-header {
|
||||||
background-image: var(--bg-img);
|
background-image: var(--bg-img);
|
||||||
background-image: var(--bg-img-set);
|
background-image: var(--bg-img-set);
|
||||||
}
|
}
|
||||||
#header-title {
|
.intro-header__title {
|
||||||
padding: 1.2rem 2rem 2.6rem 2rem;
|
padding: 1.2rem 2rem 2.6rem 2rem;
|
||||||
}
|
}
|
||||||
#header-image {
|
.intro-header__image {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#main-content {
|
/*
|
||||||
|
* Content
|
||||||
|
*/
|
||||||
|
|
||||||
|
.intro-content {
|
||||||
height: auto;
|
height: auto;
|
||||||
margin-top: 1.6rem;
|
margin-top: 1.6rem;
|
||||||
}
|
}
|
||||||
.content-body {
|
.intro-content__body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-width: 80rem;
|
max-width: 80rem;
|
||||||
}
|
}
|
||||||
.content-body:before,
|
.intro-content__body:before,
|
||||||
.content-body:after {
|
.intro-content__body:after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -143,38 +153,37 @@ a:hover:visited {
|
||||||
filter: blur(2.75rem);
|
filter: blur(2.75rem);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
inset: 11.75rem;
|
inset: 11.75rem;
|
||||||
z-index: 0;
|
|
||||||
}
|
}
|
||||||
.content-body:before {
|
.intro-content__body:before {
|
||||||
top: -1rem;
|
top: -1rem;
|
||||||
}
|
}
|
||||||
.content-body:after {
|
.intro-content__body:after {
|
||||||
bottom: -1rem;
|
bottom: -1rem;
|
||||||
}
|
}
|
||||||
@media (max-width: 48rem) {
|
@media (max-width: 48rem) {
|
||||||
.content-body {
|
.intro-content__body {
|
||||||
margin-top: -9.8rem;
|
margin-top: -9.8rem;
|
||||||
}
|
}
|
||||||
.content-body:before,
|
.intro-content__body:before,
|
||||||
.content-body:after {
|
.intro-content__body:after {
|
||||||
inset: unset;
|
inset: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 64rem) {
|
@media (min-width: 64rem) {
|
||||||
#main-content {
|
.intro-content {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.content-body {
|
.intro-content__body {
|
||||||
margin-top: -5.7rem;
|
margin-top: -5.7rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#poweredby-button {
|
.intro-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto 3rem;
|
margin: 0 auto 3rem;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
#poweredby-link {
|
.intro-button__link {
|
||||||
background: #7f1d1d;
|
background: #7f1d1d;
|
||||||
background-image: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,255,255,0));
|
background-image: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,255,255,0));
|
||||||
background-position: top left, center;
|
background-position: top left, center;
|
||||||
|
@ -187,7 +196,6 @@ a:hover:visited {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-shadow: var(--shadow);
|
|
||||||
transition: transform 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -195,7 +203,7 @@ a:hover:visited {
|
||||||
min-height: 7.6875rem;
|
min-height: 7.6875rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
#poweredby-link::before {
|
.intro-button__link::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -13.125rem;
|
top: -13.125rem;
|
||||||
|
@ -207,7 +215,7 @@ a:hover:visited {
|
||||||
transition: transform 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
#poweredby-text {
|
.intro-button__text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -217,25 +225,24 @@ a:hover:visited {
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: white;
|
color: white;
|
||||||
text-shadow: 0 0.101125rem 0.2021875rem rgba(0, 0, 0, 0.25);
|
|
||||||
font-size: 1.65rem;
|
font-size: 1.65rem;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 130.023%;
|
line-height: 130.023%;
|
||||||
letter-spacing: 0.0075rem;
|
letter-spacing: 0.0075rem;
|
||||||
}
|
}
|
||||||
#poweredby-text strong {
|
.intro-button__text strong {
|
||||||
font-size: 2.625rem;
|
font-size: 2.625rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 130.023%;
|
line-height: 130.023%;
|
||||||
letter-spacing: 0.013125rem;
|
letter-spacing: 0.013125rem;
|
||||||
}
|
}
|
||||||
#poweredby-link span {
|
.intro-button__link span {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#poweredby-link span:nth-child(1) {
|
.intro-button__link span:nth-child(1) {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -255,7 +262,7 @@ a:hover:visited {
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#poweredby-link span:nth-child(2) {
|
.intro-button__link span:nth-child(2) {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -275,7 +282,7 @@ a:hover:visited {
|
||||||
transform: translateY(100%);
|
transform: translateY(100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#poweredby-link span:nth-child(3) {
|
.intro-button__link span:nth-child(3) {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -295,27 +302,22 @@ a:hover:visited {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#poweredby-link:hover {
|
.intro-button__link:hover span {
|
||||||
transition: all .5s;
|
|
||||||
transform: rotate(-3deg) scale(1.1);
|
|
||||||
box-shadow: 0px 3px 5px rgba(0,0,0,.4);
|
|
||||||
}
|
|
||||||
#poweredby-link:hover span {
|
|
||||||
animation-play-state: paused;
|
animation-play-state: paused;
|
||||||
}
|
}
|
||||||
@media (max-width: 48rem) {
|
@media (max-width: 48rem) {
|
||||||
#poweredby-link {
|
.intro-button__link {
|
||||||
height: 6.25rem;
|
height: 6.25rem;
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
#poweredby-text {
|
.intro-button__text {
|
||||||
display: inline;
|
display: inline;
|
||||||
padding-top: .5rem;
|
padding-top: .5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 48rem) {
|
@media (min-width: 48rem) {
|
||||||
#poweredby-button {
|
.intro-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -323,9 +325,13 @@ a:hover:visited {
|
||||||
max-width: 29.375rem;
|
max-width: 29.375rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
.intro-button__link:hover {
|
||||||
|
transition: all .5s;
|
||||||
|
transform: rotate(-3deg) scale(1.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-text {
|
.intro-text {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -337,13 +343,16 @@ a:hover:visited {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin-top: -6rem;
|
margin-top: -6rem;
|
||||||
background: #fff;
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
background: #fff;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 6rem 1.063rem 0.75rem;
|
padding: 2.5rem 1.063rem 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.content-text p {
|
.intro-button + .intro-text {
|
||||||
|
padding-top: 6rem;
|
||||||
|
}
|
||||||
|
.intro-text p {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
@ -351,14 +360,16 @@ a:hover:visited {
|
||||||
margin: 0 0 1.5rem;
|
margin: 0 0 1.5rem;
|
||||||
}
|
}
|
||||||
@media (min-width: 48rem) {
|
@media (min-width: 48rem) {
|
||||||
.content-text {
|
.intro-text {
|
||||||
font-size: 1.375rem;
|
font-size: 1.375rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
.intro-button + .intro-text {
|
||||||
padding-top: 7rem;
|
padding-top: 7rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 64rem) {
|
@media (min-width: 64rem) {
|
||||||
.content-text {
|
.intro-text {
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
max-width: 60rem;
|
max-width: 60rem;
|
||||||
|
@ -368,13 +379,13 @@ a:hover:visited {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.subcontent {
|
.intro-text .block {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.subcontent h1 {
|
.intro-text .block__title {
|
||||||
margin: 1em 0 .8em;
|
margin: 1em 0 .8em;
|
||||||
}
|
}
|
||||||
.subcontent h1 span {
|
.intro-text .block__title span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px 30px 14px;
|
padding: 10px 30px 14px;
|
||||||
margin: 0 0 0 20px;
|
margin: 0 0 0 20px;
|
||||||
|
@ -385,7 +396,7 @@ a:hover:visited {
|
||||||
border-color: orangered;
|
border-color: orangered;
|
||||||
transform: rotate(-3deg) translateY(-25%);
|
transform: rotate(-3deg) translateY(-25%);
|
||||||
}
|
}
|
||||||
.subcontent h1:before {
|
.intro-text .block__title:before {
|
||||||
content: "";
|
content: "";
|
||||||
height: 5px;
|
height: 5px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -398,7 +409,7 @@ a:hover:visited {
|
||||||
transform: rotate(2deg) translateY(-50%);
|
transform: rotate(2deg) translateY(-50%);
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
}
|
}
|
||||||
.subcontent h1:after {
|
.intro-text .block__title:after {
|
||||||
content: "";
|
content: "";
|
||||||
height: 70rem;
|
height: 70rem;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -406,55 +417,80 @@ a:hover:visited {
|
||||||
left: -15%;
|
left: -15%;
|
||||||
width: 130%;
|
width: 130%;
|
||||||
z-index: -10;
|
z-index: -10;
|
||||||
background: var(--color-red);
|
background: var(--color-block-1);
|
||||||
transform: rotate(2deg);
|
transform: rotate(2deg);
|
||||||
}
|
}
|
||||||
|
.intro-text .block:nth-of-type(5n+1) .block__title:after {
|
||||||
|
background: var(--color-block-1);
|
||||||
|
}
|
||||||
|
.intro-text .block:nth-of-type(5n+2) .block__title:after {
|
||||||
|
background: var(--color-block-2);
|
||||||
|
}
|
||||||
|
.intro-text .block:nth-of-type(5n+3) .block__title:after {
|
||||||
|
background: var(--color-block-3);
|
||||||
|
}
|
||||||
|
.intro-text .block:nth-of-type(5n+4) .block__title:after {
|
||||||
|
background: var(--color-block-4);
|
||||||
|
}
|
||||||
|
.intro-text .block:nth-of-type(5n+5) .block__title:after {
|
||||||
|
background: var(--color-block-5);
|
||||||
|
}
|
||||||
|
|
||||||
#footer {
|
/*
|
||||||
|
* Footer
|
||||||
|
*/
|
||||||
|
|
||||||
|
.intro-footer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: var(--color-gray);
|
color: var(--color-gray);
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-footer__body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 10.625rem 2rem;
|
||||||
|
max-width: 80rem;
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
line-height: 100%;
|
line-height: 100%;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
#footer a:visited {
|
.intro-footer__body a:visited {
|
||||||
color: var(--color-gray);
|
color: var(--color-gray);
|
||||||
}
|
}
|
||||||
.footer-logo {
|
.intro-footer__logo,
|
||||||
max-height: 12.625rem;
|
.intro-footer__links {
|
||||||
}
|
|
||||||
.footer-logo svg {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.footer-logo,
|
|
||||||
.footer-links,
|
|
||||||
.footer-inner {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.footer-links {
|
.intro-footer__logo {
|
||||||
|
max-height: 12.625rem;
|
||||||
|
}
|
||||||
|
.intro-footer__logo svg {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.intro-footer__links {
|
||||||
gap: 1.875rem;
|
gap: 1.875rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
.footer-inner {
|
|
||||||
max-width: 80rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0 10.625rem 2rem;
|
|
||||||
}
|
|
||||||
@media (max-width: 48rem) {
|
@media (max-width: 48rem) {
|
||||||
.footer-logo {
|
.intro-footer__logo {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 64rem) {
|
@media (max-width: 64rem) {
|
||||||
.footer-inner {
|
.intro-footer__body {
|
||||||
padding: 0 1rem 2rem;
|
padding: 0 1rem 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PoweredBy component */
|
||||||
|
|
||||||
|
.poweredby a:visited {
|
||||||
|
color: var(--color-gray);
|
||||||
|
}
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
@ -17,10 +17,10 @@ async fn component_html_renders_static_markup() {
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn component_html_renders_using_context_param() {
|
async fn component_html_renders_using_context_param() {
|
||||||
let mut cx = Context::new(None).with_param("username", String::from("Alice"));
|
let mut cx = Context::new(None).with_param("username", "Alice".to_string());
|
||||||
|
|
||||||
let component = Html::with(|cx| {
|
let component = Html::with(|cx| {
|
||||||
let name = cx.get_param::<String>("username").unwrap_or_default();
|
let name = cx.param::<String>("username").cloned().unwrap_or_default();
|
||||||
html! {
|
html! {
|
||||||
span { (name) }
|
span { (name) }
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ async fn component_html_renders_using_context_param() {
|
||||||
async fn component_html_allows_replacing_render_function() {
|
async fn component_html_allows_replacing_render_function() {
|
||||||
let mut component = Html::with(|_| html! { div { "Original" } });
|
let mut component = Html::with(|_| html! { div { "Original" } });
|
||||||
|
|
||||||
component.alter_html(|_| html! { div { "Modified" } });
|
component.alter_fn(|_| html! { div { "Modified" } });
|
||||||
|
|
||||||
let markup = component
|
let markup = component
|
||||||
.prepare_component(&mut Context::new(None))
|
.prepare_component(&mut Context::new(None))
|
||||||
|
|
99
tests/component_poweredby.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn poweredby_default_shows_only_pagetop_recognition() {
|
||||||
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
|
let p = PoweredBy::default();
|
||||||
|
let html = render_component(&p);
|
||||||
|
|
||||||
|
// Debe mostrar el bloque de reconocimiento a PageTop.
|
||||||
|
assert!(html.as_str().contains("poweredby__pagetop"));
|
||||||
|
|
||||||
|
// Y NO debe mostrar el bloque de copyright.
|
||||||
|
assert!(!html.as_str().contains("poweredby__copyright"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn poweredby_new_includes_current_year_and_app_name() {
|
||||||
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
|
let p = PoweredBy::new();
|
||||||
|
let html = render_component(&p);
|
||||||
|
|
||||||
|
let year = Utc::now().format("%Y").to_string();
|
||||||
|
assert!(
|
||||||
|
html.as_str().contains(&year),
|
||||||
|
"HTML should include the current year"
|
||||||
|
);
|
||||||
|
|
||||||
|
// El nombre de la app proviene de `global::SETTINGS.app.name`.
|
||||||
|
let app_name = &global::SETTINGS.app.name;
|
||||||
|
assert!(
|
||||||
|
html.as_str().contains(app_name),
|
||||||
|
"HTML should include the application name"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debe existir el span de copyright.
|
||||||
|
assert!(html.as_str().contains("poweredby__copyright"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn poweredby_with_copyright_overrides_text() {
|
||||||
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
|
let custom = "2001 © FooBar Inc.";
|
||||||
|
let p = PoweredBy::default().with_copyright(Some(custom));
|
||||||
|
let html = render_component(&p);
|
||||||
|
|
||||||
|
assert!(html.as_str().contains(custom));
|
||||||
|
assert!(html.as_str().contains("poweredby__copyright"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn poweredby_with_copyright_none_hides_text() {
|
||||||
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
|
let p = PoweredBy::new().with_copyright(None::<String>);
|
||||||
|
let html = render_component(&p);
|
||||||
|
|
||||||
|
assert!(!html.as_str().contains("poweredby__copyright"));
|
||||||
|
// El reconocimiento a PageTop siempre debe aparecer.
|
||||||
|
assert!(html.as_str().contains("poweredby__pagetop"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn poweredby_link_points_to_crates_io() {
|
||||||
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
|
let p = PoweredBy::default();
|
||||||
|
let html = render_component(&p);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
html.as_str().contains("https://pagetop.cillero.es"),
|
||||||
|
"Link should point to pagetop.cillero.es"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn poweredby_getter_reflects_internal_state() {
|
||||||
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
|
// Por defecto no hay copyright.
|
||||||
|
let p0 = PoweredBy::default();
|
||||||
|
assert_eq!(p0.copyright(), None);
|
||||||
|
|
||||||
|
// Y `new()` lo inicializa con año + nombre de app.
|
||||||
|
let p1 = PoweredBy::new();
|
||||||
|
let c1 = p1.copyright().expect("Expected copyright to exis");
|
||||||
|
assert!(c1.contains(&Utc::now().format("%Y").to_string()));
|
||||||
|
assert!(c1.contains(&global::SETTINGS.app.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// HELPERS *****************************************************************************************
|
||||||
|
|
||||||
|
fn render_component<C: Component>(c: &C) -> Markup {
|
||||||
|
let mut cx = Context::default();
|
||||||
|
let pm = c.prepare_component(&mut cx);
|
||||||
|
pm.render()
|
||||||
|
}
|
103
tests/html.rs
|
@ -1,17 +1,108 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn prepare_markup_is_empty() {
|
async fn prepare_markup_render_none_is_empty_string() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
assert_eq!(PrepareMarkup::None.render().as_str(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn prepare_markup_render_escaped_escapes_html_and_ampersands() {
|
||||||
|
let pm = PrepareMarkup::Escaped("<b>& \" ' </b>".to_string());
|
||||||
|
assert_eq!(pm.render().as_str(), "<b>& " ' </b>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn prepare_markup_render_raw_is_inserted_verbatim() {
|
||||||
|
let pm = PrepareMarkup::Raw("<b>bold</b><script>1<2</script>".to_string());
|
||||||
|
assert_eq!(pm.render().as_str(), "<b>bold</b><script>1<2</script>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn prepare_markup_render_with_keeps_structure() {
|
||||||
|
let pm = PrepareMarkup::With(html! {
|
||||||
|
h2 { "Sample title" }
|
||||||
|
p { "This is a paragraph." }
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
pm.render().as_str(),
|
||||||
|
"<h2>Sample title</h2><p>This is a paragraph.</p>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn prepare_markup_does_not_double_escape_when_wrapped_in_html_macro() {
|
||||||
|
// Escaped: dentro de `html!` no debe volver a escaparse.
|
||||||
|
let escaped = PrepareMarkup::Escaped("<i>x</i>".into());
|
||||||
|
let wrapped_escaped = html! { div { (escaped.render()) } };
|
||||||
|
assert_eq!(
|
||||||
|
wrapped_escaped.into_string(),
|
||||||
|
"<div><i>x</i></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Raw: tampoco debe escaparse al integrarlo.
|
||||||
|
let raw = PrepareMarkup::Raw("<i>x</i>".into());
|
||||||
|
let wrapped_raw = html! { div { (raw.render()) } };
|
||||||
|
assert_eq!(wrapped_raw.into_string(), "<div><i>x</i></div>");
|
||||||
|
|
||||||
|
// With: debe incrustar el Markup tal cual.
|
||||||
|
let with = PrepareMarkup::With(html! { span.title { "ok" } });
|
||||||
|
let wrapped_with = html! { div { (with.render()) } };
|
||||||
|
assert_eq!(
|
||||||
|
wrapped_with.into_string(),
|
||||||
|
"<div><span class=\"title\">ok</span></div>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn prepare_markup_unicode_is_preserved() {
|
||||||
|
// Texto con acentos y emojis debe conservarse (salvo el escape HTML de signos).
|
||||||
|
let esc = PrepareMarkup::Escaped("Hello, tomorrow coffee ☕ & donuts!".into());
|
||||||
|
assert_eq!(
|
||||||
|
esc.render().as_str(),
|
||||||
|
"Hello, tomorrow coffee ☕ & donuts!"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Raw debe pasar íntegro.
|
||||||
|
let raw = PrepareMarkup::Raw("Title — section © 2025".into());
|
||||||
|
assert_eq!(raw.render().as_str(), "Title — section © 2025");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn prepare_markup_is_empty_semantics() {
|
||||||
assert!(PrepareMarkup::None.is_empty());
|
assert!(PrepareMarkup::None.is_empty());
|
||||||
|
|
||||||
assert!(PrepareMarkup::Text(String::from("")).is_empty());
|
|
||||||
assert!(!PrepareMarkup::Text(String::from("x")).is_empty());
|
|
||||||
|
|
||||||
assert!(PrepareMarkup::Escaped(String::new()).is_empty());
|
assert!(PrepareMarkup::Escaped(String::new()).is_empty());
|
||||||
assert!(!PrepareMarkup::Escaped("a".into()).is_empty());
|
assert!(PrepareMarkup::Escaped("".to_string()).is_empty());
|
||||||
|
assert!(!PrepareMarkup::Escaped("x".to_string()).is_empty());
|
||||||
|
|
||||||
|
assert!(PrepareMarkup::Raw(String::new()).is_empty());
|
||||||
|
assert!(PrepareMarkup::Raw("".to_string()).is_empty());
|
||||||
|
assert!(!PrepareMarkup::Raw("a".into()).is_empty());
|
||||||
|
|
||||||
assert!(PrepareMarkup::With(html! {}).is_empty());
|
assert!(PrepareMarkup::With(html! {}).is_empty());
|
||||||
assert!(!PrepareMarkup::With(html! { span { "!" } }).is_empty());
|
assert!(!PrepareMarkup::With(html! { span { "!" } }).is_empty());
|
||||||
|
|
||||||
|
// Ojo: espacios NO deberían considerarse vacíos (comportamiento actual).
|
||||||
|
assert!(!PrepareMarkup::Escaped(" ".into()).is_empty());
|
||||||
|
assert!(!PrepareMarkup::Raw(" ".into()).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn prepare_markup_equivalence_between_render_and_inline_in_html_macro() {
|
||||||
|
let cases = [
|
||||||
|
PrepareMarkup::None,
|
||||||
|
PrepareMarkup::Escaped("<b>x</b>".into()),
|
||||||
|
PrepareMarkup::Raw("<b>x</b>".into()),
|
||||||
|
PrepareMarkup::With(html! { b { "x" } }),
|
||||||
|
];
|
||||||
|
|
||||||
|
for pm in cases {
|
||||||
|
let rendered = pm.render();
|
||||||
|
let in_macro = html! { (rendered) }.into_string();
|
||||||
|
assert_eq!(
|
||||||
|
rendered.as_str(),
|
||||||
|
in_macro,
|
||||||
|
"The output of Render and (pm) inside html! must match"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ async fn translation_without_args() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
let l10n = L10n::l("test-hello-world");
|
let l10n = L10n::l("test-hello-world");
|
||||||
let translation = l10n.using(&LangMatch::resolve("es-ES"));
|
let translation = l10n.lookup(&LangMatch::resolve("es-ES"));
|
||||||
assert_eq!(translation, Some("¡Hola mundo!".to_string()));
|
assert_eq!(translation, Some("¡Hola mundo!".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ async fn translation_with_args() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
let l10n = L10n::l("test-hello-user").with_arg("userName", "Manuel");
|
let l10n = L10n::l("test-hello-user").with_arg("userName", "Manuel");
|
||||||
let translation = l10n.using(&LangMatch::resolve("es-ES"));
|
let translation = l10n.lookup(&LangMatch::resolve("es-ES"));
|
||||||
assert_eq!(translation, Some("¡Hola, Manuel!".to_string()));
|
assert_eq!(translation, Some("¡Hola, Manuel!".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ async fn translation_with_plural_and_select() {
|
||||||
("photoCount", "3"),
|
("photoCount", "3"),
|
||||||
("userGender", "male"),
|
("userGender", "male"),
|
||||||
]);
|
]);
|
||||||
let translation = l10n.using(&LangMatch::resolve("es-ES")).unwrap();
|
let translation = l10n.lookup(&LangMatch::resolve("es-ES")).unwrap();
|
||||||
assert!(translation.contains("añadido 3 nuevas fotos de él"));
|
assert!(translation.contains("añadido 3 nuevas fotos de él"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ async fn check_fallback_language() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
let l10n = L10n::l("test-hello-world");
|
let l10n = L10n::l("test-hello-world");
|
||||||
let translation = l10n.using(&LangMatch::resolve("xx-YY")); // Retrocede a "en-US".
|
let translation = l10n.lookup(&LangMatch::resolve("xx-YY")); // Retrocede a "en-US".
|
||||||
assert_eq!(translation, Some("Hello world!".to_string()));
|
assert_eq!(translation, Some("Hello world!".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,6 @@ async fn check_unknown_key() {
|
||||||
let _app = service::test::init_service(Application::new().test()).await;
|
let _app = service::test::init_service(Application::new().test()).await;
|
||||||
|
|
||||||
let l10n = L10n::l("non-existent-key");
|
let l10n = L10n::l("non-existent-key");
|
||||||
let translation = l10n.using(&LangMatch::resolve("en-US"));
|
let translation = l10n.lookup(&LangMatch::resolve("en-US"));
|
||||||
assert_eq!(translation, None);
|
assert_eq!(translation, None);
|
||||||
}
|
}
|
||||||
|
|