WIP: Preparando liberación de la versión 0.5 de PageTop #10
141 changed files with 5814 additions and 2968 deletions
130
CONTRIBUTING.md
Normal file
130
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Guía de contribución a PageTop
|
||||
|
||||
Gracias por tu interés en contribuir a **PageTop** 🎉
|
||||
|
||||
Este documento describe **cómo participar en el desarrollo del proyecto**, el flujo de trabajo y las
|
||||
normas que permitan garantizar un historial limpio, trazable y sostenible a largo plazo.
|
||||
|
||||
Por favor, léelo completo antes de abrir una *issue* o una *pull request*.
|
||||
|
||||
|
||||
## 1. Repositorios
|
||||
|
||||
PageTop mantiene **un único repositorio oficial**:
|
||||
|
||||
* **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop
|
||||
* **Repositorio espejo:** https://github.com/manuelcillero/pagetop
|
||||
|
||||
> ⚠️ **Importante**
|
||||
> Aunque GitHub permite abrir *issues* y *pull requests*, **la integración del código se realiza
|
||||
> únicamente en el repositorio oficial**. GitHub actúa como repositorio espejo que se sincroniza
|
||||
> automáticamente para reflejar el mismo estado.
|
||||
|
||||
|
||||
## 2. Issues (incidencias, propuestas, preguntas)
|
||||
|
||||
Antes de abrir una *issue* **en GitHub**:
|
||||
|
||||
* comprueba que no exista ya una similar,
|
||||
* describe claramente el problema o propuesta,
|
||||
* incluye pasos de reproducción si se trata de un *bug*,
|
||||
* indica versión, entorno y contexto cuando sea relevante.
|
||||
|
||||
Las *issues* se usan para:
|
||||
|
||||
* informar de errores,
|
||||
* propuestas de mejora,
|
||||
* discusión técnica previa a cambios relevantes.
|
||||
|
||||
|
||||
## 3. Pull Requests (PRs)
|
||||
|
||||
### 3.1 Dónde abrirlas
|
||||
|
||||
Las *pull requests* se abren **en GitHub**, normalmente contra la rama `main`. GitHub es el punto de
|
||||
entrada recomendado para contribuciones externas.
|
||||
|
||||
### 3.2 Reglas generales para PRs
|
||||
|
||||
* Cada PR debe abordar **un único objetivo claro**.
|
||||
* Mantén el alcance lo más acotado posible.
|
||||
* Incluye descripción clara del cambio.
|
||||
* Si el PR corrige una *issue*, enlázala explícitamente.
|
||||
* Asegúrate de que el código compila y pasa las pruebas.
|
||||
|
||||
### 3.3 Revisión y aceptación
|
||||
|
||||
Todas las PRs son **revisadas manualmente** y pueden recibir comentarios o solicitudes de cambios.
|
||||
|
||||
Las PRs aceptadas se integran en el repositorio oficial, nunca directamente en GitHub, preservando
|
||||
siempre la **autoría original** del contribuidor.
|
||||
|
||||
|
||||
### 3.4. Cierre de Pull Requests y sincronización
|
||||
|
||||
Una vez que el cambio ha sido integrado en el repositorio oficial:
|
||||
|
||||
* La PR en GitHub se **cierra manualmente**.
|
||||
* Se añade un **mensaje estándar de cierre** indicando que el cambio ha sido integrado.
|
||||
* El repositorio de GitHub **se sincroniza automáticamente** como espejo.
|
||||
|
||||
|
||||
## 4. Estilo de código y calidad
|
||||
|
||||
* Sigue el estilo existente del proyecto.
|
||||
* Mantén los comentarios claros y precisos.
|
||||
* La documentación es parte del código: actualízala cuando sea necesario.
|
||||
* Cambios públicos o estructurales deben ir acompañados de documentación.
|
||||
|
||||
|
||||
## 5. Commits
|
||||
|
||||
PageTop usa la especificación **gitmoji** para los mensajes de *commit*. El formato recomendado es:
|
||||
|
||||
```text
|
||||
<propósito> [(ámbito opcional):] <mensaje>
|
||||
«LÍNEA EN BLANCO»
|
||||
Cuerpo opcional
|
||||
«LÍNEA EN BLANCO»
|
||||
Nota(s) al pie opcional(es) para referencias, incidencias o cambios incompatibles
|
||||
```
|
||||
|
||||
Ejemplos (no más de 50 caracteres en la primera línea, y no más de 80 en el resto):
|
||||
|
||||
* `📝 Actualiza la guía de contribución`
|
||||
* `♻️ (locale): Refactoriza sistema de localización`
|
||||
* Un mensaje completo:
|
||||
```
|
||||
🎨 (bootsier): Mejora la asignación de clases
|
||||
|
||||
- Simplifica la generación de clases CSS para componentes Bootstrap.
|
||||
- Elimina duplicidades en enums de estilos y centraliza la lógica de composición
|
||||
para reducir errores y facilitar mantenimiento.
|
||||
- Alinea los nombres de variantes con la documentación pública.
|
||||
|
||||
Refs: PR #123
|
||||
```
|
||||
|
||||
El emoji puede usarse en formato Unicode o como *shortcode*, por ejemplo `:sparkles:` en vez de ✨.
|
||||
|
||||
Consulta la especificación oficial en https://gitmoji.dev/specification
|
||||
|
||||
Durante la integración, los *commits* pueden ajustarse para adaptarse al historial del proyecto.
|
||||
|
||||
Un *commit* debe representar una unidad lógica de cambio. Usa mensajes claros y descriptivos.
|
||||
|
||||
|
||||
## 6. Comunicación y respeto
|
||||
|
||||
PageTop sigue un enfoque profesional y colaborativo:
|
||||
|
||||
* Sé respetuoso en revisiones y discusiones.
|
||||
* Acepta sugerencias técnicas como parte del proceso.
|
||||
* Recuerda que todas las contribuciones son revisadas con el objetivo de mejorar el proyecto.
|
||||
|
||||
Si tienes dudas sobre el proceso, abre una *issue* de tipo pregunta para tratar la cuestión en
|
||||
comunidad.
|
||||
|
||||
---
|
||||
|
||||
Gracias por contribuir a **PageTop** 🚀 Cada aportación contribuye a mejorar el proyecto.
|
||||
47
Cargo.lock
generated
47
Cargo.lock
generated
|
|
@ -982,6 +982,17 @@ dependencies = [
|
|||
"wasi 0.14.7+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getter-methods"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c43d815f896a3c730f0d76b8348a1700dc8d8fd6c377e4590d531bdd646574d8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
|
|
@ -1073,9 +1084,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
|
|
@ -1279,19 +1290,22 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.4"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.6"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
|
|
@ -1554,19 +1568,19 @@ dependencies = [
|
|||
"actix-web",
|
||||
"chrono",
|
||||
"colored",
|
||||
"concat-string",
|
||||
"config",
|
||||
"figlet-rs",
|
||||
"fluent-templates",
|
||||
"indoc",
|
||||
"getter-methods",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"pagetop-aliner",
|
||||
"pagetop-bootsier",
|
||||
"pagetop-build",
|
||||
"pagetop-macros",
|
||||
"pagetop-minimal",
|
||||
"pagetop-statics",
|
||||
"parking_lot",
|
||||
"pastey",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"substring",
|
||||
|
|
@ -1614,6 +1628,15 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pagetop-minimal"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"concat-string",
|
||||
"indoc",
|
||||
"pastey",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pagetop-statics"
|
||||
version = "0.1.2"
|
||||
|
|
@ -1651,9 +1674,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
checksum = "57d6c094ee800037dff99e02cab0eaf3142826586742a270ab3d7a62656bd27a"
|
||||
|
||||
[[package]]
|
||||
name = "path-matchers"
|
||||
|
|
|
|||
|
|
@ -17,13 +17,12 @@ authors.workspace = true
|
|||
[dependencies]
|
||||
chrono = "0.4"
|
||||
colored = "3.0"
|
||||
concat-string = "1.0"
|
||||
config = { version = "0.15", default-features = false, features = ["toml"] }
|
||||
figlet-rs = "0.1"
|
||||
indoc = "2.0"
|
||||
getter-methods = "2.0"
|
||||
itoa = "1.0"
|
||||
indexmap = "2.12"
|
||||
parking_lot = "0.12"
|
||||
paste = { package = "pastey", version = "0.1" }
|
||||
substring = "1.4"
|
||||
terminal_size = "0.4"
|
||||
|
||||
|
|
@ -42,6 +41,7 @@ actix-web-files = { package = "actix-files", version = "0.6" }
|
|||
serde.workspace = true
|
||||
|
||||
pagetop-macros.workspace = true
|
||||
pagetop-minimal.workspace = true
|
||||
pagetop-statics.workspace = true
|
||||
|
||||
[features]
|
||||
|
|
@ -64,6 +64,7 @@ members = [
|
|||
# Helpers
|
||||
"helpers/pagetop-build",
|
||||
"helpers/pagetop-macros",
|
||||
"helpers/pagetop-minimal",
|
||||
"helpers/pagetop-statics",
|
||||
# Extensions
|
||||
"extensions/pagetop-aliner",
|
||||
|
|
@ -82,6 +83,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
# Helpers
|
||||
pagetop-build = { version = "0.3", path = "helpers/pagetop-build" }
|
||||
pagetop-macros = { version = "0.2", path = "helpers/pagetop-macros" }
|
||||
pagetop-minimal = { version = "0.0", path = "helpers/pagetop-minimal" }
|
||||
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
||||
# Extensions
|
||||
pagetop-aliner = { version = "0.0", path = "extensions/pagetop-aliner" }
|
||||
|
|
|
|||
156
MAINTAINERS.md
Normal file
156
MAINTAINERS.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# MAINTAINERS.md
|
||||
|
||||
## Guía para mantenedores de PageTop
|
||||
|
||||
Este documento describe **el flujo técnico interno de revisión e integración de contribuciones** en
|
||||
**PageTop**.
|
||||
|
||||
Está dirigido a **mantenedores del proyecto** y **no forma parte de la guía de contribución para
|
||||
usuarios externos**. Su objetivo es servir como **referencia operativa**, garantizando coherencia,
|
||||
trazabilidad y preservación de la autoría en un entorno con repositorios espejo.
|
||||
|
||||
|
||||
## 1. Repositorios y principios
|
||||
|
||||
PageTop mantiene **un único repositorio oficial**:
|
||||
|
||||
* **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop
|
||||
* **Repositorio espejo:** https://github.com/manuelcillero/pagetop
|
||||
|
||||
### Principios clave
|
||||
|
||||
* El repositorio oficial **es la única fuente de verdad** del historial.
|
||||
* **Nunca se realizan *merges* en GitHub**.
|
||||
* Toda integración definitiva se realiza en el repositorio oficial.
|
||||
* La autoría original debe preservarse siempre.
|
||||
|
||||
|
||||
## 2. Configuración local recomendada
|
||||
|
||||
El remoto `github` debe configurarse únicamente para operaciones de lectura (*fetch*), con la URL de
|
||||
*push* deshabilitada para evitar publicaciones accidentales en el repositorio espejo.
|
||||
|
||||
Estado esperado de `git remote -v`:
|
||||
|
||||
```text
|
||||
origin git@git.cillero.es:manuelcillero/pagetop.git (fetch)
|
||||
origin git@git.cillero.es:manuelcillero/pagetop.git (push)
|
||||
github git@github.com:manuelcillero/pagetop.git (fetch)
|
||||
github DISABLED (push)
|
||||
```
|
||||
|
||||
Convenciones usadas en este documento:
|
||||
|
||||
* `origin` -> Repositorio oficial
|
||||
* `github` -> Repositorio espejo
|
||||
|
||||
|
||||
## 3. Recepción y revisión de Pull Requests
|
||||
|
||||
Las PRs externas llegan por GitHub, normalmente contra la rama `main`.
|
||||
|
||||
Se asume que el repositorio local está configurado para recuperar PRs de GitHub como referencias
|
||||
remotas (`refs/pull/<N>/head`):
|
||||
|
||||
```bash
|
||||
git fetch github --prune
|
||||
git checkout -b pr-123 github/pr/123
|
||||
```
|
||||
|
||||
Antes de integrar:
|
||||
|
||||
* Revisar el código manualmente.
|
||||
* Verificar formato, análisis y pruebas:
|
||||
|
||||
```bash
|
||||
cargo fmt
|
||||
cargo clippy
|
||||
cargo test
|
||||
```
|
||||
|
||||
* Comprobar impacto en documentación.
|
||||
* Evaluar coherencia con la arquitectura y el estilo del proyecto.
|
||||
|
||||
Los cambios adicionales se solicitan o se aplican explicando claramente el motivo.
|
||||
|
||||
|
||||
## 4. Estrategia de integración
|
||||
|
||||
La integración **se realiza siempre en el repositorio oficial** (`origin`).
|
||||
|
||||
### 4.1 Estrategia por defecto: *rebase* + *fast-forward*
|
||||
|
||||
Esta es la **estrategia estándar y recomendada** en PageTop. Ventajas:
|
||||
|
||||
* conserva los commits originales,
|
||||
* preserva la autoría real de cada cambio,
|
||||
* mantiene un historial lineal y trazable,
|
||||
* facilita auditoría y depuración.
|
||||
|
||||
Procedimiento típico:
|
||||
|
||||
```bash
|
||||
git checkout pr-123
|
||||
git rebase main
|
||||
|
||||
# Resolver conflictos si los hay
|
||||
|
||||
git checkout main
|
||||
git merge --ff-only pr-123
|
||||
```
|
||||
|
||||
Si `merge --ff-only` falla, **no se debe continuar**, indica divergencias que deben resolverse antes
|
||||
de integrar la PR.
|
||||
|
||||
### 4.2 Estrategia excepcional: *Squash*
|
||||
|
||||
Sólo debe usarse cuando esté justificado:
|
||||
|
||||
* la PR contiene múltiples commits de prueba o ruido,
|
||||
* el historial aportado no es significativo,
|
||||
* el cambio es pequeño y autocontenido.
|
||||
|
||||
En este caso, se debe **preservar explícitamente la autoría**:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git merge --squash pr-123
|
||||
git commit --author="Nombre Apellido <email@ejemplo.com>"
|
||||
```
|
||||
|
||||
|
||||
### 4.3. Publicación en el repositorio oficial
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Este *push* representa la **integración definitiva** del cambio en la rama `main`.
|
||||
|
||||
|
||||
## 5. Cierre de la PR y sincronización
|
||||
|
||||
Tras integrar el cambio en el repositorio oficial, se cierra manualmente la PR en GitHub con un
|
||||
mensaje estándar:
|
||||
|
||||
```text
|
||||
Gracias por tu contribución.
|
||||
|
||||
Este cambio ha sido integrado en el repositorio oficial en `main` (`<hash>`).
|
||||
GitHub actúa como repositorio espejo sincronizado.
|
||||
```
|
||||
|
||||
|
||||
## 6. Principios de mantenimiento
|
||||
|
||||
* Priorizar **claridad y trazabilidad** frente a rapidez.
|
||||
* Mantener un historial legible y significativo.
|
||||
* Documentar cambios estructurales o públicos.
|
||||
* Tratar las contribuciones externas con respeto y transparencia.
|
||||
|
||||
---
|
||||
|
||||
Este documento puede evolucionar con el proyecto.
|
||||
|
||||
No se trata de imponer rigidez, sino de **capturar el conocimiento operativo real** de PageTop como
|
||||
guía práctica para el mantenimiento.
|
||||
53
README.md
53
README.md
|
|
@ -29,7 +29,7 @@ según las necesidades de cada proyecto, incluyendo:
|
|||
componentes sin comprometer su funcionalidad.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
La aplicación más sencilla de PageTop se ve así:
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ Este programa implementa una extensión llamada `HelloWorld` que sirve una pági
|
|||
(`/`) mostrando el texto "Hello world!" dentro de un elemento HTML `<h1>`.
|
||||
|
||||
|
||||
# 📂 Repositorio
|
||||
## 📂 Proyecto
|
||||
|
||||
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
||||
|
||||
|
|
@ -82,21 +82,25 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
|
|||
fuente de la librería principal. Reúne algunos de los *crates* más estables y populares del
|
||||
ecosistema Rust para proporcionar APIs y recursos para la creación avanzada de soluciones web.
|
||||
|
||||
## Auxiliares
|
||||
|
||||
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
|
||||
es la librería que permite incluir archivos estáticos en el ejecutable de las aplicaciones
|
||||
PageTop para servirlos de forma eficiente, con detección de cambios que optimizan el tiempo de
|
||||
compilación.
|
||||
### Auxiliares
|
||||
|
||||
* **[pagetop-build](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build)**,
|
||||
prepara los archivos estáticos o archivos SCSS compilados para incluirlos en el binario de las
|
||||
aplicaciones PageTop durante la compilación de los ejecutables.
|
||||
|
||||
* **[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 procedurales que mejoran la experiencia de desarrollo con
|
||||
PageTop.
|
||||
|
||||
## Extensiones
|
||||
* **[pagetop-minimal](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-minimal)**,
|
||||
ofrece macros declarativas esenciales para optimizar tareas comunes como la composición de
|
||||
texto, la concatenación de cadenas y el manejo de colecciones clave-valor.
|
||||
|
||||
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
|
||||
permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para servirlos
|
||||
de forma eficiente, con detección de cambios que optimizan el tiempo de compilación.
|
||||
|
||||
### Extensiones
|
||||
|
||||
* **[pagetop-aliner](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner)**,
|
||||
es un tema para demos y pruebas que muestra esquemáticamente la composición de las páginas HTML.
|
||||
|
|
@ -106,7 +110,7 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
|
|||
componentes flexibles.
|
||||
|
||||
|
||||
# 🧪 Pruebas
|
||||
## 🧪 Pruebas
|
||||
|
||||
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
||||
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
|
||||
|
|
@ -123,14 +127,14 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
|
|||
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
@ -144,7 +148,28 @@ Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el es
|
|||
el ecosistema Rust.
|
||||
|
||||
|
||||
# ✨ Contribuir
|
||||
## ✨ Contribuir
|
||||
|
||||
PageTop mantiene **un único repositorio oficial**:
|
||||
|
||||
* **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop
|
||||
* **Repositorio espejo:** https://github.com/manuelcillero/pagetop
|
||||
|
||||
El repositorio de GitHub actúa como espejo y punto de entrada para:
|
||||
|
||||
* dar mayor visibilidad al proyecto,
|
||||
* facilitar la participación de la comunidad,
|
||||
* centralizar *issues* y *pull requests* externas.
|
||||
|
||||
Aunque GitHub permite abrir *pull requests*, **la integración del código se realiza únicamente en el
|
||||
repositorio oficial**. El repositorio de GitHub se sincroniza posteriormente para reflejar el mismo
|
||||
estado.
|
||||
|
||||
En todos los casos, se respeta la **autoría original** de las contribuciones integradas, tanto en el
|
||||
historial como en la documentación asociada al cambio.
|
||||
|
||||
Para conocer el proceso completo de participación, revisión e integración de cambios, consulta el
|
||||
archivo [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
|
||||
Cualquier contribución para añadir al proyecto se considerará automáticamente bajo la doble licencia
|
||||
indicada arriba (MIT o Apache v2.0), sin términos o condiciones adicionales, tal y como permite la
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ async fn hello_name(
|
|||
) -> ResultPage<Markup, ErrorPage> {
|
||||
let name = path.into_inner();
|
||||
Page::new(request)
|
||||
.add_child(Html::with(move |_| html! { h1 { "Hello " (name) "!" } }))
|
||||
.add_child(Html::with(move |_| {
|
||||
html! {
|
||||
h1 style="text-align: center;" { "Hello " (name) "!" }
|
||||
}
|
||||
}))
|
||||
.render()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ impl Extension for HelloWorld {
|
|||
|
||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||
.add_child(Html::with(|_| {
|
||||
html! {
|
||||
h1 style="text-align: center;" { "Hello World!" }
|
||||
}
|
||||
}))
|
||||
.render()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,22 +10,16 @@ impl Extension for SuperMenu {
|
|||
}
|
||||
|
||||
fn initialize(&self) {
|
||||
let home_path = |cx: &Context| match cx.langid().language.as_str() {
|
||||
"en" => "/en",
|
||||
_ => "/",
|
||||
};
|
||||
|
||||
let navbar_menu = Navbar::brand_left(navbar::Brand::new().with_path(Some(home_path)))
|
||||
let navbar_menu = Navbar::brand_left(navbar::Brand::new())
|
||||
.with_expand(BreakPoint::LG)
|
||||
.add_item(navbar::Item::nav(
|
||||
Nav::new()
|
||||
.add_item(nav::Item::link(
|
||||
L10n::l("sample_menus_item_link"),
|
||||
home_path,
|
||||
))
|
||||
.add_item(nav::Item::link(L10n::l("sample_menus_item_link"), |cx| {
|
||||
cx.route("/")
|
||||
}))
|
||||
.add_item(nav::Item::link_blank(
|
||||
L10n::l("sample_menus_item_blank"),
|
||||
|_| "https://docs.rs/pagetop",
|
||||
|_| "https://docs.rs/pagetop".into(),
|
||||
))
|
||||
.add_item(nav::Item::dropdown(
|
||||
Dropdown::new()
|
||||
|
|
@ -33,28 +27,28 @@ impl Extension for SuperMenu {
|
|||
.add_item(dropdown::Item::header(L10n::l("sample_menus_dev_header")))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_dev_getting_started"),
|
||||
|_| "/dev/getting-started",
|
||||
|cx| cx.route("/dev/getting-started"),
|
||||
))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_dev_guides"),
|
||||
|_| "/dev/guides",
|
||||
|cx| cx.route("/dev/guides"),
|
||||
))
|
||||
.add_item(dropdown::Item::link_blank(
|
||||
L10n::l("sample_menus_dev_forum"),
|
||||
|_| "https://forum.example.dev",
|
||||
|_| "https://forum.example.dev".into(),
|
||||
))
|
||||
.add_item(dropdown::Item::divider())
|
||||
.add_item(dropdown::Item::header(L10n::l("sample_menus_sdk_header")))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_sdk_rust"),
|
||||
|_| "/dev/sdks/rust",
|
||||
|cx| cx.route("/dev/sdks/rust"),
|
||||
))
|
||||
.add_item(dropdown::Item::link(L10n::l("sample_menus_sdk_js"), |_| {
|
||||
"/dev/sdks/js"
|
||||
.add_item(dropdown::Item::link(L10n::l("sample_menus_sdk_js"), |cx| {
|
||||
cx.route("/dev/sdks/js")
|
||||
}))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_sdk_python"),
|
||||
|_| "/dev/sdks/python",
|
||||
|cx| cx.route("/dev/sdks/python"),
|
||||
))
|
||||
.add_item(dropdown::Item::divider())
|
||||
.add_item(dropdown::Item::header(L10n::l(
|
||||
|
|
@ -62,22 +56,22 @@ impl Extension for SuperMenu {
|
|||
)))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_plugin_auth"),
|
||||
|_| "/dev/sdks/rust/plugins/auth",
|
||||
|cx| cx.route("/dev/sdks/rust/plugins/auth"),
|
||||
))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_plugin_cache"),
|
||||
|_| "/dev/sdks/rust/plugins/cache",
|
||||
|cx| cx.route("/dev/sdks/rust/plugins/cache"),
|
||||
))
|
||||
.add_item(dropdown::Item::divider())
|
||||
.add_item(dropdown::Item::label(L10n::l("sample_menus_item_label")))
|
||||
.add_item(dropdown::Item::link_disabled(
|
||||
L10n::l("sample_menus_item_disabled"),
|
||||
|_| "#",
|
||||
|cx| cx.route("#"),
|
||||
)),
|
||||
))
|
||||
.add_item(nav::Item::link_disabled(
|
||||
L10n::l("sample_menus_item_disabled"),
|
||||
|_| "#",
|
||||
|cx| cx.route("#"),
|
||||
)),
|
||||
))
|
||||
.add_item(navbar::Item::nav(
|
||||
|
|
@ -88,14 +82,14 @@ impl Extension for SuperMenu {
|
|||
)
|
||||
.add_item(nav::Item::link(
|
||||
L10n::l("sample_menus_item_sign_up"),
|
||||
|_| "/auth/sign-up",
|
||||
|cx| cx.route("/auth/sign-up"),
|
||||
))
|
||||
.add_item(nav::Item::link(L10n::l("sample_menus_item_login"), |_| {
|
||||
"/auth/login"
|
||||
.add_item(nav::Item::link(L10n::l("sample_menus_item_login"), |cx| {
|
||||
cx.route("/auth/login")
|
||||
})),
|
||||
));
|
||||
|
||||
InRegion::Named("header").add(Child::with(
|
||||
InRegion::Global(&DefaultRegion::Header).add(Child::with(
|
||||
Container::new()
|
||||
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
||||
.add_child(navbar_menu),
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@
|
|||
<br>
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
|
|
@ -80,14 +80,14 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
```
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -104,12 +104,25 @@ impl Extension for Aliner {
|
|||
}
|
||||
|
||||
impl Theme for Aliner {
|
||||
fn after_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_param("include_basic_assets", true)
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/aliner/css/styles.css")
|
||||
.with_version(env!("CARGO_PKG_VERSION"))
|
||||
.with_weight(-90),
|
||||
));
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/basic.css")
|
||||
.with_version(PAGETOP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/aliner/css/styles.css")
|
||||
.with_version(env!("CARGO_PKG_VERSION"))
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@
|
|||
<br>
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
|
|
@ -80,14 +80,14 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
```
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,34 @@ pub mod prelude {
|
|||
pub use crate::theme::*;
|
||||
}
|
||||
|
||||
/// Plantillas que Bootsier añade.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum BootsierTemplate {
|
||||
/// Plantilla predeterminada de Bootsier.
|
||||
#[default]
|
||||
Standard,
|
||||
}
|
||||
|
||||
impl Template for BootsierTemplate {
|
||||
fn render(&'static self, cx: &mut Context) -> Markup {
|
||||
match self {
|
||||
Self::Standard => theme::Container::new()
|
||||
.with_classes(ClassesOp::Add, "container-wrapper")
|
||||
.with_width(theme::container::Width::FluidMax(
|
||||
config::SETTINGS.bootsier.max_width,
|
||||
))
|
||||
.add_child(Html::with(|cx| {
|
||||
html! {
|
||||
(DefaultRegion::Header.render(cx))
|
||||
(DefaultRegion::Content.render(cx))
|
||||
(DefaultRegion::Footer.render(cx))
|
||||
}
|
||||
})),
|
||||
}
|
||||
.render(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementa el tema.
|
||||
pub struct Bootsier;
|
||||
|
||||
|
|
@ -117,7 +145,12 @@ impl Extension for Bootsier {
|
|||
}
|
||||
|
||||
impl Theme for Bootsier {
|
||||
fn after_render_page_body(&self, page: &mut Page) {
|
||||
#[inline]
|
||||
fn default_template(&self) -> TemplateRef {
|
||||
&BootsierTemplate::Standard
|
||||
}
|
||||
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/bootsier/bs/bootstrap.min.css")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
|
|
@ -127,6 +160,10 @@ impl Theme for Bootsier {
|
|||
JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
));
|
||||
))
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
e404-description = Oops! Page Not Found
|
||||
e404-message = The page you are looking for may have been removed, had its name changed, or is temporarily unavailable.
|
||||
e500-description = Oops! Unexpected Error
|
||||
e500-message = We're having an issue. Please report this error to an administrator.
|
||||
back-homepage = Back to homepage
|
||||
e404_description = Oops! Page Not Found
|
||||
e404_message = The page you are looking for may have been removed, had its name changed, or is temporarily unavailable.
|
||||
e500_description = Oops! Unexpected Error
|
||||
e500_message = We're having an issue. Please report this error to an administrator.
|
||||
back_homepage = Back to homepage
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Toggle Dropdown
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Close
|
||||
# form::Input
|
||||
input_required = This field is required
|
||||
|
||||
# Navbar
|
||||
toggle = Toggle navigation
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Close
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
header = Header
|
||||
nav_branding = Navigation branding region
|
||||
nav_main = Main navigation region
|
||||
nav_additional = Additional navigation region (eg search form, social icons, etc)
|
||||
breadcrumb = Breadcrumb
|
||||
content = Main content
|
||||
sidebar_first = Sidebar first
|
||||
sidebar_second = Sidebar second
|
||||
footer = Footer
|
||||
region_header = Header
|
||||
region_nav_branding = Navigation branding region
|
||||
region_nav_main = Main navigation region
|
||||
region_nav_additional = Additional navigation region (eg search form, social icons, etc)
|
||||
region_breadcrumb = Breadcrumb
|
||||
region_content = Main content
|
||||
region_sidebar_first = Sidebar first
|
||||
region_sidebar_second = Sidebar second
|
||||
region_footer = Footer
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
e404-description = ¡Vaya! Página No Encontrada
|
||||
e404-message = La página que está buscando puede haber sido eliminada, cambiada de nombre o no está disponible temporalmente.
|
||||
e500-description = ¡Vaya! Error Inesperado
|
||||
e500-message = Está ocurriendo una incidencia. Por favor, informe de este error a un administrador.
|
||||
back-homepage = Volver al inicio
|
||||
e404_description = ¡Vaya! Página No Encontrada
|
||||
e404_message = La página que está buscando puede haber sido eliminada, cambiada de nombre o no está disponible temporalmente.
|
||||
e500_description = ¡Vaya! Error Inesperado
|
||||
e500_message = Está ocurriendo una incidencia. Por favor, informe de este error a un administrador.
|
||||
back_homepage = Volver al inicio
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Mostrar/ocultar menú
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Cerrar
|
||||
# form::Input
|
||||
input_required = Este campo es obligatorio
|
||||
|
||||
# Navbar
|
||||
toggle = Mostrar/ocultar navegación
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Cerrar
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
header = Cabecera
|
||||
nav_branding = Navegación y marca
|
||||
nav_main = Navegación principal
|
||||
nav_additional = Navegación adicional (p.e. formulario de búsqueda, iconos sociales, etc.)
|
||||
breadcrumb = Ruta de posicionamiento
|
||||
content = Contenido principal
|
||||
sidebar_first = Barra lateral primera
|
||||
sidebar_second = Barra lateral segunda
|
||||
footer = Pie
|
||||
region_header = Cabecera
|
||||
region_nav_branding = Navegación y marca
|
||||
region_nav_main = Navegación principal
|
||||
region_nav_additional = Navegación adicional (p.e. formulario de búsqueda, iconos sociales, etc.)
|
||||
region_breadcrumb = Ruta de posicionamiento
|
||||
region_content = Contenido principal
|
||||
region_sidebar_first = Barra lateral primera
|
||||
region_sidebar_second = Barra lateral segunda
|
||||
region_footer = Pie
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ pub mod dropdown;
|
|||
#[doc(inline)]
|
||||
pub use dropdown::Dropdown;
|
||||
|
||||
// Form.
|
||||
pub mod form;
|
||||
#[doc(inline)]
|
||||
pub use form::Form;
|
||||
|
||||
// Image.
|
||||
pub mod image;
|
||||
#[doc(inline)]
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ impl BorderColor {
|
|||
const BORDER: &str = "border";
|
||||
const BORDER_PREFIX: &str = "border-";
|
||||
|
||||
// Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase.
|
||||
/// Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -35,7 +35,7 @@ impl BorderColor {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase `border-*` a la cadena de clases.
|
||||
/// Añade la clase `border-*` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -64,7 +64,6 @@ impl BorderColor {
|
|||
/// assert_eq!(BorderColor::Black.to_class(), "border-black");
|
||||
/// assert_eq!(BorderColor::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = match self {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub enum BreakPoint {
|
|||
}
|
||||
|
||||
impl BreakPoint {
|
||||
// Devuelve la identificación del punto de ruptura.
|
||||
/// Devuelve la identificación del punto de ruptura.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
|
|
@ -33,11 +33,11 @@ impl BreakPoint {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade el punto de ruptura con un prefijo y un sufijo (opcional) separados por un guion `-` a
|
||||
// la cadena de clases.
|
||||
//
|
||||
// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
||||
// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
||||
/// Añade el punto de ruptura con un prefijo y un sufijo (opcional) separados por un guion `-` a
|
||||
/// la cadena de clases.
|
||||
///
|
||||
/// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
||||
/// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str, suffix: &str) {
|
||||
if prefix.is_empty() {
|
||||
|
|
@ -60,30 +60,30 @@ impl BreakPoint {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve la clase para el punto de ruptura, con un prefijo y un sufijo opcional, separados
|
||||
// por un guion `-`.
|
||||
//
|
||||
// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
||||
// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
||||
// - Si `prefix` está vacío devuelve `""`.
|
||||
//
|
||||
// # Ejemplos
|
||||
//
|
||||
// ```rust
|
||||
// # use pagetop_bootsier::prelude::*;
|
||||
// let bp = BreakPoint::MD;
|
||||
// assert_eq!(bp.class_with("col", ""), "col-md");
|
||||
// assert_eq!(bp.class_with("col", "6"), "col-md-6");
|
||||
//
|
||||
// let bp = BreakPoint::None;
|
||||
// assert_eq!(bp.class_with("offcanvas", ""), "offcanvas");
|
||||
// assert_eq!(bp.class_with("col", "12"), "col-12");
|
||||
//
|
||||
// let bp = BreakPoint::LG;
|
||||
// assert_eq!(bp.class_with("", "3"), "");
|
||||
// ```
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, prefix: &str, suffix: &str) -> String {
|
||||
/// Devuelve la clase para el punto de ruptura, con un prefijo y un sufijo opcional, separados
|
||||
/// por un guion `-`.
|
||||
///
|
||||
/// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
||||
/// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
||||
/// - Si `prefix` está vacío devuelve `""`.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let bp = BreakPoint::MD;
|
||||
/// assert_eq!(bp.class_with("col", ""), "col-md");
|
||||
/// assert_eq!(bp.class_with("col", "6"), "col-md-6");
|
||||
///
|
||||
/// let bp = BreakPoint::None;
|
||||
/// assert_eq!(bp.class_with("offcanvas", ""), "offcanvas");
|
||||
/// assert_eq!(bp.class_with("col", "12"), "col-12");
|
||||
///
|
||||
/// let bp = BreakPoint::LG;
|
||||
/// assert_eq!(bp.class_with("", "3"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, prefix: &str, suffix: &str) -> String {
|
||||
if prefix.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ impl ButtonColor {
|
|||
const BTN_OUTLINE_PREFIX: &str = "btn-outline-";
|
||||
const BTN_LINK: &str = "btn-link";
|
||||
|
||||
// Añade la clase `btn-*` a la cadena de clases.
|
||||
/// Añade la clase `btn-*` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Self::Default = self {
|
||||
|
|
@ -65,7 +65,6 @@ impl ButtonColor {
|
|||
/// assert_eq!(ButtonColor::Link.to_class(), "btn-link");
|
||||
/// assert_eq!(ButtonColor::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
match self {
|
||||
Self::Default => String::new(),
|
||||
|
|
@ -106,7 +105,7 @@ impl ButtonSize {
|
|||
const BTN_SM: &str = "btn-sm";
|
||||
const BTN_LG: &str = "btn-lg";
|
||||
|
||||
// Añade la clase de tamaño `btn-sm` o `btn-lg` a la cadena de clases.
|
||||
/// Añade la clase de tamaño `btn-sm` o `btn-lg` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Self::Default = self {
|
||||
|
|
@ -132,7 +131,6 @@ impl ButtonSize {
|
|||
/// assert_eq!(ButtonSize::Large.to_class(), "btn-lg");
|
||||
/// assert_eq!(ButtonSize::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
match self {
|
||||
Self::Default => String::new(),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ pub enum Color {
|
|||
}
|
||||
|
||||
impl Color {
|
||||
// Devuelve el nombre del color.
|
||||
/// Devuelve el nombre del color.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
|
|
@ -94,7 +94,7 @@ impl Opacity {
|
|||
const OPACITY: &str = "opacity";
|
||||
const OPACITY_PREFIX: &str = "-opacity";
|
||||
|
||||
// Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase.
|
||||
/// Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -109,8 +109,8 @@ impl Opacity {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la opacidad a la cadena de clases usando el prefijo dado (`bg`, `border`, `text`, o
|
||||
// vacío para `opacity-*`).
|
||||
/// Añade la opacidad a la cadena de clases usando el prefijo dado (`bg`, `border`, `text`, o
|
||||
/// vacío para `opacity-*`).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -127,20 +127,20 @@ impl Opacity {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve la clase de opacidad con el prefijo dado (`bg`, `border`, `text`, o vacío para
|
||||
// `opacity-*`).
|
||||
//
|
||||
// # Ejemplos
|
||||
//
|
||||
// ```rust
|
||||
// # use pagetop_bootsier::prelude::*;
|
||||
// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
|
||||
// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
|
||||
// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
|
||||
// assert_eq!(Opacity::Default.class_with("bg"), "");
|
||||
// ```
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, prefix: &str) -> String {
|
||||
/// Devuelve la clase de opacidad con el prefijo dado (`bg`, `border`, `text`, o vacío para
|
||||
/// `opacity-*`).
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
|
||||
/// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
|
||||
/// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
|
||||
/// assert_eq!(Opacity::Default.class_with("bg"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, prefix: &str) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = if prefix.is_empty() {
|
||||
Self::OPACITY.len()
|
||||
|
|
@ -206,7 +206,7 @@ impl ColorBg {
|
|||
const BG: &str = "bg";
|
||||
const BG_PREFIX: &str = "bg-";
|
||||
|
||||
// Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase.
|
||||
/// Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -223,7 +223,7 @@ impl ColorBg {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase de fondo `bg-*` a la cadena de clases.
|
||||
/// Añade la clase de fondo `bg-*` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -253,7 +253,6 @@ impl ColorBg {
|
|||
/// assert_eq!(ColorBg::Transparent.to_class(), "bg-transparent");
|
||||
/// assert_eq!(ColorBg::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = match self {
|
||||
|
|
@ -305,7 +304,7 @@ impl ColorText {
|
|||
const TEXT: &str = "text";
|
||||
const TEXT_PREFIX: &str = "text-";
|
||||
|
||||
// Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase.
|
||||
/// Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -322,7 +321,7 @@ impl ColorText {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase de texto `text-*` a la cadena de clases.
|
||||
/// Añade la clase de texto `text-*` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -352,7 +351,6 @@ impl ColorText {
|
|||
/// assert_eq!(ColorText::Black.to_class(), "text-black");
|
||||
/// assert_eq!(ColorText::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = match self {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ pub enum ScaleSize {
|
|||
}
|
||||
|
||||
impl ScaleSize {
|
||||
// Devuelve el sufijo para el tamaño (`"-0"`, `"-1"`, etc.), o `None` si no define ninguna
|
||||
// clase, o `""` para el tamaño automático.
|
||||
/// Devuelve el sufijo para el tamaño (`"-0"`, `"-1"`, etc.), o `None` si no define ninguna
|
||||
/// clase, o `""` para el tamaño automático.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -42,7 +42,7 @@ impl ScaleSize {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade el tamaño a la cadena de clases usando el prefijo dado.
|
||||
/// Añade el tamaño a la cadena de clases usando el prefijo dado.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
|
||||
if !prefix.is_empty() {
|
||||
|
|
@ -57,18 +57,18 @@ impl ScaleSize {
|
|||
}
|
||||
|
||||
/* Devuelve la clase del tamaño para el prefijo, o una cadena vacía si no aplica (reservado).
|
||||
//
|
||||
// # Ejemplo
|
||||
//
|
||||
// ```rust
|
||||
// # use pagetop_bootsier::prelude::*;
|
||||
// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
|
||||
// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
|
||||
// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
|
||||
// assert_eq!(ScaleSize::None.class_with("border"), "");
|
||||
// ```
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, prefix: &str) -> String {
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
|
||||
/// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
|
||||
/// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
|
||||
/// assert_eq!(ScaleSize::None.class_with("border"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, prefix: &str) -> String {
|
||||
if !prefix.is_empty() {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let mut class = String::with_capacity(prefix.len() + suffix.len());
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ pub enum RoundedRadius {
|
|||
impl RoundedRadius {
|
||||
const ROUNDED: &str = "rounded";
|
||||
|
||||
// Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el
|
||||
// redondeo por defecto.
|
||||
/// Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el
|
||||
/// redondeo por defecto.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -48,8 +48,8 @@ impl RoundedRadius {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`,
|
||||
// `rounded-bottom-start`, o vacío para `rounded-*`).
|
||||
/// Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`,
|
||||
/// `rounded-bottom-start`, o vacío para `rounded-*`).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -65,21 +65,21 @@ impl RoundedRadius {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve la clase para el redondeo de esquinas con el prefijo dado (`rounded-top`,
|
||||
// `rounded-bottom-start`, o vacío para `rounded-*`).
|
||||
//
|
||||
// # Ejemplos
|
||||
//
|
||||
// ```rust
|
||||
// # use pagetop_bootsier::prelude::*;
|
||||
// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
|
||||
// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
|
||||
// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
|
||||
// assert_eq!(RoundedRadius::Circle.class_with(""), "rounded-circle");
|
||||
// assert_eq!(RoundedRadius::None.class_with("rounded-bottom-start"), "");
|
||||
// ```
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, prefix: &str) -> String {
|
||||
/// Devuelve la clase para el redondeo de esquinas con el prefijo dado (`rounded-top`,
|
||||
/// `rounded-bottom-start`, o vacío para `rounded-*`).
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
|
||||
/// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
|
||||
/// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
|
||||
/// assert_eq!(RoundedRadius::Circle.class_with(""), "rounded-circle");
|
||||
/// assert_eq!(RoundedRadius::None.class_with("rounded-bottom-start"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, prefix: &str) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = if prefix.is_empty() {
|
||||
Self::ROUNDED.len()
|
||||
|
|
|
|||
|
|
@ -145,7 +145,6 @@ impl Border {
|
|||
/// `"border border-top-0 border-end-3 border-primary border-opacity-50"`, etc.).
|
||||
///
|
||||
/// Si no se define ningún tamaño, color ni opacidad, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ impl Background {
|
|||
/// Devuelve las clases de fondo como cadena (`"bg-primary"`, `"bg-body-secondary bg-opacity-50"`, etc.).
|
||||
///
|
||||
/// Si no se define ni color ni opacidad, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
|
|
@ -189,7 +188,6 @@ impl Text {
|
|||
/// etc.).
|
||||
///
|
||||
/// Si no se define ni color ni opacidad, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ impl Margin {
|
|||
|
||||
// **< Margin HELPERS >*************************************************************************
|
||||
|
||||
// Devuelve el prefijo `m*` según el lado.
|
||||
/// Devuelve el prefijo `m*` según el lado.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn side_prefix(&self) -> &'static str {
|
||||
|
|
@ -63,7 +63,7 @@ impl Margin {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase.
|
||||
/// Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn size_suffix(&self) -> Option<&'static str> {
|
||||
|
|
@ -80,8 +80,8 @@ impl Margin {
|
|||
}
|
||||
|
||||
/* Añade la clase de **margin** a la cadena de clases (reservado).
|
||||
//
|
||||
// No añade nada si `size` es `ScaleSize::None`.
|
||||
///
|
||||
/// No añade nada si `size` es `ScaleSize::None`.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let Some(size) = self.size_suffix() else {
|
||||
|
|
@ -94,7 +94,6 @@ impl Margin {
|
|||
/// Devuelve la clase de **margin** como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.).
|
||||
///
|
||||
/// Si `size` es `ScaleSize::None`, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let Some(size) = self.size_suffix() else {
|
||||
return String::new();
|
||||
|
|
@ -148,7 +147,7 @@ impl Padding {
|
|||
|
||||
// **< Padding HELPERS >************************************************************************
|
||||
|
||||
// Devuelve el prefijo `p*` según el lado.
|
||||
/// Devuelve el prefijo `p*` según el lado.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn prefix(&self) -> &'static str {
|
||||
|
|
@ -163,9 +162,9 @@ impl Padding {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve el sufijo del tamaño (`0`..`5`), o `None` si no define clase.
|
||||
//
|
||||
// Nota: `ScaleSize::Auto` **no aplica** a padding ⇒ devuelve `None`.
|
||||
/// Devuelve el sufijo del tamaño (`0`..`5`), o `None` si no define clase.
|
||||
///
|
||||
/// Nota: `ScaleSize::Auto` **no aplica** a padding ⇒ devuelve `None`.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(&self) -> Option<&'static str> {
|
||||
|
|
@ -182,8 +181,8 @@ impl Padding {
|
|||
}
|
||||
|
||||
/* Añade la clase de **padding** a la cadena de clases (reservado).
|
||||
//
|
||||
// No añade nada si `size` es `ScaleSize::None` o `ScaleSize::Auto`.
|
||||
///
|
||||
/// No añade nada si `size` es `ScaleSize::None` o `ScaleSize::Auto`.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let Some(size) = self.suffix() else {
|
||||
|
|
@ -192,10 +191,9 @@ impl Padding {
|
|||
self.breakpoint.push_class(classes, self.prefix(), size);
|
||||
} */
|
||||
|
||||
// Devuelve la clase de **padding** como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
|
||||
//
|
||||
// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`.
|
||||
#[inline]
|
||||
/// Devuelve la clase de **padding** como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
|
||||
///
|
||||
/// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`.
|
||||
pub fn to_class(self) -> String {
|
||||
let Some(size) = self.suffix() else {
|
||||
return String::new();
|
||||
|
|
|
|||
|
|
@ -160,7 +160,6 @@ impl Rounded {
|
|||
/// `"rounded-top rounded-bottom-start-4 rounded-bottom-end-circle"`, etc.).
|
||||
///
|
||||
/// Si no se define ningún radio, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
|
|
|
|||
|
|
@ -6,19 +6,23 @@ use crate::prelude::*;
|
|||
///
|
||||
/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza
|
||||
/// si existen componentes hijos (*children*).
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Container {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
container_kind : container::Kind,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al contenedor.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo semántico del contenedor.
|
||||
container_kind: container::Kind,
|
||||
/// Devuelve el comportamiento para el ancho del contenedor.
|
||||
container_width: container::Width,
|
||||
children : Children,
|
||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Container {
|
||||
fn new() -> Self {
|
||||
Container::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -26,7 +30,7 @@ impl Component for Container {
|
|||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, self.width().to_class());
|
||||
self.alter_classes(ClassesOp::Prepend, self.container_width().to_class());
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
|
|
@ -34,9 +38,9 @@ impl Component for Container {
|
|||
if output.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
let style = match self.width() {
|
||||
let style = match self.container_width() {
|
||||
container::Width::FluidMax(w) if w.is_measurable() => {
|
||||
Some(join!("max-width: ", w.to_string(), ";"))
|
||||
Some(util::join!("max-width: ", w.to_string(), ";"))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
|
@ -78,7 +82,7 @@ impl Component for Container {
|
|||
impl Container {
|
||||
/// Crea un contenedor de tipo `Main` (`<main>`).
|
||||
pub fn main() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Main,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -86,7 +90,7 @@ impl Container {
|
|||
|
||||
/// Crea un contenedor de tipo `Header` (`<header>`).
|
||||
pub fn header() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Header,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -94,7 +98,7 @@ impl Container {
|
|||
|
||||
/// Crea un contenedor de tipo `Footer` (`<footer>`).
|
||||
pub fn footer() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Footer,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -102,7 +106,7 @@ impl Container {
|
|||
|
||||
/// Crea un contenedor de tipo `Section` (`<section>`).
|
||||
pub fn section() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Section,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -110,7 +114,7 @@ impl Container {
|
|||
|
||||
/// Crea un contenedor de tipo `Article` (`<article>`).
|
||||
pub fn article() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Article,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -121,7 +125,7 @@ impl Container {
|
|||
/// Establece el identificador único (`id`) del contenedor.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +139,7 @@ impl Container {
|
|||
/// - Redondear las esquinas ([`classes::Rounded`]).
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -159,26 +163,4 @@ impl Container {
|
|||
self.children.alter_child(op);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Container GETTERS >**********************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al contenedor.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el tipo semántico del contenedor.
|
||||
pub fn container_kind(&self) -> &container::Kind {
|
||||
&self.container_kind
|
||||
}
|
||||
|
||||
/// Devuelve el comportamiento para el ancho del contenedor.
|
||||
pub fn width(&self) -> &container::Width {
|
||||
&self.container_width
|
||||
}
|
||||
|
||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||
pub fn children(&self) -> &Children {
|
||||
&self.children
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ impl Width {
|
|||
} */
|
||||
|
||||
/// Devuelve la clase asociada al comportamiento del contenedor según el ajuste de su ancho.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
match self {
|
||||
Self::Default => BreakPoint::None.class_with(Self::CONTAINER, ""),
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
//! .with_button_color(ButtonColor::Background(Color::Secondary))
|
||||
//! .with_auto_close(dropdown::AutoClose::ClickableInside)
|
||||
//! .with_direction(dropdown::Direction::Dropend)
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://www.google.es"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .add_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://google.es".into()))
|
||||
//! .add_item(dropdown::Item::divider())
|
||||
//! .add_item(dropdown::Item::header(L10n::n("User session")))
|
||||
//! .add_item(dropdown::Item::button(L10n::n("Sign out")));
|
||||
|
|
|
|||
|
|
@ -19,26 +19,37 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// Ver ejemplo en el módulo [`dropdown`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Dropdown {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
title : L10n,
|
||||
button_size : ButtonSize,
|
||||
button_color : ButtonColor,
|
||||
button_split : bool,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al menú desplegable.
|
||||
classes: Classes,
|
||||
/// Devuelve el título del menú desplegable.
|
||||
title: L10n,
|
||||
/// Devuelve el tamaño configurado del botón.
|
||||
button_size: ButtonSize,
|
||||
/// Devuelve el color/estilo configurado del botón.
|
||||
button_color: ButtonColor,
|
||||
/// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*).
|
||||
button_split: bool,
|
||||
/// Devuelve si el botón del menú está integrado en un grupo de botones.
|
||||
button_grouped: bool,
|
||||
auto_close : dropdown::AutoClose,
|
||||
direction : dropdown::Direction,
|
||||
menu_align : dropdown::MenuAlign,
|
||||
menu_position : dropdown::MenuPosition,
|
||||
items : Children,
|
||||
/// Devuelve la política de cierre automático del menú desplegado.
|
||||
auto_close: dropdown::AutoClose,
|
||||
/// Devuelve la dirección de despliegue configurada.
|
||||
direction: dropdown::Direction,
|
||||
/// Devuelve la configuración de alineación horizontal del menú desplegable.
|
||||
menu_align: dropdown::MenuAlign,
|
||||
/// Devuelve la posición configurada para el menú desplegable.
|
||||
menu_position: dropdown::MenuPosition,
|
||||
/// Devuelve la lista de elementos del menú.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
impl Component for Dropdown {
|
||||
fn new() -> Self {
|
||||
Dropdown::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -48,7 +59,7 @@ impl Component for Dropdown {
|
|||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(
|
||||
ClassesOp::Prepend,
|
||||
self.direction().class_with(self.button_grouped()),
|
||||
self.direction().class_with(*self.button_grouped()),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +76,7 @@ impl Component for Dropdown {
|
|||
PrepareMarkup::With(html! {
|
||||
div id=[self.id()] class=[self.classes().get()] {
|
||||
@if !title.is_empty() {
|
||||
@let mut btn_classes = AttrClasses::new({
|
||||
@let mut btn_classes = Classes::new({
|
||||
let mut classes = "btn".to_string();
|
||||
self.button_size().push_class(&mut classes);
|
||||
self.button_color().push_class(&mut classes);
|
||||
|
|
@ -75,14 +86,14 @@ impl Component for Dropdown {
|
|||
@let offset = pos.data_offset();
|
||||
@let reference = pos.data_reference();
|
||||
@let auto_close = self.auto_close.as_str();
|
||||
@let menu_classes = AttrClasses::new({
|
||||
@let menu_classes = Classes::new({
|
||||
let mut classes = "dropdown-menu".to_string();
|
||||
self.menu_align().push_class(&mut classes);
|
||||
classes
|
||||
});
|
||||
|
||||
// Renderizado en modo split (dos botones) o simple (un botón).
|
||||
@if self.button_split() {
|
||||
@if *self.button_split() {
|
||||
// Botón principal (acción/etiqueta).
|
||||
@let btn = html! {
|
||||
button
|
||||
|
|
@ -96,7 +107,7 @@ impl Component for Dropdown {
|
|||
@let btn_toggle = html! {
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.alter_value(
|
||||
class=[btn_classes.alter_classes(
|
||||
ClassesOp::Add, "dropdown-toggle dropdown-toggle-split"
|
||||
).get()]
|
||||
data-bs-toggle="dropdown"
|
||||
|
|
@ -127,7 +138,7 @@ impl Component for Dropdown {
|
|||
// Botón único con funcionalidad de *toggle*.
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.alter_value(
|
||||
class=[btn_classes.alter_classes(
|
||||
ClassesOp::Add, "dropdown-toggle"
|
||||
).get()]
|
||||
data-bs-toggle="dropdown"
|
||||
|
|
@ -155,14 +166,14 @@ impl Dropdown {
|
|||
/// Establece el identificador único (`id`) del menú desplegable.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al menú desplegable.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -242,61 +253,4 @@ impl Dropdown {
|
|||
self.items.alter_typed(op);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Dropdown GETTERS >***********************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al menú desplegable.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el título del menú desplegable.
|
||||
pub fn title(&self) -> &L10n {
|
||||
&self.title
|
||||
}
|
||||
|
||||
/// Devuelve el tamaño configurado del botón.
|
||||
pub fn button_size(&self) -> &ButtonSize {
|
||||
&self.button_size
|
||||
}
|
||||
|
||||
/// Devuelve el color/estilo configurado del botón.
|
||||
pub fn button_color(&self) -> &ButtonColor {
|
||||
&self.button_color
|
||||
}
|
||||
|
||||
/// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*).
|
||||
pub fn button_split(&self) -> bool {
|
||||
self.button_split
|
||||
}
|
||||
|
||||
/// Devuelve si el botón del menú está integrado en un grupo de botones.
|
||||
pub fn button_grouped(&self) -> bool {
|
||||
self.button_grouped
|
||||
}
|
||||
|
||||
/// Devuelve la política de cierre automático del menú desplegado.
|
||||
pub fn auto_close(&self) -> &dropdown::AutoClose {
|
||||
&self.auto_close
|
||||
}
|
||||
|
||||
/// Devuelve la dirección de despliegue configurada.
|
||||
pub fn direction(&self) -> &dropdown::Direction {
|
||||
&self.direction
|
||||
}
|
||||
|
||||
/// Devuelve la configuración de alineación horizontal del menú desplegable.
|
||||
pub fn menu_align(&self) -> &dropdown::MenuAlign {
|
||||
&self.menu_align
|
||||
}
|
||||
|
||||
/// Devuelve la posición configurada para el menú desplegable.
|
||||
pub fn menu_position(&self) -> &dropdown::MenuPosition {
|
||||
&self.menu_position
|
||||
}
|
||||
|
||||
/// Devuelve la lista de elementos (`children`) del menú.
|
||||
pub fn items(&self) -> &Children {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ pub enum ItemKind {
|
|||
Void,
|
||||
/// Etiqueta sin comportamiento interactivo.
|
||||
Label(L10n),
|
||||
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
|
||||
/// inicialmente deshabilitado.
|
||||
/// Elemento de navegación basado en una [`RoutePath`] dinámica devuelta por
|
||||
/// [`FnPathByContext`]. Opcionalmente, puede abrirse en una nueva ventana y estar inicialmente
|
||||
/// deshabilitado.
|
||||
Link {
|
||||
label: L10n,
|
||||
path: FnPathByContext,
|
||||
route: FnPathByContext,
|
||||
blank: bool,
|
||||
disabled: bool,
|
||||
},
|
||||
|
|
@ -40,19 +41,21 @@ pub enum ItemKind {
|
|||
/// visible que puede comportarse como texto, enlace, botón, encabezado o separador, según su
|
||||
/// [`ItemKind`].
|
||||
///
|
||||
/// Permite definir identificador, clases de estilo adicionales o tipo de interacción asociada,
|
||||
/// manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Item {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al elemento.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo de elemento representado.
|
||||
item_kind: ItemKind,
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Item::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -73,13 +76,13 @@ impl Component for Item {
|
|||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let path = path(cx);
|
||||
let route_link = route(cx);
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = !*disabled && (current_path == Some(path));
|
||||
let is_current = !*disabled && (current_path == Some(route_link.path()));
|
||||
|
||||
let mut classes = "dropdown-item".to_string();
|
||||
if is_current {
|
||||
|
|
@ -89,9 +92,9 @@ impl Component for Item {
|
|||
classes.push_str(" disabled");
|
||||
}
|
||||
|
||||
let href = (!disabled).then_some(path);
|
||||
let target = (!disabled && *blank).then_some("_blank");
|
||||
let rel = (!disabled && *blank).then_some("noopener noreferrer");
|
||||
let href = (!*disabled).then_some(route_link);
|
||||
let target = (!*disabled && *blank).then_some("_blank");
|
||||
let rel = (!*disabled && *blank).then_some("noopener noreferrer");
|
||||
|
||||
let aria_current = (href.is_some() && is_current).then_some("page");
|
||||
let aria_disabled = disabled.then_some("true");
|
||||
|
|
@ -155,18 +158,22 @@ impl Component for Item {
|
|||
impl Item {
|
||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||
pub fn label(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
///
|
||||
/// La ruta se obtiene invocando [`FnPathByContext`], que devuelve dinámicamente una
|
||||
/// [`RoutePath`] en función del [`Context`]. El enlace se marca como `active` si la ruta actual
|
||||
/// del *request* coincide con la ruta de destino (devuelta por `RoutePath::path`).
|
||||
pub fn link(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
|
|
@ -175,11 +182,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_disabled(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: false,
|
||||
disabled: true,
|
||||
},
|
||||
|
|
@ -188,11 +195,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
||||
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_blank(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: true,
|
||||
disabled: false,
|
||||
},
|
||||
|
|
@ -201,11 +208,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_blank_disabled(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: true,
|
||||
disabled: true,
|
||||
},
|
||||
|
|
@ -215,7 +222,7 @@ impl Item {
|
|||
|
||||
/// Crea un botón de acción local, sin navegación asociada.
|
||||
pub fn button(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Button {
|
||||
label,
|
||||
disabled: false,
|
||||
|
|
@ -226,7 +233,7 @@ impl Item {
|
|||
|
||||
/// Crea un botón deshabilitado.
|
||||
pub fn button_disabled(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Button {
|
||||
label,
|
||||
disabled: true,
|
||||
|
|
@ -237,7 +244,7 @@ impl Item {
|
|||
|
||||
/// Crea un encabezado para un grupo de elementos dentro del menú.
|
||||
pub fn header(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Header(label),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -245,7 +252,7 @@ impl Item {
|
|||
|
||||
/// Crea un separador visual entre bloques de elementos.
|
||||
pub fn divider() -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Divider,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -256,26 +263,14 @@ impl Item {
|
|||
/// Establece el identificador único (`id`) del elemento.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al elemento.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Item GETTERS >***************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al elemento.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el tipo de elemento representado.
|
||||
pub fn item_kind(&self) -> &ItemKind {
|
||||
&self.item_kind
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ pub enum AutoClose {
|
|||
}
|
||||
|
||||
impl AutoClose {
|
||||
// Devuelve el valor para `data-bs-auto-close`, o `None` si es el comportamiento por defecto.
|
||||
/// Devuelve el valor para `data-bs-auto-close`, o `None` si es el comportamiento por defecto.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) const fn as_str(self) -> Option<&'static str> {
|
||||
|
|
@ -59,7 +59,7 @@ pub enum Direction {
|
|||
}
|
||||
|
||||
impl Direction {
|
||||
// Mapea la dirección teniendo en cuenta si se agrupa con otros menús [`Dropdown`].
|
||||
/// Mapea la dirección teniendo en cuenta si se agrupa con otros menús [`Dropdown`].
|
||||
#[rustfmt::skip ]
|
||||
#[inline]
|
||||
const fn as_str(self, grouped: bool) -> &'static str {
|
||||
|
|
@ -74,8 +74,8 @@ impl Direction {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la dirección de despliegue a la cadena de clases teniendo en cuenta si se agrupa con
|
||||
// otros menús [`Dropdown`].
|
||||
/// Añade la dirección de despliegue a la cadena de clases teniendo en cuenta si se agrupa con
|
||||
/// otros menús [`Dropdown`].
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, grouped: bool) {
|
||||
if grouped {
|
||||
|
|
@ -93,10 +93,10 @@ impl Direction {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve la clase asociada a la dirección teniendo en cuenta si se agrupa con otros menús
|
||||
// [`Dropdown`], o `""` si no corresponde ninguna.
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, grouped: bool) -> String {
|
||||
/// Devuelve la clase asociada a la dirección teniendo en cuenta si se agrupa con otros menús
|
||||
/// [`Dropdown`], o `""` si no corresponde ninguna.
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, grouped: bool) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes, grouped);
|
||||
classes
|
||||
|
|
@ -138,7 +138,7 @@ impl MenuAlign {
|
|||
classes.push_str(class);
|
||||
}
|
||||
|
||||
// Añade las clases de alineación a `classes` (sin incluir la base `dropdown-menu`).
|
||||
/// Añade las clases de alineación a `classes` (sin incluir la base `dropdown-menu`).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
match self {
|
||||
|
|
@ -179,8 +179,7 @@ impl MenuAlign {
|
|||
}
|
||||
|
||||
/* Devuelve las clases de alineación sin incluir `dropdown-menu` (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
classes
|
||||
|
|
@ -206,7 +205,7 @@ pub enum MenuPosition {
|
|||
}
|
||||
|
||||
impl MenuPosition {
|
||||
// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
|
||||
/// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
|
||||
#[inline]
|
||||
pub(crate) fn data_offset(self) -> Option<String> {
|
||||
match self {
|
||||
|
|
@ -215,7 +214,7 @@ impl MenuPosition {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
|
||||
/// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
|
||||
#[inline]
|
||||
pub(crate) fn data_reference(self) -> Option<&'static str> {
|
||||
match self {
|
||||
|
|
|
|||
14
extensions/pagetop-bootsier/src/theme/form.rs
Normal file
14
extensions/pagetop-bootsier/src/theme/form.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//! Definiciones para crear formularios ([`Form`]).
|
||||
|
||||
mod props;
|
||||
pub use props::{Autocomplete, AutofillField};
|
||||
pub use props::{InputType, Method};
|
||||
|
||||
mod component;
|
||||
pub use component::Form;
|
||||
|
||||
mod fieldset;
|
||||
pub use fieldset::Fieldset;
|
||||
|
||||
mod input;
|
||||
pub use input::Input;
|
||||
130
extensions/pagetop-bootsier/src/theme/form/component.rs
Normal file
130
extensions/pagetop-bootsier/src/theme/form/component.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
|
||||
/// Componente para crear un **formulario**.
|
||||
///
|
||||
/// Este componente renderiza un `<form>` estándar con soporte para los atributos más habituales:
|
||||
///
|
||||
/// - `id`: identificador opcional del formulario.
|
||||
/// - `classes`: clases CSS adicionales (p. ej. utilidades CSS).
|
||||
/// - `action`: URL/ruta de destino para el envío.
|
||||
/// - `method`: método usado por el formulario para el envío de los datos (ver explicaciones en
|
||||
/// [`form::Method`](crate::theme::form::Method)).
|
||||
/// - `accept-charset`: juego de caracteres aceptado (por defecto es `"UTF-8"`).
|
||||
/// - `children`: contenido del formulario.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```ignore
|
||||
/// use pagetop::prelude::*;
|
||||
/// use crate::prelude::*;
|
||||
///
|
||||
/// let form = Form::new()
|
||||
/// .with_id("search")
|
||||
/// .with_action("/search")
|
||||
/// .with_method(form::Method::Get)
|
||||
/// .with_classes(ClassesOp::Add, "mb-3")
|
||||
/// .add_child(Input::new().with_name("q"));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Form {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
classes: Classes,
|
||||
action: AttrValue,
|
||||
method: form::Method,
|
||||
#[default(_code = "AttrValue::new(\"UTF-8\")")]
|
||||
charset: AttrValue,
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Form {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "form");
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let method = match self.method() {
|
||||
form::Method::Post => Some("post"),
|
||||
form::Method::Get => None,
|
||||
};
|
||||
PrepareMarkup::With(html! {
|
||||
form
|
||||
id=[self.id()]
|
||||
class=[self.classes().get()]
|
||||
action=[self.action().get()]
|
||||
method=[method]
|
||||
accept-charset=[self.charset().get()]
|
||||
{
|
||||
(self.children().render(cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Form {
|
||||
// **< Form BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la URL/ruta de destino del formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_action(mut self, action: impl AsRef<str>) -> Self {
|
||||
self.action.alter_str(action);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el método para enviar el formulario.
|
||||
///
|
||||
/// - `GET`: el atributo `method` se omite.
|
||||
/// - `POST`: se establece `method="post"`.
|
||||
#[builder_fn]
|
||||
pub fn with_method(mut self, method: form::Method) -> Self {
|
||||
self.method = method;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el juego de caracteres aceptado por el formulario.
|
||||
///
|
||||
/// Por defecto se usa `"UTF-8"`.
|
||||
#[builder_fn]
|
||||
pub fn with_charset(mut self, charset: impl AsRef<str>) -> Self {
|
||||
self.charset.alter_str(charset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente hijo al formulario.
|
||||
#[inline]
|
||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
||||
self.children.add(Child::with(component));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
||||
self.children.alter_child(op);
|
||||
self
|
||||
}
|
||||
}
|
||||
81
extensions/pagetop-bootsier/src/theme/form/fieldset.rs
Normal file
81
extensions/pagetop-bootsier/src/theme/form/fieldset.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Agrupa controles relacionados de un formulario (`<fieldset>`).
|
||||
///
|
||||
/// Se usa para mejorar la accesibilidad cuando se acompaña de una leyenda que encabeza el grupo.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Fieldset {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
classes: Classes,
|
||||
legend: Attr<L10n>,
|
||||
disabled: bool,
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Fieldset {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
PrepareMarkup::With(html! {
|
||||
fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] {
|
||||
@if let Some(legend) = self.legend().lookup(cx) {
|
||||
legend { (legend) }
|
||||
}
|
||||
(self.children().render(cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Fieldset {
|
||||
// **< Fieldset BUILDER >***********************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del `fieldset` (grupo de controles).
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al `fieldset`.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la leyenda del `fieldset`.
|
||||
#[builder_fn]
|
||||
pub fn with_legend(mut self, legend: L10n) -> Self {
|
||||
self.legend.alter_value(legend);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el `fieldset` está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente hijo al `fieldset`.
|
||||
#[inline]
|
||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
||||
self.children.add(Child::with(component));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
||||
self.children.alter_child(op);
|
||||
self
|
||||
}
|
||||
}
|
||||
205
extensions/pagetop-bootsier/src/theme/form/input.rs
Normal file
205
extensions/pagetop-bootsier/src/theme/form/input.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Input {
|
||||
classes: Classes,
|
||||
input_type: form::InputType,
|
||||
name: AttrName,
|
||||
value: AttrValue,
|
||||
label: Attr<L10n>,
|
||||
help_text: Attr<L10n>,
|
||||
#[default(_code = "Attr::<u16>::some(60)")]
|
||||
size: Attr<u16>,
|
||||
minlength: Attr<u16>,
|
||||
#[default(_code = "Attr::<u16>::some(128)")]
|
||||
maxlength: Attr<u16>,
|
||||
placeholder: AttrValue,
|
||||
autocomplete: Attr<form::Autocomplete>,
|
||||
autofocus: bool,
|
||||
readonly: bool,
|
||||
required: bool,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Component for Input {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(
|
||||
ClassesOp::Prepend,
|
||||
util::join!("form-item form-type-", self.input_type().to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let id = self.name().get().map(|name| util::join!("edit-", name));
|
||||
PrepareMarkup::With(html! {
|
||||
div class=[self.classes().get()] {
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label for=[&id] class="form-label" {
|
||||
(label)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
input
|
||||
type=(self.input_type())
|
||||
id=[id]
|
||||
class="form-control"
|
||||
name=[self.name().get()]
|
||||
value=[self.value().get()]
|
||||
size=[self.size().get()]
|
||||
minlength=[self.minlength().get()]
|
||||
maxlength=[self.maxlength().get()]
|
||||
placeholder=[self.placeholder().get()]
|
||||
autocomplete=[self.autocomplete().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
readonly[*self.readonly()]
|
||||
required[*self.required()]
|
||||
disabled[*self.disabled()] {}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn textfield() -> Self {
|
||||
Input::default()
|
||||
}
|
||||
|
||||
pub fn password() -> Self {
|
||||
Self {
|
||||
input_type: form::InputType::Password,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search() -> Self {
|
||||
Self {
|
||||
input_type: form::InputType::Search,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn email() -> Self {
|
||||
Self {
|
||||
input_type: form::InputType::Email,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn telephone() -> Self {
|
||||
Self {
|
||||
input_type: form::InputType::Telephone,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url() -> Self {
|
||||
Self {
|
||||
input_type: form::InputType::Url,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Input BUILDER >**************************************************************************
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al `input`.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
|
||||
self.value.alter_str(value);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: L10n) -> Self {
|
||||
self.label.alter_value(label);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: L10n) -> Self {
|
||||
self.help_text.alter_value(help_text);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_size(mut self, size: Option<u16>) -> Self {
|
||||
self.size.alter_opt(size);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_minlength(mut self, minlength: Option<u16>) -> Self {
|
||||
self.minlength.alter_opt(minlength);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_maxlength(mut self, maxlength: Option<u16>) -> Self {
|
||||
self.maxlength.alter_opt(maxlength);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_placeholder(mut self, placeholder: impl AsRef<str>) -> Self {
|
||||
self.placeholder.alter_str(placeholder);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self {
|
||||
self.autocomplete.alter_opt(autocomplete);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_readonly(mut self, readonly: bool) -> Self {
|
||||
self.readonly = readonly;
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
449
extensions/pagetop-bootsier/src/theme/form/props.rs
Normal file
449
extensions/pagetop-bootsier/src/theme/form/props.rs
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
// **< Autocomplete >*******************************************************************************
|
||||
|
||||
/// Valor del atributo HTML `autocomplete`.
|
||||
///
|
||||
/// Según la [especificación](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill)
|
||||
/// oficial este valor puede ser:
|
||||
///
|
||||
/// - `on` / `off`, o
|
||||
/// - una lista ordenada de tokens predefinidos separados por espacios.
|
||||
///
|
||||
/// Las variantes de `Autocomplete` permiten:
|
||||
///
|
||||
/// - Generar valores canónicos `on`/`off` ([`Autocomplete::On`], [`Autocomplete::Off`]).
|
||||
/// - Generar una lista de tokens en formato texto ([`Autocomplete::Custom`]). Los valores creados
|
||||
/// mediante [`Autocomplete::custom()`] se normalizan con [`util::normalize_ascii_or_empty()`].
|
||||
///
|
||||
/// Las entradas no válidas que lleguen a [`Autocomplete::custom()`] se degradan a
|
||||
/// [`Autocomplete::On`] (valor canónico y seguro).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Autocomplete {
|
||||
/// Genera `autocomplete="on"`.
|
||||
On,
|
||||
/// Genera `autocomplete="off"`.
|
||||
Off,
|
||||
/// Genera un valor personalizado (se espera en formato canónico).
|
||||
///
|
||||
/// Normalmente contiene una lista de tokens separados por espacios (p. ej. `"username"` o
|
||||
/// `"username webauthn"`).
|
||||
Custom(CowStr),
|
||||
}
|
||||
|
||||
impl Autocomplete {
|
||||
// --< Field token >----------------------------------------------------------------------------
|
||||
|
||||
/// Genera `autocomplete="<field>"` usando un campo predefinido.
|
||||
#[inline]
|
||||
pub fn field(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Borrowed(field.as_str()))
|
||||
}
|
||||
|
||||
// --< Sections >-------------------------------------------------------------------------------
|
||||
|
||||
/// Construye `autocomplete` usando un nombre de sección y un campo predefinido.
|
||||
///
|
||||
/// Genera `autocomplete="section-<name> <field>"`.
|
||||
///
|
||||
/// Si `name` contiene espacios tras normalizar con [`util::normalize_ascii()`] (o si no es
|
||||
/// ASCII / queda vacío), se ignora la sección y se genera solo el campo (`<field>`).
|
||||
pub fn section(name: impl AsRef<str>, field: AutofillField) -> Self {
|
||||
match util::normalize_ascii(name.as_ref()) {
|
||||
Ok(n) if !n.as_ref().contains(' ') => {
|
||||
Self::custom(util::join!("section-", n.as_ref(), " ", field.as_str()))
|
||||
}
|
||||
_ => Self::field(field),
|
||||
}
|
||||
}
|
||||
|
||||
// --< Common fields >--------------------------------------------------------------------------
|
||||
|
||||
/// Genera `autocomplete="username"`.
|
||||
pub fn username() -> Self {
|
||||
Self::field(AutofillField::Username)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="username webauthn"` (Passkeys / WebAuthn).
|
||||
pub fn username_webauthn() -> Self {
|
||||
Self::custom("username webauthn")
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="email"`.
|
||||
pub fn email() -> Self {
|
||||
Self::field(AutofillField::Email)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="current-password"`.
|
||||
pub fn current_password() -> Self {
|
||||
Self::field(AutofillField::CurrentPassword)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="current-password webauthn"` (Passkeys / WebAuthn).
|
||||
pub fn current_password_webauthn() -> Self {
|
||||
Self::custom("current-password webauthn")
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="new-password"`.
|
||||
pub fn new_password() -> Self {
|
||||
Self::field(AutofillField::NewPassword)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="one-time-code"`.
|
||||
pub fn otp() -> Self {
|
||||
Self::field(AutofillField::OneTimeCode)
|
||||
}
|
||||
|
||||
// --< Address contexts >-----------------------------------------------------------------------
|
||||
|
||||
/// Contexto de dirección de envío. Genera `autocomplete="shipping <field>"`.
|
||||
pub fn shipping(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("shipping ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Contexto de dirección de facturación. Genera `autocomplete="billing <field>"`.
|
||||
pub fn billing(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("billing ", field.as_str())))
|
||||
}
|
||||
|
||||
// --< Contact hints >--------------------------------------------------------------------------
|
||||
|
||||
/// Detalle de contacto: `autocomplete="home <field>"`.
|
||||
pub fn home(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("home ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="work <field>"`.
|
||||
pub fn work(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("work ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="mobile <field>"`.
|
||||
pub fn mobile(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("mobile ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="fax <field>"`.
|
||||
pub fn fax(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("fax ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="pager <field>"`.
|
||||
pub fn pager(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("pager ", field.as_str())))
|
||||
}
|
||||
|
||||
// --< Custom tokens >--------------------------------------------------------------------------
|
||||
|
||||
/// Crea un `autocomplete` con texto libre (se espera en formato canónico).
|
||||
///
|
||||
/// Esta función acepta una cadena con `on`/`off` o una lista de tokens separados por espacios:
|
||||
///
|
||||
/// - Rechaza entradas no ASCII.
|
||||
/// - Recorta separadores ASCII al inicio/fin.
|
||||
/// - Compacta secuencias de separadores ASCII en un único espacio.
|
||||
/// - Convierte a minúsculas.
|
||||
///
|
||||
/// - Si el valor normalizado es `"on"` o `"off"`, devuelve [`Autocomplete::On`] o
|
||||
/// [`Autocomplete::Off`].
|
||||
/// - Si el valor es inválido (vacío tras normalizar o contiene bytes no ASCII), devuelve
|
||||
/// [`Autocomplete::On`].
|
||||
pub fn custom(autocomplete: impl Into<CowStr>) -> Self {
|
||||
let value: CowStr = autocomplete.into();
|
||||
let raw = value.as_ref();
|
||||
|
||||
// Normaliza la entrada.
|
||||
let Some(normalized) = util::normalize_ascii_or_empty(raw, "Autocomplete::custom") else {
|
||||
return Self::On;
|
||||
};
|
||||
let autocomplete = normalized.as_ref();
|
||||
|
||||
// Identifica valores especiales.
|
||||
if autocomplete == "on" {
|
||||
return Self::On;
|
||||
}
|
||||
if autocomplete == "off" {
|
||||
return Self::Off;
|
||||
}
|
||||
|
||||
// Mantiene el `Cow` original si no cambia nada (no reserva espacio).
|
||||
if autocomplete == raw {
|
||||
return Self::Custom(value);
|
||||
}
|
||||
// En otro caso asigna espacio para la normalización.
|
||||
Self::Custom(Cow::Owned(normalized.into_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Autocomplete {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Autocomplete::On => f.write_str("on"),
|
||||
Autocomplete::Off => f.write_str("off"),
|
||||
Autocomplete::Custom(c) => f.write_str(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum AutofillField {
|
||||
// Identidad / cuenta
|
||||
/// Nombre completo.
|
||||
Name,
|
||||
/// Tratamiento o título (p. ej. "Sr.", "Sra.", "Dra.").
|
||||
HonorificPrefix,
|
||||
/// Nombre de pila.
|
||||
GivenName,
|
||||
/// Nombre adicional (p. ej. segundo nombre).
|
||||
AdditionalName,
|
||||
/// Apellidos.
|
||||
FamilyName,
|
||||
/// Sufijo honorífico (p. ej. "Jr.", "PhD").
|
||||
HonorificSuffix,
|
||||
/// Apodo.
|
||||
Nickname,
|
||||
/// Identificador de usuario (login).
|
||||
Username,
|
||||
|
||||
// Credenciales
|
||||
/// Contraseña actual.
|
||||
CurrentPassword,
|
||||
/// Nueva contraseña.
|
||||
NewPassword,
|
||||
/// Código de un solo uso (OTP).
|
||||
OneTimeCode,
|
||||
|
||||
// Organización
|
||||
/// Cargo o título dentro de una organización.
|
||||
OrganizationTitle,
|
||||
/// Nombre de la organización.
|
||||
Organization,
|
||||
|
||||
// Contacto
|
||||
/// Correo electrónico.
|
||||
Email,
|
||||
/// Teléfono.
|
||||
Tel,
|
||||
/// Prefijo/código de país del teléfono (incluye `+`).
|
||||
TelCountryCode,
|
||||
/// Teléfono sin el código de país.
|
||||
TelNational,
|
||||
/// Código de área (si aplica).
|
||||
TelAreaCode,
|
||||
/// Teléfono sin código de país ni de área.
|
||||
TelLocal,
|
||||
/// Prefijo local (primera parte tras el área).
|
||||
TelLocalPrefix,
|
||||
/// Sufijo local (segunda parte tras el área).
|
||||
TelLocalSuffix,
|
||||
/// Extensión interna.
|
||||
TelExtension,
|
||||
/// URL.
|
||||
Url,
|
||||
/// Referencia de mensajería instantánea (URL).
|
||||
Impp,
|
||||
|
||||
// Dirección (muy habitual en formularios)
|
||||
/// Dirección postal completa (una sola línea/textarea).
|
||||
StreetAddress,
|
||||
/// Línea 1 de dirección.
|
||||
AddressLine1,
|
||||
/// Línea 2 de dirección.
|
||||
AddressLine2,
|
||||
/// Línea 3 de dirección.
|
||||
AddressLine3,
|
||||
/// Nivel administrativo 4 (el más específico).
|
||||
AddressLevel4,
|
||||
/// Nivel administrativo 3.
|
||||
AddressLevel3,
|
||||
/// Nivel administrativo 2 (p. ej. ciudad/municipio).
|
||||
AddressLevel2,
|
||||
/// Nivel administrativo 1 (p. ej. provincia/estado).
|
||||
AddressLevel1,
|
||||
/// Código postal.
|
||||
PostalCode,
|
||||
/// País (código o token `country`).
|
||||
Country,
|
||||
/// Nombre del país.
|
||||
CountryName,
|
||||
|
||||
// Pago (si algún día lo necesitas)
|
||||
/// Nombre del titular de la tarjeta.
|
||||
CcName,
|
||||
/// Nombre de pila del titular de la tarjeta.
|
||||
CcGivenName,
|
||||
/// Nombre adicional del titular de la tarjeta.
|
||||
CcAdditionalName,
|
||||
/// Apellidos del titular de la tarjeta.
|
||||
CcFamilyName,
|
||||
/// Número de tarjeta.
|
||||
CcNumber,
|
||||
/// Fecha de caducidad (completa).
|
||||
CcExp,
|
||||
/// Mes de caducidad.
|
||||
CcExpMonth,
|
||||
/// Año de caducidad.
|
||||
CcExpYear,
|
||||
/// Código de seguridad (CVC/CVV).
|
||||
CcCsc,
|
||||
/// Tipo de tarjeta (p. ej. visa/mastercard).
|
||||
CcType,
|
||||
|
||||
// Transacción / preferencias
|
||||
/// Moneda preferida para la transacción (código ISO 4217).
|
||||
TransactionCurrency,
|
||||
/// Cantidad de la transacción (número).
|
||||
TransactionAmount,
|
||||
/// Idioma preferido (BCP 47).
|
||||
Language,
|
||||
|
||||
// Otros datos personales (según necesidad del producto)
|
||||
/// Fecha de nacimiento completa.
|
||||
Bday,
|
||||
/// Día de nacimiento.
|
||||
BdayDay,
|
||||
/// Mes de nacimiento.
|
||||
BdayMonth,
|
||||
/// Año de nacimiento.
|
||||
BdayYear,
|
||||
/// Sexo (según el valor que el UA tenga guardado).
|
||||
Sex,
|
||||
/// Foto (URL o referencia, según UA).
|
||||
Photo,
|
||||
}
|
||||
|
||||
impl AutofillField {
|
||||
/// Devuelve el token exacto definido por HTML para `autocomplete`.
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
AutofillField::Name => "name",
|
||||
AutofillField::HonorificPrefix => "honorific-prefix",
|
||||
AutofillField::GivenName => "given-name",
|
||||
AutofillField::AdditionalName => "additional-name",
|
||||
AutofillField::FamilyName => "family-name",
|
||||
AutofillField::HonorificSuffix => "honorific-suffix",
|
||||
AutofillField::Nickname => "nickname",
|
||||
AutofillField::Username => "username",
|
||||
|
||||
AutofillField::CurrentPassword => "current-password",
|
||||
AutofillField::NewPassword => "new-password",
|
||||
AutofillField::OneTimeCode => "one-time-code",
|
||||
|
||||
AutofillField::OrganizationTitle => "organization-title",
|
||||
AutofillField::Organization => "organization",
|
||||
|
||||
AutofillField::Email => "email",
|
||||
AutofillField::Tel => "tel",
|
||||
AutofillField::TelCountryCode => "tel-country-code",
|
||||
AutofillField::TelNational => "tel-national",
|
||||
AutofillField::TelAreaCode => "tel-area-code",
|
||||
AutofillField::TelLocal => "tel-local",
|
||||
AutofillField::TelLocalPrefix => "tel-local-prefix",
|
||||
AutofillField::TelLocalSuffix => "tel-local-suffix",
|
||||
AutofillField::TelExtension => "tel-extension",
|
||||
AutofillField::Url => "url",
|
||||
AutofillField::Impp => "impp",
|
||||
|
||||
AutofillField::StreetAddress => "street-address",
|
||||
AutofillField::AddressLine1 => "address-line1",
|
||||
AutofillField::AddressLine2 => "address-line2",
|
||||
AutofillField::AddressLine3 => "address-line3",
|
||||
AutofillField::AddressLevel4 => "address-level4",
|
||||
AutofillField::AddressLevel3 => "address-level3",
|
||||
AutofillField::AddressLevel2 => "address-level2",
|
||||
AutofillField::AddressLevel1 => "address-level1",
|
||||
AutofillField::PostalCode => "postal-code",
|
||||
AutofillField::Country => "country",
|
||||
AutofillField::CountryName => "country-name",
|
||||
|
||||
AutofillField::CcName => "cc-name",
|
||||
AutofillField::CcGivenName => "cc-given-name",
|
||||
AutofillField::CcAdditionalName => "cc-additional-name",
|
||||
AutofillField::CcFamilyName => "cc-family-name",
|
||||
AutofillField::CcNumber => "cc-number",
|
||||
AutofillField::CcExp => "cc-exp",
|
||||
AutofillField::CcExpMonth => "cc-exp-month",
|
||||
AutofillField::CcExpYear => "cc-exp-year",
|
||||
AutofillField::CcCsc => "cc-csc",
|
||||
AutofillField::CcType => "cc-type",
|
||||
|
||||
AutofillField::TransactionCurrency => "transaction-currency",
|
||||
AutofillField::TransactionAmount => "transaction-amount",
|
||||
AutofillField::Language => "language",
|
||||
|
||||
AutofillField::Bday => "bday",
|
||||
AutofillField::BdayDay => "bday-day",
|
||||
AutofillField::BdayMonth => "bday-month",
|
||||
AutofillField::BdayYear => "bday-year",
|
||||
AutofillField::Sex => "sex",
|
||||
AutofillField::Photo => "photo",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< InputType >**********************************************************************************
|
||||
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum InputType {
|
||||
#[default]
|
||||
Textfield,
|
||||
Password,
|
||||
Search,
|
||||
Email,
|
||||
Telephone,
|
||||
Url,
|
||||
}
|
||||
|
||||
impl fmt::Display for InputType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
InputType::Textfield => "text",
|
||||
InputType::Password => "password",
|
||||
InputType::Search => "search",
|
||||
InputType::Email => "email",
|
||||
InputType::Telephone => "tel",
|
||||
InputType::Url => "url",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// **< Method >*************************************************************************************
|
||||
|
||||
/// Método HTTP usado por un formulario ([`Form`](crate::theme::Form)) para el envío de los datos.
|
||||
///
|
||||
/// En HTML, el atributo `method` del formulario indica **cómo** se envían los datos:
|
||||
///
|
||||
/// - **GET**: los pares `name=value` se codifican en la **URL** añadiendo una cadena de consulta
|
||||
/// como `?a=1&b=2`. Es el método por defecto en HTML cuando no se especifica. Suele ser apropiado
|
||||
/// para **búsquedas** o formularios que no modifican datos ni el estado del sistema.
|
||||
///
|
||||
/// - **POST**: los datos se envían en el **cuerpo** de la petición (*request body*). Es apropiado
|
||||
/// para acciones que **modifican el estado** o cuando hay formularios grandes. Es el **método por
|
||||
/// defecto** en PageTop.
|
||||
///
|
||||
/// # Consideraciones prácticas
|
||||
///
|
||||
/// - **Visibilidad y privacidad**: con GET los datos quedan visibles en la URL (historial, *logs*,
|
||||
/// marcadores). No se recomienda para datos sensibles. Con POST no van en la URL, pero **no se
|
||||
/// cifran** por sí mismos; por eso es esencial el uso de HTTPS.
|
||||
/// - **Tamaño**: GET está limitado por la longitud máxima de URL que acepten el navegador y el
|
||||
/// servidor. POST es más flexible para cargas grandes.
|
||||
/// - **Ficheros**: la subida de ficheros requiere `method="post"` y un `enctype` adecuado
|
||||
/// (habitualmente `multipart/form-data`).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Method {
|
||||
/// Envía los datos en el cuerpo de la petición.
|
||||
///
|
||||
/// Es el **método por defecto** en PageTop. Recomendado para operaciones que modifican el
|
||||
/// estado o para envíos grandes.
|
||||
#[default]
|
||||
Post,
|
||||
|
||||
/// Envía los datos en la URL como una cadena *query*.
|
||||
///
|
||||
/// Recomendado para búsquedas y operaciones que no modifican datos ni el estado del sistema.
|
||||
Get,
|
||||
}
|
||||
|
|
@ -13,17 +13,17 @@ pub enum IconKind {
|
|||
},
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Icon {
|
||||
classes : AttrClasses,
|
||||
icon_kind : IconKind,
|
||||
/// Devuelve las clases CSS asociadas al icono.
|
||||
classes: Classes,
|
||||
icon_kind: IconKind,
|
||||
aria_label: AttrL10n,
|
||||
}
|
||||
|
||||
impl Component for Icon {
|
||||
fn new() -> Self {
|
||||
Icon::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
|
|
@ -75,22 +75,22 @@ impl Component for Icon {
|
|||
|
||||
impl Icon {
|
||||
pub fn font() -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Font(FontSize::default()))
|
||||
Self::default().with_icon_kind(IconKind::Font(FontSize::default()))
|
||||
}
|
||||
|
||||
pub fn font_sized(font_size: FontSize) -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Font(font_size))
|
||||
Self::default().with_icon_kind(IconKind::Font(font_size))
|
||||
}
|
||||
|
||||
pub fn svg(shapes: Markup) -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Svg {
|
||||
Self::default().with_icon_kind(IconKind::Svg {
|
||||
shapes,
|
||||
viewbox: AttrValue::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn svg_with_viewbox(shapes: Markup, viewbox: impl AsRef<str>) -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Svg {
|
||||
Self::default().with_icon_kind(IconKind::Svg {
|
||||
shapes,
|
||||
viewbox: AttrValue::new(viewbox),
|
||||
})
|
||||
|
|
@ -116,19 +116,4 @@ impl Icon {
|
|||
self.aria_label.alter_value(label);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Icon GETTERS >***************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al icono.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
pub fn icon_kind(&self) -> &IconKind {
|
||||
&self.icon_kind
|
||||
}
|
||||
|
||||
pub fn aria_label(&self) -> &AttrL10n {
|
||||
&self.aria_label
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,19 +9,23 @@ use crate::prelude::*;
|
|||
/// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas**
|
||||
/// ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
||||
/// - Resuelve el texto alternativo `alt` con **localización** mediante [`L10n`].
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Image {
|
||||
id : AttrId,
|
||||
classes: AttrClasses,
|
||||
size : image::Size,
|
||||
source : image::Source,
|
||||
alt : AttrL10n,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas a la imagen.
|
||||
classes: Classes,
|
||||
/// Devuelve las dimensiones de la imagen.
|
||||
size: image::Size,
|
||||
/// Devuelve el origen de la imagen.
|
||||
source: image::Source,
|
||||
/// Devuelve el texto alternativo localizado.
|
||||
alternative: Attr<L10n>,
|
||||
}
|
||||
|
||||
impl Component for Image {
|
||||
fn new() -> Self {
|
||||
Image::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -69,7 +73,7 @@ impl Component for Image {
|
|||
impl Image {
|
||||
/// Crea rápidamente una imagen especificando su origen.
|
||||
pub fn with(source: image::Source) -> Self {
|
||||
Image::default().with_source(source)
|
||||
Self::default().with_source(source)
|
||||
}
|
||||
|
||||
// **< Image BUILDER >**************************************************************************
|
||||
|
|
@ -77,7 +81,7 @@ impl Image {
|
|||
/// Establece el identificador único (`id`) de la imagen.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +93,7 @@ impl Image {
|
|||
/// - Redondear las esquinas ([`classes::Rounded`]).
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -107,35 +111,13 @@ impl Image {
|
|||
self
|
||||
}
|
||||
|
||||
/// Define el texto alternativo localizado ([`L10n`]) para la imagen.
|
||||
/// Define un *texto localizado* ([`L10n`]) alternativo para la imagen.
|
||||
///
|
||||
/// Se recomienda siempre aportar un texto alternativo salvo que la imagen sea puramente
|
||||
/// decorativa.
|
||||
#[builder_fn]
|
||||
pub fn with_alternative(mut self, alt: L10n) -> Self {
|
||||
self.alt.alter_value(alt);
|
||||
self.alternative.alter_value(alt);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Image GETTERS >**************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas a la imagen.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve las dimensiones de la imagen.
|
||||
pub fn size(&self) -> &image::Size {
|
||||
&self.size
|
||||
}
|
||||
|
||||
/// Devuelve el origen de la imagen.
|
||||
pub fn source(&self) -> &image::Source {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// Devuelve el texto alternativo localizado.
|
||||
pub fn alternative(&self) -> &AttrL10n {
|
||||
&self.alt
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ pub enum Size {
|
|||
}
|
||||
|
||||
impl Size {
|
||||
// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
|
||||
/// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
|
||||
#[inline]
|
||||
pub(crate) fn to_style(self) -> Option<String> {
|
||||
match self {
|
||||
|
|
@ -54,23 +54,47 @@ pub enum Source {
|
|||
Logo(PageTopSvg),
|
||||
/// Imagen que se adapta automáticamente a su contenedor.
|
||||
///
|
||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
||||
Responsive(String),
|
||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
||||
Responsive(CowStr),
|
||||
/// Imagen que aplica el estilo **miniatura** de Bootstrap.
|
||||
///
|
||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
||||
Thumbnail(String),
|
||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
||||
Thumbnail(CowStr),
|
||||
/// Imagen sin clases específicas de Bootstrap, útil para controlar con CSS propio.
|
||||
///
|
||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
||||
Plain(String),
|
||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
||||
Plain(CowStr),
|
||||
}
|
||||
|
||||
impl Source {
|
||||
const IMG_FLUID: &str = "img-fluid";
|
||||
const IMG_THUMBNAIL: &str = "img-thumbnail";
|
||||
|
||||
// Devuelve la clase base asociada a la imagen según la fuente.
|
||||
/// Imagen con el logotipo de PageTop.
|
||||
#[inline]
|
||||
pub fn logo(svg: PageTopSvg) -> Self {
|
||||
Self::Logo(svg)
|
||||
}
|
||||
|
||||
/// Imagen responsive (`img-fluid`).
|
||||
#[inline]
|
||||
pub fn responsive(url: impl Into<CowStr>) -> Self {
|
||||
Self::Responsive(url.into())
|
||||
}
|
||||
|
||||
/// Imagen miniatura (`img-thumbnail`).
|
||||
#[inline]
|
||||
pub fn thumbnail(url: impl Into<CowStr>) -> Self {
|
||||
Self::Thumbnail(url.into())
|
||||
}
|
||||
|
||||
/// Imagen sin clases adicionales.
|
||||
#[inline]
|
||||
pub fn plain(url: impl Into<CowStr>) -> Self {
|
||||
Self::Plain(url.into())
|
||||
}
|
||||
|
||||
/// Devuelve la clase base asociada a la imagen según la fuente.
|
||||
#[inline]
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
|
|
@ -93,9 +117,8 @@ impl Source {
|
|||
classes.push_str(s);
|
||||
} */
|
||||
|
||||
// Devuelve la clase asociada a la imagen según la fuente.
|
||||
#[inline]
|
||||
pub(crate) fn to_class(&self) -> String {
|
||||
/// Devuelve la clase asociada a la imagen según la fuente.
|
||||
pub fn to_class(&self) -> String {
|
||||
let s = self.as_str();
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
|
|
|
|||
|
|
@ -14,17 +14,17 @@
|
|||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let nav = Nav::tabs()
|
||||
//! .with_layout(nav::Layout::End)
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(nav::Item::link_blank(L10n::n("External"), |_| "https://www.google.es"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .add_item(nav::Item::link_blank(L10n::n("External"), |_| "https://google.es".into()))
|
||||
//! .add_item(nav::Item::dropdown(
|
||||
//! Dropdown::new()
|
||||
//! .with_title(L10n::n("Options"))
|
||||
//! .with_items(TypedOp::AddMany(vec![
|
||||
//! Typed::with(dropdown::Item::link(L10n::n("Action"), |_| "/action")),
|
||||
//! Typed::with(dropdown::Item::link(L10n::n("Another action"), |_| "/another")),
|
||||
//! Typed::with(dropdown::Item::link(L10n::n("Action"), |_| "/action".into())),
|
||||
//! Typed::with(dropdown::Item::link(L10n::n("Another"), |_| "/another".into())),
|
||||
//! ])),
|
||||
//! ))
|
||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"));
|
||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
|
|
|
|||
|
|
@ -10,19 +10,23 @@ use crate::prelude::*;
|
|||
///
|
||||
/// Ver ejemplo en el módulo [`nav`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Nav {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
items : Children,
|
||||
nav_kind : nav::Kind,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al menú.
|
||||
classes: Classes,
|
||||
/// Devuelve el estilo visual seleccionado.
|
||||
nav_kind: nav::Kind,
|
||||
/// Devuelve la distribución y orientación seleccionada.
|
||||
nav_layout: nav::Layout,
|
||||
/// Devuelve la lista de elementos del menú.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
impl Component for Nav {
|
||||
fn new() -> Self {
|
||||
Nav::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -55,17 +59,17 @@ impl Component for Nav {
|
|||
impl Nav {
|
||||
/// Crea un `Nav` usando pestañas para los elementos (*Tabs*).
|
||||
pub fn tabs() -> Self {
|
||||
Nav::default().with_kind(nav::Kind::Tabs)
|
||||
Self::default().with_kind(nav::Kind::Tabs)
|
||||
}
|
||||
|
||||
/// Crea un `Nav` usando botones para los elementos (*Pills*).
|
||||
pub fn pills() -> Self {
|
||||
Nav::default().with_kind(nav::Kind::Pills)
|
||||
Self::default().with_kind(nav::Kind::Pills)
|
||||
}
|
||||
|
||||
/// Crea un `Nav` usando elementos subrayados (*Underline*).
|
||||
pub fn underline() -> Self {
|
||||
Nav::default().with_kind(nav::Kind::Underline)
|
||||
Self::default().with_kind(nav::Kind::Underline)
|
||||
}
|
||||
|
||||
// **< Nav BUILDER >****************************************************************************
|
||||
|
|
@ -73,14 +77,14 @@ impl Nav {
|
|||
/// Establece el identificador único (`id`) del menú.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al menú.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -110,26 +114,4 @@ impl Nav {
|
|||
self.items.alter_typed(op);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Nav GETTERS >****************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al menú.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el estilo visual seleccionado.
|
||||
pub fn nav_kind(&self) -> &nav::Kind {
|
||||
&self.nav_kind
|
||||
}
|
||||
|
||||
/// Devuelve la distribución y orientación seleccionada.
|
||||
pub fn nav_layout(&self) -> &nav::Layout {
|
||||
&self.nav_layout
|
||||
}
|
||||
|
||||
/// Devuelve la lista de elementos (`children`) del menú.
|
||||
pub fn items(&self) -> &Children {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,18 @@ pub enum ItemKind {
|
|||
Void,
|
||||
/// Etiqueta sin comportamiento interactivo.
|
||||
Label(L10n),
|
||||
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
|
||||
/// inicialmente deshabilitado.
|
||||
/// Elemento de navegación basado en una [`RoutePath`] dinámica devuelta por
|
||||
/// [`FnPathByContext`]. Opcionalmente, puede abrirse en una nueva ventana y estar inicialmente
|
||||
/// deshabilitado.
|
||||
Link {
|
||||
label: L10n,
|
||||
path: FnPathByContext,
|
||||
route: FnPathByContext,
|
||||
blank: bool,
|
||||
disabled: bool,
|
||||
},
|
||||
/// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del
|
||||
/// menú, sin añadir ningún comportamiento de navegación adicional.
|
||||
Html(Typed<Html>),
|
||||
/// Elemento que despliega un menú [`Dropdown`].
|
||||
Dropdown(Typed<Dropdown>),
|
||||
}
|
||||
|
|
@ -56,7 +60,7 @@ impl ItemKind {
|
|||
classes.push_str(class);
|
||||
} */
|
||||
|
||||
// Devuelve las clases asociadas al tipo de elemento.
|
||||
/// Devuelve las clases asociadas al tipo de elemento.
|
||||
#[inline]
|
||||
pub(crate) fn to_class(&self) -> String {
|
||||
self.as_str().to_owned()
|
||||
|
|
@ -68,21 +72,23 @@ impl ItemKind {
|
|||
/// Representa un **elemento individual** de un menú [`Nav`](crate::theme::Nav).
|
||||
///
|
||||
/// Cada instancia de [`nav::Item`](crate::theme::nav::Item) se traduce en un componente visible que
|
||||
/// puede comportarse como texto, enlace, botón o menú desplegable según su [`ItemKind`].
|
||||
/// puede comportarse como texto, enlace, contenido HTML o menú desplegable, según su [`ItemKind`].
|
||||
///
|
||||
/// Permite definir identificador, clases de estilo adicionales o tipo de interacción asociada,
|
||||
/// manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Item {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al elemento.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo de elemento representado.
|
||||
item_kind: ItemKind,
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Item::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -107,13 +113,13 @@ impl Component for Item {
|
|||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let path = path(cx);
|
||||
let route_link = route(cx);
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = !*disabled && (current_path == Some(path));
|
||||
let is_current = !*disabled && (current_path == Some(route_link.path()));
|
||||
|
||||
let mut classes = "nav-link".to_string();
|
||||
if is_current {
|
||||
|
|
@ -123,7 +129,7 @@ impl Component for Item {
|
|||
classes.push_str(" disabled");
|
||||
}
|
||||
|
||||
let href = (!*disabled).then_some(path);
|
||||
let href = (!*disabled).then_some(route_link);
|
||||
let target = (!*disabled && *blank).then_some("_blank");
|
||||
let rel = (!*disabled && *blank).then_some("noopener noreferrer");
|
||||
|
||||
|
|
@ -146,6 +152,12 @@ impl Component for Item {
|
|||
})
|
||||
}
|
||||
|
||||
ItemKind::Html(html) => PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
(html.render(cx))
|
||||
}
|
||||
}),
|
||||
|
||||
ItemKind::Dropdown(menu) => {
|
||||
if let Some(dd) = menu.borrow() {
|
||||
let items = dd.items().render(cx);
|
||||
|
|
@ -184,18 +196,22 @@ impl Component for Item {
|
|||
impl Item {
|
||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||
pub fn label(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
///
|
||||
/// La ruta se obtiene invocando [`FnPathByContext`], que devuelve dinámicamente una
|
||||
/// [`RoutePath`] en función del [`Context`]. El enlace se marca como `active` si la ruta actual
|
||||
/// del *request* coincide con la ruta de destino (devuelta por `RoutePath::path`).
|
||||
pub fn link(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
|
|
@ -204,11 +220,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_disabled(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: false,
|
||||
disabled: true,
|
||||
},
|
||||
|
|
@ -217,11 +233,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
||||
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_blank(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: true,
|
||||
disabled: false,
|
||||
},
|
||||
|
|
@ -230,11 +246,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_blank_disabled(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: true,
|
||||
disabled: true,
|
||||
},
|
||||
|
|
@ -242,13 +258,24 @@ impl Item {
|
|||
}
|
||||
}
|
||||
|
||||
/// Crea un elemento con contenido HTML arbitrario.
|
||||
///
|
||||
/// El contenido se renderiza tal cual lo devuelve el componente [`Html`], dentro de un `<li>`
|
||||
/// con las clases de navegación asociadas a [`Item`].
|
||||
pub fn html(html: Html) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Html(Typed::with(html)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un elemento de navegación que contiene un menú desplegable [`Dropdown`].
|
||||
///
|
||||
/// Sólo se tienen en cuenta **el título** (si no existe le asigna uno por defecto) y **la lista
|
||||
/// de elementos** del [`Dropdown`]; el resto de propiedades del componente no afectarán a su
|
||||
/// representación en [`Nav`].
|
||||
/// Sólo se tienen en cuenta **el título** (si no existe, se asigna uno por defecto) y **la
|
||||
/// lista de elementos** del [`Dropdown`]; el resto de propiedades del componente no afectarán
|
||||
/// a su representación en [`Nav`].
|
||||
pub fn dropdown(menu: Dropdown) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Dropdown(Typed::with(menu)),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -259,26 +286,14 @@ impl Item {
|
|||
/// Establece el identificador único (`id`) del elemento.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al elemento.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Item GETTERS >***************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al elemento.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el tipo de elemento representado.
|
||||
pub fn item_kind(&self) -> &ItemKind {
|
||||
&self.item_kind
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ impl Kind {
|
|||
const PILLS: &str = "nav-pills";
|
||||
const UNDERLINE: &str = "nav-underline";
|
||||
|
||||
// Devuelve la clase base asociada al tipo de menú, o una cadena vacía si no aplica.
|
||||
/// Devuelve la clase base asociada al tipo de menú, o una cadena vacía si no aplica.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
|
|
@ -33,7 +33,7 @@ impl Kind {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada al tipo de menú a la cadena de clases.
|
||||
/// Añade la clase asociada al tipo de menú a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
|
|
@ -83,7 +83,7 @@ impl Layout {
|
|||
const FILL: &str = "nav-fill";
|
||||
const JUSTIFIED: &str = "nav-justified";
|
||||
|
||||
// Devuelve la clase base asociada a la distribución y orientación del menú.
|
||||
/// Devuelve la clase base asociada a la distribución y orientación del menú.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
|
|
@ -98,7 +98,7 @@ impl Layout {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada a la distribución y orientación del menú a la cadena de clases.
|
||||
/// Añade la clase asociada a la distribución y orientación del menú a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
|
|
@ -112,7 +112,7 @@ impl Layout {
|
|||
}
|
||||
|
||||
/* Devuelve la clase asociada a la distribución y orientación del menú, o una cadena vacía si no
|
||||
// aplica (reservado).
|
||||
/// aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_owned()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra
|
||||
//! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús
|
||||
//! [`Nav`](crate::theme::Nav) o textos localizados usando [`L10n`](pagetop::locale::L10n).
|
||||
//! [`Nav`](crate::theme::Nav) o *textos localizados* usando [`L10n`](pagetop::locale::L10n).
|
||||
//!
|
||||
//! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand))
|
||||
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
|
||||
|
|
@ -17,9 +17,9 @@
|
|||
//! let navbar = Navbar::simple()
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(nav::Item::link(L10n::n("About"), |_| "/about"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Contact"), |_| "/contact"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .add_item(nav::Item::link(L10n::n("About"), |_| "/about".into()))
|
||||
//! .add_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -32,9 +32,9 @@
|
|||
//! .with_expand(BreakPoint::MD)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.example.com"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Support"), |_| "/support"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .add_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://sample.com".into()))
|
||||
//! .add_item(nav::Item::link(L10n::n("Support"), |_| "/support".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -45,19 +45,23 @@
|
|||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("PageTop"))
|
||||
//! .with_path(Some(|_| "/"));
|
||||
//! .with_route(Some(|cx| cx.route("/")));
|
||||
//!
|
||||
//! let navbar = Navbar::brand_left(brand)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .add_item(nav::Item::dropdown(
|
||||
//! Dropdown::new()
|
||||
//! .with_title(L10n::n("Tools"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Generator"), |_| "/tools/gen"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Reports"), |_| "/tools/reports"))
|
||||
//! .add_item(dropdown::Item::link(
|
||||
//! L10n::n("Generator"), |_| "/tools/gen".into())
|
||||
//! )
|
||||
//! .add_item(dropdown::Item::link(
|
||||
//! L10n::n("Reports"), |_| "/tools/reports".into())
|
||||
//! )
|
||||
//! ))
|
||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"))
|
||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -68,14 +72,14 @@
|
|||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("Intranet"))
|
||||
//! .with_path(Some(|_| "/"));
|
||||
//! .with_route(Some(|cx| cx.route("/")));
|
||||
//!
|
||||
//! let navbar = Navbar::brand_right(brand)
|
||||
//! .with_expand(BreakPoint::LG)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! Nav::pills()
|
||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Users"), |_| "/users"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into()))
|
||||
//! .add_item(nav::Item::link(L10n::n("Users"), |_| "/users".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -93,13 +97,13 @@
|
|||
//! let navbar = Navbar::offcanvas(oc)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Profile"), |_| "/profile"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .add_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into()))
|
||||
//! .add_item(nav::Item::dropdown(
|
||||
//! Dropdown::new()
|
||||
//! .with_title(L10n::n("More"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Help"), |_| "/help"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into()))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into()))
|
||||
//! ))
|
||||
//! ));
|
||||
//! ```
|
||||
|
|
@ -111,15 +115,15 @@
|
|||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("Main App"))
|
||||
//! .with_path(Some(|_| "/"));
|
||||
//! .with_route(Some(|cx| cx.route("/")));
|
||||
//!
|
||||
//! let navbar = Navbar::brand_left(brand)
|
||||
//! .with_position(navbar::Position::FixedTop)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Donors"), |_| "/donors"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Stock"), |_| "/stock"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into()))
|
||||
//! .add_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into()))
|
||||
//! .add_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
|
||||
|
|
|
|||
|
|
@ -6,26 +6,30 @@ use crate::prelude::*;
|
|||
///
|
||||
/// Representa la identidad del sitio con una imagen, título y eslogan:
|
||||
///
|
||||
/// - Si hay URL ([`with_path()`](Self::with_path)), el bloque completo actúa como enlace. Por
|
||||
/// - Si hay URL ([`with_route()`](Self::with_route)), el bloque completo actúa como enlace. Por
|
||||
/// defecto enlaza a la raíz del sitio (`/`).
|
||||
/// - Si no hay imagen ([`with_image()`](Self::with_image)) ni título
|
||||
/// ([`with_title()`](Self::with_title)), la marca de identidad no se renderiza.
|
||||
/// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Brand {
|
||||
id : AttrId,
|
||||
image : Typed<Image>,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve la imagen de marca (si la hay).
|
||||
image: Typed<Image>,
|
||||
/// Devuelve el título de la identidad de marca.
|
||||
#[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
|
||||
title : L10n,
|
||||
title: L10n,
|
||||
/// Devuelve el eslogan de la marca.
|
||||
slogan: L10n,
|
||||
#[default(_code = "Some(|_| \"/\")")]
|
||||
path : Option<FnPathByContext>,
|
||||
/// Devuelve la función que resuelve la URL asociada a la marca (si existe).
|
||||
#[default(_code = "Some(|cx| cx.route(\"/\"))")]
|
||||
route: Option<FnPathByContext>,
|
||||
}
|
||||
|
||||
impl Component for Brand {
|
||||
fn new() -> Self {
|
||||
Brand::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -40,8 +44,8 @@ impl Component for Brand {
|
|||
}
|
||||
let slogan = self.slogan().using(cx);
|
||||
PrepareMarkup::With(html! {
|
||||
@if let Some(path) = self.path() {
|
||||
a class="navbar-brand" href=(path(cx)) { (image) (title) (slogan) }
|
||||
@if let Some(route) = self.route() {
|
||||
a class="navbar-brand" href=(route(cx)) { (image) (title) (slogan) }
|
||||
} @else {
|
||||
span class="navbar-brand" { (image) (title) (slogan) }
|
||||
}
|
||||
|
|
@ -55,7 +59,7 @@ impl Brand {
|
|||
/// Establece el identificador único (`id`) de la marca.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -82,30 +86,8 @@ impl Brand {
|
|||
|
||||
/// Define la URL de destino. Si es `None`, la marca no será un enlace.
|
||||
#[builder_fn]
|
||||
pub fn with_path(mut self, path: Option<FnPathByContext>) -> Self {
|
||||
self.path = path;
|
||||
pub fn with_route(mut self, route: Option<FnPathByContext>) -> Self {
|
||||
self.route = route;
|
||||
self
|
||||
}
|
||||
|
||||
// **< Brand GETTERS >**************************************************************************
|
||||
|
||||
/// Devuelve la imagen de marca (si la hay).
|
||||
pub fn image(&self) -> &Typed<Image> {
|
||||
&self.image
|
||||
}
|
||||
|
||||
/// Devuelve el título de la identidad de marca.
|
||||
pub fn title(&self) -> &L10n {
|
||||
&self.title
|
||||
}
|
||||
|
||||
/// Devuelve el eslogan de la marca.
|
||||
pub fn slogan(&self) -> &L10n {
|
||||
&self.slogan
|
||||
}
|
||||
|
||||
/// Devuelve la función que resuelve la URL asociada a la marca (si existe).
|
||||
pub fn path(&self) -> &Option<FnPathByContext> {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,20 +14,25 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
|||
///
|
||||
/// Ver ejemplos en el módulo [`navbar`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Navbar {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
expand : BreakPoint,
|
||||
layout : navbar::Layout,
|
||||
position : navbar::Position,
|
||||
items : Children,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas a la barra de navegación.
|
||||
classes: Classes,
|
||||
/// Devuelve el punto de ruptura configurado.
|
||||
expand: BreakPoint,
|
||||
/// Devuelve la disposición configurada para la barra de navegación.
|
||||
layout: navbar::Layout,
|
||||
/// Devuelve la posición configurada para la barra de navegación.
|
||||
position: navbar::Position,
|
||||
/// Devuelve la lista de contenidos.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
impl Component for Navbar {
|
||||
fn new() -> Self {
|
||||
Navbar::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -46,7 +51,7 @@ impl Component for Navbar {
|
|||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
// Botón de despliegue (colapso u offcanvas) para la barra.
|
||||
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
|
||||
let id_content_target = join!("#", id_content);
|
||||
let id_content_target = util::join!("#", id_content);
|
||||
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
|
||||
Some("false")
|
||||
} else {
|
||||
|
|
@ -73,7 +78,7 @@ impl Component for Navbar {
|
|||
return PrepareMarkup::None;
|
||||
}
|
||||
|
||||
// Asegura que la barra tiene un id estable para poder asociarlo al colapso/offcanvas.
|
||||
// Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas.
|
||||
let id = cx.required_id::<Self>(self.id());
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
|
|
@ -87,7 +92,7 @@ impl Component for Navbar {
|
|||
|
||||
// Barra sencilla que se puede contraer/expandir.
|
||||
navbar::Layout::SimpleToggle => {
|
||||
@let id_content = join!(id, "-content");
|
||||
@let id_content = util::join!(id, "-content");
|
||||
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
div id=(id_content) class="collapse navbar-collapse" {
|
||||
|
|
@ -103,7 +108,7 @@ impl Component for Navbar {
|
|||
|
||||
// Barra con marca a la izquierda y botón a la derecha.
|
||||
navbar::Layout::BrandLeft(brand) => {
|
||||
@let id_content = join!(id, "-content");
|
||||
@let id_content = util::join!(id, "-content");
|
||||
|
||||
(brand.render(cx))
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
|
|
@ -114,7 +119,7 @@ impl Component for Navbar {
|
|||
|
||||
// Barra con botón a la izquierda y marca a la derecha.
|
||||
navbar::Layout::BrandRight(brand) => {
|
||||
@let id_content = join!(id, "-content");
|
||||
@let id_content = util::join!(id, "-content");
|
||||
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
(brand.render(cx))
|
||||
|
|
@ -164,37 +169,37 @@ impl Component for Navbar {
|
|||
impl Navbar {
|
||||
/// Crea una barra de navegación **simple**, sin marca y sin botón.
|
||||
pub fn simple() -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::Simple)
|
||||
Self::default().with_layout(navbar::Layout::Simple)
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación **simple pero colapsable**, con botón a la izquierda.
|
||||
pub fn simple_toggle() -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::SimpleToggle)
|
||||
Self::default().with_layout(navbar::Layout::SimpleToggle)
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación **con marca a la izquierda**, siempre visible.
|
||||
pub fn simple_brand_left(brand: navbar::Brand) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::SimpleBrandLeft(Typed::with(brand)))
|
||||
Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Typed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**.
|
||||
pub fn brand_left(brand: navbar::Brand) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::BrandLeft(Typed::with(brand)))
|
||||
Self::default().with_layout(navbar::Layout::BrandLeft(Typed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**.
|
||||
pub fn brand_right(brand: navbar::Brand) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::BrandRight(Typed::with(brand)))
|
||||
Self::default().with_layout(navbar::Layout::BrandRight(Typed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
|
||||
pub fn offcanvas(oc: Offcanvas) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::Offcanvas(Typed::with(oc)))
|
||||
Self::default().with_layout(navbar::Layout::Offcanvas(Typed::with(oc)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**.
|
||||
pub fn offcanvas_brand_left(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
|
||||
Self::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
|
||||
Typed::with(brand),
|
||||
Typed::with(oc),
|
||||
))
|
||||
|
|
@ -202,7 +207,7 @@ impl Navbar {
|
|||
|
||||
/// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**.
|
||||
pub fn offcanvas_brand_right(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::OffcanvasBrandRight(
|
||||
Self::default().with_layout(navbar::Layout::OffcanvasBrandRight(
|
||||
Typed::with(brand),
|
||||
Typed::with(oc),
|
||||
))
|
||||
|
|
@ -213,7 +218,7 @@ impl Navbar {
|
|||
/// Establece el identificador único (`id`) de la barra de navegación.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +230,7 @@ impl Navbar {
|
|||
/// - Definir la apariencia del texto ([`classes::Text`]).
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -263,31 +268,4 @@ impl Navbar {
|
|||
self.items.alter_typed(op);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Navbar GETTERS >*************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas a la barra de navegación.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el punto de ruptura configurado.
|
||||
pub fn expand(&self) -> &BreakPoint {
|
||||
&self.expand
|
||||
}
|
||||
|
||||
/// Devuelve la disposición configurada para la barra de navegación.
|
||||
pub fn layout(&self) -> &navbar::Layout {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
/// Devuelve la posición configurada para la barra de navegación.
|
||||
pub fn position(&self) -> &navbar::Position {
|
||||
&self.position
|
||||
}
|
||||
|
||||
/// Devuelve la lista de contenidos (`children`).
|
||||
pub fn items(&self) -> &Children {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ pub enum Item {
|
|||
Brand(Typed<navbar::Brand>),
|
||||
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
|
||||
Nav(Typed<Nav>),
|
||||
/// Representa un texto libre localizado.
|
||||
/// Representa un *texto localizado* libre.
|
||||
Text(L10n),
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Item::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -88,7 +88,7 @@ impl Item {
|
|||
Self::Nav(Typed::with(item))
|
||||
}
|
||||
|
||||
/// Crea un elemento de texto localizado, mostrado sin interacción.
|
||||
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
|
||||
pub fn text(item: L10n) -> Self {
|
||||
Self::Text(item)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ pub enum Position {
|
|||
}
|
||||
|
||||
impl Position {
|
||||
// Devuelve la clase base asociada a la posición de la barra de navegación.
|
||||
/// Devuelve la clase base asociada a la posición de la barra de navegación.
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
|
|
@ -76,7 +76,7 @@ impl Position {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada a la posición de la barra de navegación a la cadena de clases.
|
||||
/// Añade la clase asociada a la posición de la barra de navegación a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
|
|
@ -90,7 +90,7 @@ impl Position {
|
|||
}
|
||||
|
||||
/* Devuelve la clase asociada a la posición de la barra de navegación, o cadena vacía si no
|
||||
// aplica (reservado).
|
||||
/// aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_string()
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
//! .add_child(Dropdown::new()
|
||||
//! .with_title(L10n::n("Menu"))
|
||||
//! .add_item(dropdown::Item::label(L10n::n("Label")))
|
||||
//! .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://www.google.es"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout"))
|
||||
//! .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://google.es".into()))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into()))
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
|
|
|
|||
|
|
@ -21,23 +21,31 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// Ver ejemplo en el módulo [`offcanvas`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Offcanvas {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
title : L10n,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al panel.
|
||||
classes: Classes,
|
||||
/// Devuelve el título del panel.
|
||||
title: L10n,
|
||||
/// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel.
|
||||
breakpoint: BreakPoint,
|
||||
backdrop : offcanvas::Backdrop,
|
||||
scrolling : offcanvas::BodyScroll,
|
||||
placement : offcanvas::Placement,
|
||||
/// Devuelve el comportamiento configurado para la capa de fondo.
|
||||
backdrop: offcanvas::Backdrop,
|
||||
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
|
||||
body_scroll: offcanvas::BodyScroll,
|
||||
/// Devuelve la posición de inicio del panel.
|
||||
placement: offcanvas::Placement,
|
||||
/// Devuelve el estado inicial del panel.
|
||||
visibility: offcanvas::Visibility,
|
||||
children : Children,
|
||||
/// Devuelve la lista de componentes (`children`) del panel.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Offcanvas {
|
||||
fn new() -> Self {
|
||||
Offcanvas::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -65,14 +73,14 @@ impl Offcanvas {
|
|||
/// Establece el identificador único (`id`) del panel.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al panel.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +117,7 @@ impl Offcanvas {
|
|||
/// Permite o bloquea el desplazamiento de la página principal mientras el panel está abierto.
|
||||
#[builder_fn]
|
||||
pub fn with_body_scroll(mut self, scrolling: offcanvas::BodyScroll) -> Self {
|
||||
self.scrolling = scrolling;
|
||||
self.body_scroll = scrolling;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -141,48 +149,6 @@ impl Offcanvas {
|
|||
self
|
||||
}
|
||||
|
||||
// **< Offcanvas GETTERS >**********************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al panel.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el título del panel.
|
||||
pub fn title(&self) -> &L10n {
|
||||
&self.title
|
||||
}
|
||||
|
||||
/// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel.
|
||||
pub fn breakpoint(&self) -> &BreakPoint {
|
||||
&self.breakpoint
|
||||
}
|
||||
|
||||
/// Devuelve el comportamiento configurado para la capa de fondo.
|
||||
pub fn backdrop(&self) -> &offcanvas::Backdrop {
|
||||
&self.backdrop
|
||||
}
|
||||
|
||||
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
|
||||
pub fn body_scroll(&self) -> &offcanvas::BodyScroll {
|
||||
&self.scrolling
|
||||
}
|
||||
|
||||
/// Devuelve la posición de inicio del panel.
|
||||
pub fn placement(&self) -> &offcanvas::Placement {
|
||||
&self.placement
|
||||
}
|
||||
|
||||
/// Devuelve el estado inicial del panel.
|
||||
pub fn visibility(&self) -> &offcanvas::Visibility {
|
||||
&self.visibility
|
||||
}
|
||||
|
||||
/// Devuelve la lista de componentes (`children`) del panel.
|
||||
pub fn children(&self) -> &Children {
|
||||
&self.children
|
||||
}
|
||||
|
||||
// **< Offcanvas HELPERS >**********************************************************************
|
||||
|
||||
pub(crate) fn render_offcanvas(&self, cx: &mut Context, extra: Option<&Children>) -> Markup {
|
||||
|
|
@ -193,8 +159,8 @@ impl Offcanvas {
|
|||
}
|
||||
|
||||
let id = cx.required_id::<Self>(self.id());
|
||||
let id_label = join!(id, "-label");
|
||||
let id_target = join!("#", id);
|
||||
let id_label = util::join!(id, "-label");
|
||||
let id_target = util::join!("#", id);
|
||||
|
||||
let body_scroll = match self.body_scroll() {
|
||||
offcanvas::BodyScroll::Disabled => None,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ pub enum Placement {
|
|||
}
|
||||
|
||||
impl Placement {
|
||||
// Devuelve la clase base asociada a la posición de aparición del panel.
|
||||
/// Devuelve la clase base asociada a la posición de aparición del panel.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
|
|
@ -60,7 +60,7 @@ impl Placement {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada a la posición de aparición del panel a la cadena de clases.
|
||||
/// Añade la clase asociada a la posición de aparición del panel a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if !classes.is_empty() {
|
||||
|
|
@ -89,7 +89,7 @@ pub enum Visibility {
|
|||
}
|
||||
|
||||
impl Visibility {
|
||||
// Devuelve la clase base asociada al estado inicial del panel.
|
||||
/// Devuelve la clase base asociada al estado inicial del panel.
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
|
|
@ -98,7 +98,7 @@ impl Visibility {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada al estado inicial del panel a la cadena de clases.
|
||||
/// Añade la clase asociada al estado inicial del panel a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
|
|
|
|||
|
|
@ -106,3 +106,9 @@ $utilities: map-merge(
|
|||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Region Footer
|
||||
.region-footer {
|
||||
padding: .75rem 0 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@
|
|||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
Añadir en el archivo `Cargo.toml` del proyecto:
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ pagetop-build = { ... }
|
|||
Y crear un archivo `build.rs` a la altura de `Cargo.toml` para indicar cómo se van a incluir los
|
||||
archivos estáticos o cómo se van a compilar los archivos SCSS para el proyecto. Casos de uso:
|
||||
|
||||
## Incluir archivos estáticos desde un directorio
|
||||
### Incluir archivos estáticos desde un directorio
|
||||
|
||||
Hay que preparar una carpeta en el proyecto con todos los archivos que se quieren incluir, por
|
||||
ejemplo `static`, y añadir el siguiente código en `build.rs` para crear el conjunto de recursos:
|
||||
|
|
@ -64,7 +64,7 @@ fn main() -> std::io::Result<()> {
|
|||
}
|
||||
```
|
||||
|
||||
## Compilar archivos SCSS a CSS
|
||||
### Compilar archivos SCSS a CSS
|
||||
|
||||
Se puede compilar un archivo SCSS, que podría importar otros a su vez, para preparar un recurso con
|
||||
el archivo CSS minificado obtenido. Por ejemplo:
|
||||
|
|
@ -83,7 +83,7 @@ Este código compila el archivo `main.scss` de la carpeta `static` del proyecto,
|
|||
llamado `main_styles` que contiene el archivo `styles.min.css` obtenido.
|
||||
|
||||
|
||||
# 📦 Archivos generados
|
||||
## 📦 Archivos generados
|
||||
|
||||
Cada conjunto de recursos [`StaticFilesBundle`] genera un archivo en el directorio estándar
|
||||
[OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts)
|
||||
|
|
@ -111,14 +111,14 @@ impl Extension for MyExtension {
|
|||
```
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ use pagetop::prelude::*;
|
|||
pub struct MyExtension;
|
||||
|
||||
impl Extension for MyExtension {
|
||||
// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
|
||||
/// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
static_files_service!(scfg, guides => "/ruta/a/guides");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,16 @@
|
|||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
## Créditos
|
||||
|
||||
Esta librería incluye entre sus macros una adaptación de
|
||||
## 📚 Créditos
|
||||
|
||||
Este *crate* incluye entre sus macros una adaptación de
|
||||
[maud-macros](https://crates.io/crates/maud_macros)
|
||||
([0.27.0](https://github.com/lambda-fairy/maud/tree/v0.27.0/maud_macros)) de
|
||||
[Chris Wong](https://crates.io/users/lambda-fairy) y una versión renombrada de
|
||||
|
|
@ -29,14 +30,14 @@ necesidad de referenciar `maud` o `smart_default` en las dependencias del archiv
|
|||
cada proyecto PageTop.
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ configurables, basadas en HTML, CSS y JavaScript.
|
|||
|
||||
## Créditos
|
||||
|
||||
Esta librería incluye entre sus macros una adaptación de
|
||||
Este *crate* incluye entre sus macros una adaptación de
|
||||
[maud-macros](https://crates.io/crates/maud_macros)
|
||||
([0.27.0](https://github.com/lambda-fairy/maud/tree/v0.27.0/maud_macros)) de
|
||||
[Chris Wong](https://crates.io/users/lambda-fairy) y una versión renombrada de
|
||||
|
|
@ -85,7 +85,7 @@ pub fn html(input: TokenStream) -> TokenStream {
|
|||
/// b: 0,
|
||||
/// c: Some(0),
|
||||
/// d: vec![1, 2, 3],
|
||||
/// e: "four".to_owned(),
|
||||
/// e: "four".to_string(),
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
|
|
@ -138,7 +138,7 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
|
|||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// la macro rescribirá el método `with_` y generará un nuevo método `alter_`:
|
||||
/// la macro reescribirá el método `with_` y generará un nuevo método `alter_`:
|
||||
///
|
||||
/// ```rust
|
||||
/// # struct Example {value: Option<String>};
|
||||
|
|
@ -157,7 +157,11 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
|
|||
/// ```
|
||||
///
|
||||
/// De esta forma, cada método *builder* `with_...()` generará automáticamente su correspondiente
|
||||
/// método `alter_...()` para dejar modificar instancias existentes.
|
||||
/// método `alter_...()` para modificar instancias existentes.
|
||||
///
|
||||
/// La documentación del método `with_...()` incluirá también la firma resumida del método
|
||||
/// `alter_...()` y un alias de búsqueda con su nombre, de tal manera que buscando `alter_...` en la
|
||||
/// documentación se mostrará la entrada del método `with_...()`.
|
||||
#[proc_macro_attribute]
|
||||
pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||
use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type};
|
||||
|
|
@ -282,11 +286,11 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
// Genera el nombre del método alter_...().
|
||||
// Genera el nombre del método `alter_...()`.
|
||||
let stem = with_name_str.strip_prefix("with_").expect("validated");
|
||||
let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span());
|
||||
|
||||
// Extrae genéricos y cláusulas where.
|
||||
// Extrae genéricos y cláusulas `where`.
|
||||
let generics = &sig.generics;
|
||||
let where_clause = &sig.generics.where_clause;
|
||||
|
||||
|
|
@ -319,28 +323,75 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
v
|
||||
};
|
||||
|
||||
// Filtra los atributos descartando `#[doc]` y `#[inline]` para el método `alter_...()`.
|
||||
let non_doc_or_inline_attrs: Vec<_> = attrs
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
let p = a.path();
|
||||
!p.is_ident("doc") && !p.is_ident("inline")
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
// Separa atributos de documentación y resto.
|
||||
let mut doc_attrs = Vec::new();
|
||||
let mut other_attrs = Vec::new();
|
||||
let mut non_doc_or_inline_attrs = Vec::new();
|
||||
|
||||
// Documentación del método alter_...().
|
||||
let doc = format!("Equivale a [`Self::{with_name_str}()`], pero fuera del patrón *builder*.");
|
||||
for a in attrs.iter() {
|
||||
let p = a.path();
|
||||
if p.is_ident("doc") {
|
||||
doc_attrs.push(a.clone());
|
||||
} else {
|
||||
other_attrs.push(a.clone());
|
||||
if !p.is_ident("inline") {
|
||||
non_doc_or_inline_attrs.push(a.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Firma resumida de la función `alter_...()` para mostrarla en la doc de `with_...()`.
|
||||
let alter_sig_tokens = if args.is_empty() {
|
||||
// Sin argumentos sólo se muestra `&mut self` (puede que no tenga mucho sentido).
|
||||
quote! { #vis_pub fn #alter_ident #generics (&mut self) -> &mut Self #where_clause }
|
||||
} else {
|
||||
// Con argumentos se muestra `&mut self, ...`.
|
||||
quote! { #vis_pub fn #alter_ident #generics (&mut self, ...) -> &mut Self #where_clause }
|
||||
};
|
||||
|
||||
// Normaliza espacios raros tipo `& mut`.
|
||||
let alter_sig_str = alter_sig_tokens.to_string().replace("& mut", "&mut");
|
||||
|
||||
// Nombre de la función `alter_...()` como alias de búsqueda.
|
||||
let alter_name_str = alter_ident.to_string();
|
||||
|
||||
// Texto introductorio para la documentación adicional de `with_...()`.
|
||||
let with_alter_title = format!(
|
||||
"# {} el método `{}()` generado por [`#[builder_fn]`](pagetop_macros::builder_fn)",
|
||||
if doc_attrs.is_empty() {
|
||||
"Añade"
|
||||
} else {
|
||||
"También añade"
|
||||
},
|
||||
alter_name_str
|
||||
);
|
||||
let with_alter_doc = concat!(
|
||||
"Permite modificar la instancia actual (`&mut self`) con los mismos argumentos, ",
|
||||
"sin consumirla."
|
||||
);
|
||||
|
||||
// Atributos completos que se aplican siempre a `with_...()`.
|
||||
let with_prefix = quote! {
|
||||
#(#other_attrs)*
|
||||
#(#doc_attrs)*
|
||||
#[doc(alias = #alter_name_str)]
|
||||
#[doc = ""]
|
||||
#[doc = #with_alter_title]
|
||||
#[doc = #with_alter_doc]
|
||||
#[doc = "```text"]
|
||||
#[doc = #alter_sig_str]
|
||||
#[doc = "```"]
|
||||
};
|
||||
|
||||
// Genera el código final.
|
||||
let expanded = match body_opt {
|
||||
None => {
|
||||
quote! {
|
||||
#(#attrs)*
|
||||
#with_prefix
|
||||
fn #with_name #generics (self, #(#args),*) -> Self #where_clause;
|
||||
|
||||
#(#non_doc_or_inline_attrs)*
|
||||
#[doc = #doc]
|
||||
#[doc(hidden)]
|
||||
fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause;
|
||||
}
|
||||
}
|
||||
|
|
@ -351,8 +402,10 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
} else {
|
||||
quote! { #[inline] }
|
||||
};
|
||||
|
||||
let with_fn = if is_trait {
|
||||
quote! {
|
||||
#with_prefix
|
||||
#force_inline
|
||||
#vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause {
|
||||
let mut s = self;
|
||||
|
|
@ -362,6 +415,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
}
|
||||
} else {
|
||||
quote! {
|
||||
#with_prefix
|
||||
#force_inline
|
||||
#vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause {
|
||||
self.#alter_ident(#(#call_idents),*);
|
||||
|
|
@ -369,12 +423,12 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
#(#attrs)*
|
||||
#with_fn
|
||||
|
||||
#(#non_doc_or_inline_attrs)*
|
||||
#[doc = #doc]
|
||||
#[doc(hidden)]
|
||||
#vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause {
|
||||
#body
|
||||
}
|
||||
|
|
|
|||
21
helpers/pagetop-minimal/Cargo.toml
Normal file
21
helpers/pagetop-minimal/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "pagetop-minimal"
|
||||
version = "0.0.10"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones
|
||||
básicas en PageTop.
|
||||
"""
|
||||
categories = ["development-tools::build-utils"]
|
||||
keywords = ["pagetop", "build", "assets", "resources", "static"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
concat-string = "1.0"
|
||||
indoc = "2.0"
|
||||
pastey = "0.2"
|
||||
201
helpers/pagetop-minimal/LICENSE-APACHE
Normal file
201
helpers/pagetop-minimal/LICENSE-APACHE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022 Manuel Cillero
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
21
helpers/pagetop-minimal/LICENSE-MIT
Normal file
21
helpers/pagetop-minimal/LICENSE-MIT
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Manuel Cillero
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
61
helpers/pagetop-minimal/README.md
Normal file
61
helpers/pagetop-minimal/README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Minimal</h1>
|
||||
|
||||
<p>Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones básicas en <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-minimal)
|
||||
[](https://crates.io/crates/pagetop-minimal)
|
||||
[](https://crates.io/crates/pagetop-minimal)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-minimal#licencia)
|
||||
|
||||
</div>
|
||||
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
## 🗺️ Descripción general
|
||||
|
||||
Este *crate* proporciona un conjunto básico de macros que se integran en las utilidades de PageTop
|
||||
para optimizar operaciones habituales relacionadas con la composición estructurada de texto, la
|
||||
concatenación de cadenas y el uso rápido de colecciones clave-valor.
|
||||
|
||||
|
||||
## 📚 Créditos
|
||||
|
||||
Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del
|
||||
*crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay).
|
||||
|
||||
Las macros para la concatenación de cadenas **`join!`** y **`join_pair!`** se apoyan internamente en
|
||||
el *crate* [concat-string](https://crates.io/crates/concat_string), desarrollado por
|
||||
[FaultyRAM](https://crates.io/users/FaultyRAM), para evitar el formato de cadenas cuando la
|
||||
eficiencia pueda ser relevante.
|
||||
|
||||
La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate*
|
||||
[pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular
|
||||
`paste!` de [David Tolnay](https://crates.io/users/dtolnay).
|
||||
|
||||
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
* **Licencia MIT**
|
||||
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
|
||||
|
||||
* **Licencia Apache, Versión 2.0**
|
||||
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||
el ecosistema Rust.
|
||||
161
helpers/pagetop-minimal/src/lib.rs
Normal file
161
helpers/pagetop-minimal/src/lib.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop Minimal</h1>
|
||||
|
||||
<p>Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones básicas en <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-minimal)
|
||||
[](https://crates.io/crates/pagetop-minimal)
|
||||
[](https://crates.io/crates/pagetop-minimal)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-minimal#licencia)
|
||||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
## Descripción general
|
||||
|
||||
Este *crate* proporciona un conjunto básico de macros que se integran en las utilidades de PageTop
|
||||
para optimizar operaciones habituales relacionadas con la composición estructurada de texto, la
|
||||
concatenación de cadenas y el uso rápido de colecciones clave-valor.
|
||||
|
||||
## Créditos
|
||||
|
||||
Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del
|
||||
*crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay).
|
||||
|
||||
Las macros para la concatenación de cadenas **`join!`** y **`join_pair!`** se apoyan internamente en
|
||||
el *crate* [concat-string](https://crates.io/crates/concat_string), desarrollado por
|
||||
[FaultyRAM](https://crates.io/users/FaultyRAM), para evitar el formato de cadenas cuando la
|
||||
eficiencia pueda ser relevante.
|
||||
|
||||
La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate*
|
||||
[pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular
|
||||
`paste!` de [David Tolnay](https://crates.io/users/dtolnay).
|
||||
*/
|
||||
|
||||
#![doc(
|
||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||
)]
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use concat_string::concat_string;
|
||||
|
||||
pub use indoc::{concatdoc, formatdoc, indoc};
|
||||
|
||||
/// Permite *pegar* tokens y generar identificadores a partir de otros.
|
||||
///
|
||||
/// Dentro de `paste!`, los identificadores escritos como `[< ... >]` se combinan en uno solo que
|
||||
/// puede reutilizarse para referirse a items existentes o para definir nuevos (funciones,
|
||||
/// estructuras, métodos, etc.).
|
||||
///
|
||||
/// También admite modificadores de estilo (`lower`, `upper`, `snake`, `camel`, etc.) para
|
||||
/// transformar fragmentos interpolados antes de construir el nuevo identificador.
|
||||
pub use pastey::paste;
|
||||
// La documentación anterior se copia en `pagetop::util::paste!` porque el *crate* original no la
|
||||
// define y `pagetop` no la hereda automáticamente.
|
||||
|
||||
/// 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_minimal::join;
|
||||
/// // Concatena todos los fragmentos directamente.
|
||||
/// let result = join!("Hello", " ", "World");
|
||||
/// assert_eq!(result, "Hello World".to_string());
|
||||
///
|
||||
/// // También funciona con valores vacíos.
|
||||
/// let result_with_empty = join!("Hello", "", "World");
|
||||
/// assert_eq!(result_with_empty, "HelloWorld".to_string());
|
||||
///
|
||||
/// // Un único fragmento devuelve el mismo valor.
|
||||
/// let single_result = join!("Hello");
|
||||
/// assert_eq!(single_result, "Hello".to_string());
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! join {
|
||||
($($arg:expr),+) => {
|
||||
$crate::concat_string!($($arg),+)
|
||||
};
|
||||
}
|
||||
|
||||
/// Concatena dos fragmentos en un [`String`] usando un separador inteligente.
|
||||
///
|
||||
/// 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_minimal::join_pair;
|
||||
/// let first = "Hello";
|
||||
/// let separator = "-";
|
||||
/// let second = "World";
|
||||
///
|
||||
/// // Concatena los dos fragmentos cuando ambos no están vacíos.
|
||||
/// let result = join_pair!(first, separator, second);
|
||||
/// assert_eq!(result, "Hello-World".to_string());
|
||||
///
|
||||
/// // Si el primer fragmento está vacío, devuelve el segundo.
|
||||
/// let result_empty_first = join_pair!("", separator, second);
|
||||
/// assert_eq!(result_empty_first, "World".to_string());
|
||||
///
|
||||
/// // Si el segundo fragmento está vacío, devuelve el primero.
|
||||
/// let result_empty_second = join_pair!(first, separator, "");
|
||||
/// assert_eq!(result_empty_second, "Hello".to_string());
|
||||
///
|
||||
/// // Si ambos fragmentos están vacíos, devuelve una cadena vacía.
|
||||
/// let result_both_empty = join_pair!("", separator, "");
|
||||
/// assert_eq!(result_both_empty, "".to_string());
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! join_pair {
|
||||
($first:expr, $separator:expr, $second:expr) => {{
|
||||
let first_val = $first;
|
||||
let second_val = $second;
|
||||
let separator_val = $separator;
|
||||
|
||||
let first = AsRef::<str>::as_ref(&first_val);
|
||||
let second = AsRef::<str>::as_ref(&second_val);
|
||||
let separator = if first.is_empty() || second.is_empty() {
|
||||
""
|
||||
} else {
|
||||
AsRef::<str>::as_ref(&separator_val)
|
||||
};
|
||||
|
||||
$crate::concat_string!(first, separator, second)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Macro para construir una colección de pares clave-valor.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_minimal::kv;
|
||||
/// # use std::collections::HashMap;
|
||||
/// let args:HashMap<&str, String> = kv![
|
||||
/// "userName" => "Roberto",
|
||||
/// "photoCount" => "3",
|
||||
/// "userGender" => "male",
|
||||
/// ];
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! kv {
|
||||
( $($key:expr => $value:expr),* $(,)? ) => {{
|
||||
let mut a = std::collections::HashMap::new();
|
||||
$(
|
||||
a.insert($key.into(), $value.into());
|
||||
)*
|
||||
a
|
||||
}};
|
||||
}
|
||||
|
|
@ -11,19 +11,21 @@
|
|||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
## Descripción general
|
||||
|
||||
Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
## 🗺️ Descripción general
|
||||
|
||||
Este *crate* permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
|
||||
compilación.
|
||||
|
||||
## Créditos
|
||||
|
||||
## 📚 Créditos
|
||||
|
||||
Para ello, adapta el código de los *crates* [static-files](https://crates.io/crates/static_files)
|
||||
(versión [0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) y
|
||||
|
|
@ -35,14 +37,14 @@ Estas implementaciones se integran en PageTop para evitar que cada proyecto teng
|
|||
`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
|
||||
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**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ configurables, basadas en HTML, CSS y JavaScript.
|
|||
|
||||
## Descripción general
|
||||
|
||||
Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
Este *crate* permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
|
||||
compilación.
|
||||
|
||||
|
|
|
|||
17
src/app.rs
17
src/app.rs
|
|
@ -4,9 +4,10 @@ mod figfont;
|
|||
|
||||
use crate::core::{extension, extension::ExtensionRef};
|
||||
use crate::html::Markup;
|
||||
use crate::locale::Locale;
|
||||
use crate::response::page::{ErrorPage, ResultPage};
|
||||
use crate::service::HttpRequest;
|
||||
use crate::{global, locale, service, trace};
|
||||
use crate::{global, service, trace, PAGETOP_VERSION};
|
||||
|
||||
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
|
||||
use actix_session::storage::CookieSessionStore;
|
||||
|
|
@ -49,7 +50,7 @@ impl Application {
|
|||
Self::internal_prepare(Some(root_extension))
|
||||
}
|
||||
|
||||
// Método interno para preparar la aplicación, opcionalmente con una extensión.
|
||||
/// Método interno para preparar la aplicación, opcionalmente con una extensión.
|
||||
fn internal_prepare(root_extension: Option<ExtensionRef>) -> Self {
|
||||
// Al arrancar muestra una cabecera para la aplicación.
|
||||
Self::show_banner();
|
||||
|
|
@ -57,8 +58,8 @@ impl Application {
|
|||
// Inicia gestión de trazas y registro de eventos (logging).
|
||||
LazyLock::force(&trace::TRACING);
|
||||
|
||||
// Valida el identificador de idioma por defecto.
|
||||
LazyLock::force(&locale::DEFAULT_LANGID);
|
||||
// Inicializa el idioma predeterminado.
|
||||
Locale::init();
|
||||
|
||||
// Registra las extensiones de la aplicación.
|
||||
extension::all::register_extensions(root_extension);
|
||||
|
|
@ -72,12 +73,12 @@ impl Application {
|
|||
Self
|
||||
}
|
||||
|
||||
// Muestra una cabecera para la aplicación basada en la configuración.
|
||||
/// Muestra una cabecera para la aplicación basada en la configuración.
|
||||
fn show_banner() {
|
||||
use colored::Colorize;
|
||||
use terminal_size::{terminal_size, Width};
|
||||
|
||||
if global::SETTINGS.app.startup_banner.to_lowercase() != "off" {
|
||||
if global::SETTINGS.app.startup_banner != global::StartupBanner::Off {
|
||||
// Nombre de la aplicación, ajustado al ancho del terminal si es necesario.
|
||||
let mut app_ff = String::new();
|
||||
let app_name = &global::SETTINGS.app.name;
|
||||
|
|
@ -108,7 +109,7 @@ impl Application {
|
|||
println!(
|
||||
"{} {}\n",
|
||||
"Powered by PageTop".yellow(),
|
||||
env!("CARGO_PKG_VERSION").yellow()
|
||||
PAGETOP_VERSION.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -163,7 +164,7 @@ impl Application {
|
|||
Self::service_app()
|
||||
}
|
||||
|
||||
// Configura el servicio web de la aplicación.
|
||||
/// Configura el servicio web de la aplicación.
|
||||
fn service_app() -> service::App<
|
||||
impl service::Factory<
|
||||
service::Request,
|
||||
|
|
|
|||
|
|
@ -10,21 +10,11 @@ pub static FIGFONT: LazyLock<FIGfont> = LazyLock::new(|| {
|
|||
let speed = include_str!("speed.flf");
|
||||
let starwars = include_str!("starwars.flf");
|
||||
|
||||
FIGfont::from_content(
|
||||
match global::SETTINGS.app.startup_banner.to_lowercase().as_str() {
|
||||
"off" => slant,
|
||||
"slant" => slant,
|
||||
"small" => small,
|
||||
"speed" => speed,
|
||||
"starwars" => starwars,
|
||||
_ => {
|
||||
println!(
|
||||
"\n FIGfont \"{}\" not found for banner. Using \"Slant\". Check settings.",
|
||||
global::SETTINGS.app.startup_banner,
|
||||
);
|
||||
slant
|
||||
}
|
||||
},
|
||||
)
|
||||
FIGfont::from_content(match global::SETTINGS.app.startup_banner {
|
||||
global::StartupBanner::Off | global::StartupBanner::Slant => slant,
|
||||
global::StartupBanner::Small => small,
|
||||
global::StartupBanner::Speed => speed,
|
||||
global::StartupBanner::Starwars => starwars,
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ impl<C: Component> AfterRender<C> {
|
|||
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
|
||||
/// `C` con identificador `id`.
|
||||
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.referer_id.alter_value(id);
|
||||
self.referer_id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ impl<C: Component> AfterRender<C> {
|
|||
self
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
|
||||
// Primero despacha las acciones para el tipo de componente.
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ impl<C: Component> BeforeRender<C> {
|
|||
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
|
||||
/// `C` con identificador `id`.
|
||||
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.referer_id.alter_value(id);
|
||||
self.referer_id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ impl<C: Component> BeforeRender<C> {
|
|||
self
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
|
||||
// Primero despacha las acciones para el tipo de componente.
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ impl AfterRenderBody {
|
|||
self
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline(always)]
|
||||
#[allow(clippy::inline_always)]
|
||||
pub(crate) fn dispatch(page: &mut Page) {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ impl BeforeRenderBody {
|
|||
self
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline(always)]
|
||||
#[allow(clippy::inline_always)]
|
||||
pub(crate) fn dispatch(page: &mut Page) {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ impl<C: Component> AfterRender<C> {
|
|||
}
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
|
||||
dispatch_actions(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ impl<C: Component> BeforeRender<C> {
|
|||
}
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
|
||||
dispatch_actions(
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ impl<C: Component> PrepareRender<C> {
|
|||
}
|
||||
}
|
||||
|
||||
// Despacha las acciones. Se detiene en cuanto una renderiza.
|
||||
/// Despacha las acciones. Se detiene en cuanto una renderiza.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &C, cx: &mut Context) -> PrepareMarkup {
|
||||
let mut render_component = PrepareMarkup::None;
|
||||
|
|
|
|||
|
|
@ -1,46 +1,8 @@
|
|||
//! Componentes nativos proporcionados por PageTop.
|
||||
//!
|
||||
//! Conviene destacar que PageTop distingue entre:
|
||||
//!
|
||||
//! - **Componentes estructurales** que definen el esqueleto de un documento HTML, como [`Template`]
|
||||
//! y [`Region`], utilizados por [`Page`](crate::response::page::Page) para generar la estructura
|
||||
//! final.
|
||||
//! - **Componentes de contenido** (menús, barras, tarjetas, etc.), que se incluyen en las regiones
|
||||
//! gestionadas por los componentes estructurales.
|
||||
//!
|
||||
//! El componente [`Template`] describe cómo maquetar el cuerpo del documento a partir de varias
|
||||
//! regiones lógicas ([`Region`]). En función de la plantilla seleccionada, determina qué regiones
|
||||
//! se renderizan y en qué orden. Por ejemplo, la plantilla predeterminada [`Template::DEFAULT`]
|
||||
//! utiliza las regiones [`Region::HEADER`], [`Region::CONTENT`] y [`Region::FOOTER`].
|
||||
//!
|
||||
//! Un componente [`Region`] es un contenedor lógico asociado a un nombre de región. Su contenido se
|
||||
//! obtiene del [`Context`](crate::core::component::Context), donde los componentes se registran
|
||||
//! mediante [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in) y
|
||||
//! otros mecanismos similares, y se integra en el documento a través de [`Template`].
|
||||
//!
|
||||
//! Por su parte, una página ([`Page`](crate::response::page::Page)) representa un documento HTML
|
||||
//! completo. Implementa [`Contextual`](crate::core::component::Contextual) para mantener su propio
|
||||
//! [`Context`](crate::core::component::Context), donde gestiona el tema activo, la plantilla
|
||||
//! seleccionada y los componentes asociados a cada región, y se encarga de generar la estructura
|
||||
//! final de la página.
|
||||
//!
|
||||
//! De este modo, temas y extensiones colaboran sobre una estructura común: las aplicaciones
|
||||
//! registran componentes en el [`Context`](crate::core::component::Context), las plantillas
|
||||
//! organizan las regiones y las páginas generan el documento HTML resultante.
|
||||
//!
|
||||
//! Los temas pueden sobrescribir [`Template`] para exponer nuevas plantillas o adaptar las
|
||||
//! predeterminadas, y lo mismo con [`Region`] para añadir regiones adicionales o personalizar su
|
||||
//! representación.
|
||||
|
||||
mod html;
|
||||
pub use html::Html;
|
||||
|
||||
mod region;
|
||||
pub use region::Region;
|
||||
|
||||
mod template;
|
||||
pub use template::Template;
|
||||
|
||||
mod block;
|
||||
pub use block::Block;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,18 +4,21 @@ use crate::prelude::*;
|
|||
///
|
||||
/// 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)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct Block {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
title : L10n,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al bloque.
|
||||
classes: Classes,
|
||||
/// Devuelve el título del bloque.
|
||||
title: L10n,
|
||||
/// Devuelve la lista de componentes hijo del bloque.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Block {
|
||||
fn new() -> Self {
|
||||
Block::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -52,14 +55,14 @@ impl Block {
|
|||
/// 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.id.alter_id(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.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -83,21 +86,4 @@ impl Block {
|
|||
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.
|
||||
pub fn title(&self) -> &L10n {
|
||||
&self.title
|
||||
}
|
||||
|
||||
/// Devuelve la lista de componentes (`children`) del bloque.
|
||||
pub fn children(&self) -> &Children {
|
||||
&self.children
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente básico para renderizar dinámicamente código HTML recibiendo el contexto.
|
||||
/// Componente básico que renderiza dinámicamente código HTML según el contexto.
|
||||
///
|
||||
/// Este componente permite generar contenido HTML arbitrario, usando la macro `html!` y accediendo
|
||||
/// opcionalmente al contexto de renderizado.
|
||||
|
|
@ -33,12 +33,13 @@ pub struct Html(Box<dyn Fn(&mut Context) -> Markup + Send + Sync>);
|
|||
|
||||
impl Default for Html {
|
||||
fn default() -> Self {
|
||||
Html::with(|_| html! {})
|
||||
Self::with(|_| html! {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Html {
|
||||
fn new() -> Self {
|
||||
Html::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
|
|
@ -51,9 +52,8 @@ impl Html {
|
|||
|
||||
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
|
||||
///
|
||||
/// El método [`prepare_component()`](crate::core::component::Component::prepare_component)
|
||||
/// delega el renderizado en la función proporcionada, que recibe una referencia mutable al
|
||||
/// contexto de renderizado ([`Context`]).
|
||||
/// El método [`Self::prepare_component()`] delega el renderizado a la función que aquí se
|
||||
/// proporciona, que recibe una referencia mutable al [`Context`].
|
||||
pub fn with<F>(f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
|
||||
|
|
@ -64,8 +64,8 @@ impl Html {
|
|||
/// Sustituye la función que genera el `Markup`.
|
||||
///
|
||||
/// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando
|
||||
/// [`prepare_component()`](crate::core::component::Component::prepare_component) invoque esta
|
||||
/// instancia. La nueva función también recibe una referencia al contexto ([`Context`]).
|
||||
/// [`Self::prepare_component()`] invoque esta instancia. La nueva función también recibe una
|
||||
/// referencia al [`Context`].
|
||||
#[builder_fn]
|
||||
pub fn with_fn<F>(mut self, f: F) -> Self
|
||||
where
|
||||
|
|
|
|||
|
|
@ -17,19 +17,19 @@ pub enum IntroOpening {
|
|||
Custom,
|
||||
}
|
||||
|
||||
/// Componente para presentar PageTop (como [`Welcome`](crate::base::extension::Welcome)), o mostrar
|
||||
/// introducciones.
|
||||
/// Componente para divulgar PageTop (como hace [`Welcome`](crate::base::extension::Welcome)), o
|
||||
/// mostrar presentaciones.
|
||||
///
|
||||
/// Usa la imagen de PageTop para presentar contenidos con:
|
||||
/// Usa la imagen de PageTop para mostrar:
|
||||
///
|
||||
/// - Una **imagen decorativa** (el *monster* de PageTop) antecediendo al contenido.
|
||||
/// - Una vista destacada con **título + eslogan**.
|
||||
/// - Una **figura decorativa** (que incluye el *monster* de PageTop) antecediendo al contenido.
|
||||
/// - Una vista destacada del **título** de la página con un **eslogan** de presentación.
|
||||
/// - Un **botón opcional** de llamada a la acción con texto y enlace configurables.
|
||||
/// - El **área de textos** con *badges* predefinidos (en modo [`IntroOpening::PageTop`]) y bloques
|
||||
/// ([`Block`](crate::base::component::Block)) para crear párrafos vistosos de texto. Aunque
|
||||
/// admite todo tipo de componentes.
|
||||
/// - Un **área para la presentación de contenidos**, con *badges* informativos de PageTop (si se
|
||||
/// opta por [`IntroOpening::PageTop`]) y bloques ([`Block`](crate::base::component::Block)) de
|
||||
/// contenido libre para crear párrafos vistosos de texto. Aunque admite todo tipo de componentes.
|
||||
///
|
||||
/// ### Ejemplos
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// **Intro mínima por defecto**
|
||||
///
|
||||
|
|
@ -47,11 +47,11 @@ pub enum IntroOpening {
|
|||
/// .with_slogan(L10n::l("intro_custom_slogan"))
|
||||
/// .with_button(Some((
|
||||
/// L10n::l("intro_learn_more"),
|
||||
/// |_| "/learn-more"
|
||||
/// |_| "/learn-more".into()
|
||||
/// )));
|
||||
/// ```
|
||||
///
|
||||
/// **Sin botón + modo *Custom* (sin *badges* predefinidos)**
|
||||
/// **Sin botón y en modo *Custom* (sin *badges* predefinidos)**
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -76,23 +76,29 @@ pub enum IntroOpening {
|
|||
/// })),
|
||||
/// );
|
||||
/// ```
|
||||
#[rustfmt::skip]
|
||||
#[derive(Getters)]
|
||||
pub struct Intro {
|
||||
title : L10n,
|
||||
slogan : L10n,
|
||||
button : Option<(L10n, FnPathByContext)>,
|
||||
opening : IntroOpening,
|
||||
/// Devuelve el título de entrada.
|
||||
title: L10n,
|
||||
/// Devuelve el eslogan de la entrada.
|
||||
slogan: L10n,
|
||||
/// Devuelve el botón de llamada a la acción, si existe.
|
||||
button: Option<(L10n, FnPathByContext)>,
|
||||
/// Devuelve el modo de apertura configurado.
|
||||
opening: IntroOpening,
|
||||
/// Devuelve la lista de componentes hijo de la intro.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Default for Intro {
|
||||
#[rustfmt::skip]
|
||||
fn default() -> Self {
|
||||
const BUTTON_LINK: &str = "https://pagetop.cillero.es";
|
||||
|
||||
Intro {
|
||||
title : L10n::l("intro_default_title"),
|
||||
slogan : L10n::l("intro_default_slogan").with_arg("app", &global::SETTINGS.app.name),
|
||||
button : Some((L10n::l("intro_default_button"), |_| "https://pagetop.cillero.es")),
|
||||
opening : IntroOpening::default(),
|
||||
title: L10n::l("intro_default_title"),
|
||||
slogan: L10n::l("intro_default_slogan").with_arg("app", &global::SETTINGS.app.name),
|
||||
button: Some((L10n::l("intro_default_button"), |_| BUTTON_LINK.into())),
|
||||
opening: IntroOpening::default(),
|
||||
children: Children::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -100,17 +106,17 @@ impl Default for Intro {
|
|||
|
||||
impl Component for Intro {
|
||||
fn new() -> Self {
|
||||
Intro::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, cx: &mut Context) {
|
||||
cx.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/intro.css").with_version(env!("CARGO_PKG_VERSION")),
|
||||
StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION),
|
||||
));
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
if self.opening() == IntroOpening::PageTop {
|
||||
if *self.opening() == IntroOpening::PageTop {
|
||||
cx.alter_assets(ContextOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
||||
util::indoc!(r#"
|
||||
try {
|
||||
|
|
@ -163,7 +169,7 @@ impl Component for Intro {
|
|||
}
|
||||
}
|
||||
div class="intro-text__children" {
|
||||
@if self.opening() == IntroOpening::PageTop {
|
||||
@if *self.opening() == IntroOpening::PageTop {
|
||||
p { (L10n::l("intro_text1").using(cx)) }
|
||||
div id="intro-badges" {
|
||||
img
|
||||
|
|
@ -246,7 +252,7 @@ impl Intro {
|
|||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// // Define un botón con texto y una URL fija.
|
||||
/// let intro = Intro::default().with_button(Some((L10n::n("Learn more"), |_| "/start")));
|
||||
/// let intro = Intro::default().with_button(Some((L10n::n("Start"), |_| "/start".into())));
|
||||
/// // Descarta el botón de la intro.
|
||||
/// let intro_no_button = Intro::default().with_button(None);
|
||||
/// ```
|
||||
|
|
@ -289,31 +295,4 @@ impl Intro {
|
|||
self.children.alter_child(op);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Intro GETTERS >**************************************************************************
|
||||
|
||||
/// Devuelve el título de entrada.
|
||||
pub fn title(&self) -> &L10n {
|
||||
&self.title
|
||||
}
|
||||
|
||||
/// Devuelve el eslogan de la entrada.
|
||||
pub fn slogan(&self) -> &L10n {
|
||||
&self.slogan
|
||||
}
|
||||
|
||||
/// Devuelve el botón de llamada a la acción, si existe.
|
||||
pub fn button(&self) -> Option<(&L10n, &FnPathByContext)> {
|
||||
self.button.as_ref().map(|(txt, lnk)| (txt, lnk))
|
||||
}
|
||||
|
||||
/// Devuelve el modo de apertura configurado.
|
||||
pub fn opening(&self) -> IntroOpening {
|
||||
self.opening
|
||||
}
|
||||
|
||||
/// Devuelve la lista de componentes (`children`) de la intro.
|
||||
pub fn children(&self) -> &Children {
|
||||
&self.children
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ use crate::prelude::*;
|
|||
// Enlace a la página oficial de PageTop.
|
||||
const LINK: &str = "<a href=\"https://pagetop.cillero.es\" rel=\"noopener noreferrer\">PageTop</a>";
|
||||
|
||||
/// Componente que informa del 'Powered by' (*Funciona con*) típica del pie de página.
|
||||
/// Componente que muestra el típico mensaje *Powered by* (*Funciona con*) en el 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)]
|
||||
#[derive(AutoDefault, Getters)]
|
||||
pub struct PoweredBy {
|
||||
/// Devuelve el texto de copyright actual, si existe.
|
||||
copyright: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -17,10 +18,10 @@ 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`].
|
||||
/// configurada en [`global::SETTINGS`], en el formato `YYYY © Nombre de la aplicación`.
|
||||
fn new() -> Self {
|
||||
let year = Utc::now().format("%Y").to_string();
|
||||
let c = join!(year, " © ", global::SETTINGS.app.name);
|
||||
let c = util::join!(year, " © ", global::SETTINGS.app.name);
|
||||
PoweredBy { copyright: Some(c) }
|
||||
}
|
||||
|
||||
|
|
@ -56,11 +57,4 @@ impl PoweredBy {
|
|||
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,150 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente estructural que renderiza el contenido de una región del documento.
|
||||
///
|
||||
/// `Region` actúa como un contenedor lógico asociado a un nombre de región. Su contenido se obtiene
|
||||
/// del contexto de renderizado ([`Context`]), donde los componentes suelen registrarse con métodos
|
||||
/// como [`Contextual::with_child_in()`]. Cada región puede integrarse posteriormente en el cuerpo
|
||||
/// del documento mediante [`Template`], normalmente desde una página ([`Page`]).
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Region {
|
||||
#[default(AttrName::new(Self::DEFAULT))]
|
||||
name: AttrName,
|
||||
#[default(L10n::l("region-content"))]
|
||||
label: L10n,
|
||||
}
|
||||
|
||||
impl Component for Region {
|
||||
fn new() -> Self {
|
||||
Region::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.name.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let Some(name) = self.name().get() else {
|
||||
return PrepareMarkup::None;
|
||||
};
|
||||
let output = cx.render_region(&name);
|
||||
if output.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
PrepareMarkup::With(html! {
|
||||
div
|
||||
id=[self.id()]
|
||||
class=(join!("region region-", &name))
|
||||
role="region"
|
||||
aria-label=[self.label().lookup(cx)]
|
||||
{
|
||||
(output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Region {
|
||||
/// Región especial situada al **inicio del documento**.
|
||||
///
|
||||
/// Su función es proporcionar un punto estable donde las extensiones puedan inyectar contenido
|
||||
/// global antes de renderizar el resto de regiones principales (cabecera, contenido, etc.).
|
||||
///
|
||||
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
|
||||
/// sino como punto de anclaje para elementos auxiliares, marcadores técnicos, inicializadores o
|
||||
/// contenido de depuración que deban situarse en la parte superior del documento.
|
||||
///
|
||||
/// Se considera una región **reservada** para este tipo de usos globales.
|
||||
pub const PAGETOP: &str = "page-top";
|
||||
|
||||
/// Región estándar para la **cabecera** del documento.
|
||||
///
|
||||
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
|
||||
pub const HEADER: &str = "header";
|
||||
|
||||
/// Región principal de **contenido**.
|
||||
///
|
||||
/// Es la región donde se espera que se renderice el contenido principal de la página (p. ej.
|
||||
/// cuerpo de la ruta actual, bloques centrales, vistas principales, etc.). En muchos temas será
|
||||
/// la región mínima imprescindible para que la página tenga sentido.
|
||||
pub const CONTENT: &str = "content";
|
||||
|
||||
/// Región estándar para el **pie de página**.
|
||||
///
|
||||
/// Suele contener información legal, enlaces secundarios, créditos, etc.
|
||||
pub const FOOTER: &str = "footer";
|
||||
|
||||
/// Región especial situada al **final del documento**.
|
||||
///
|
||||
/// Pensada para proporcionar un punto estable donde las extensiones puedan inyectar contenido
|
||||
/// global después de renderizar el resto de regiones principales (cabecera, contenido, etc.).
|
||||
///
|
||||
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
|
||||
/// sino como punto de anclaje para elementos auxiliares asociados a comportamientos dinámicos
|
||||
/// que deban situarse en la parte inferior del documento.
|
||||
///
|
||||
/// Igual que [`Self::PAGETOP`], se considera una región **reservada** para este tipo de usos
|
||||
/// globales.
|
||||
pub const PAGEBOTTOM: &str = "page-bottom";
|
||||
|
||||
/// Región por defecto que se asigna cuando no se especifica ningún nombre.
|
||||
///
|
||||
/// Por diseño, la región por defecto es la de contenido principal ([`Self::CONTENT`]), de
|
||||
/// manera que un tema sencillo pueda limitarse a definir una sola región funcional.
|
||||
pub const DEFAULT: &str = Self::CONTENT;
|
||||
|
||||
/// Prepara una región para el nombre indicado.
|
||||
///
|
||||
/// El valor de `name` se utiliza como nombre de la región y como identificador (`id`) del
|
||||
/// contenedor. Al renderizarse, este componente mostrará el contenido registrado en el contexto
|
||||
/// bajo ese nombre.
|
||||
pub fn named(name: impl AsRef<str>) -> Self {
|
||||
Region {
|
||||
name: AttrName::new(name),
|
||||
label: L10n::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepara una región para el nombre indicado con una etiqueta de accesibilidad.
|
||||
///
|
||||
/// El valor de `name` se utiliza como nombre de la región y como identificador (`id`) del
|
||||
/// contenedor, mientras que `label` será el texto localizado que se usará como `aria-label` del
|
||||
/// contenedor.
|
||||
pub fn labeled(name: impl AsRef<str>, label: L10n) -> Self {
|
||||
Region {
|
||||
name: AttrName::new(name),
|
||||
label,
|
||||
}
|
||||
}
|
||||
|
||||
// **< Region BUILDER >*************************************************************************
|
||||
|
||||
/// Establece o modifica el nombre de la región.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_value(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la etiqueta localizada de la región.
|
||||
///
|
||||
/// Esta etiqueta se utiliza como `aria-label` del contenedor predefinido `<div role="region">`,
|
||||
/// lo que mejora la accesibilidad para lectores de pantalla y otras tecnologías de apoyo.
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: L10n) -> Self {
|
||||
self.label = label;
|
||||
self
|
||||
}
|
||||
|
||||
// **< Region GETTERS >*************************************************************************
|
||||
|
||||
/// Devuelve el nombre de la región.
|
||||
pub fn name(&self) -> &AttrName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Devuelve la etiqueta localizada asociada a la región.
|
||||
pub fn label(&self) -> &L10n {
|
||||
&self.label
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente estructural para renderizar plantillas de contenido.
|
||||
///
|
||||
/// `Template` describe cómo se compone el cuerpo del documento a partir de varias regiones lógicas
|
||||
/// ([`Region`]). En función de su nombre, decide qué regiones se renderizan y en qué orden.
|
||||
///
|
||||
/// Normalmente se invoca desde una página ([`Page`]), que consulta el nombre de plantilla guardado
|
||||
/// en el [`Context`] y delega en `Template` la composición de las regiones que forman el cuerpo del
|
||||
/// documento.
|
||||
///
|
||||
/// Los temas pueden sobrescribir este componente para exponer sus propias plantillas o adaptar las
|
||||
/// plantillas predeterminadas.
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Template {
|
||||
#[default(AttrName::new(Self::DEFAULT))]
|
||||
name: AttrName,
|
||||
}
|
||||
|
||||
impl Component for Template {
|
||||
fn new() -> Self {
|
||||
Template::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.name.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let Some(name) = self.name().get() else {
|
||||
return PrepareMarkup::None;
|
||||
};
|
||||
match name.as_str() {
|
||||
Self::DEFAULT | Self::ERROR => PrepareMarkup::With(html! {
|
||||
(Region::labeled(Region::HEADER, L10n::l("region-header")).render(cx))
|
||||
(Region::default().render(cx))
|
||||
(Region::labeled(Region::FOOTER, L10n::l("region-footer")).render(cx))
|
||||
}),
|
||||
_ => PrepareMarkup::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Template {
|
||||
/// Nombre de la plantilla predeterminada.
|
||||
///
|
||||
/// Por defecto define una estructura básica con las regiones [`Region::HEADER`],
|
||||
/// [`Region::CONTENT`] y [`Region::FOOTER`], en ese orden. Esta plantilla se usa cuando no se
|
||||
/// selecciona ninguna otra de forma explícita (ver [`Contextual::with_template()`]).
|
||||
pub const DEFAULT: &str = "default";
|
||||
|
||||
/// Nombre de la plantilla de error.
|
||||
///
|
||||
/// Se utiliza para páginas de error u otros estados excepcionales. Por defecto reutiliza
|
||||
/// la misma estructura que [`Self::DEFAULT`], pero permite a temas y extensiones distinguir
|
||||
/// el contexto de error para aplicar estilos o contenidos específicos.
|
||||
pub const ERROR: &str = "error";
|
||||
|
||||
/// Selecciona la plantilla asociada al nombre indicado.
|
||||
///
|
||||
/// El valor de `name` se utiliza como nombre de la plantilla y como identificador (`id`) del
|
||||
/// componente.
|
||||
pub fn named(name: impl AsRef<str>) -> Self {
|
||||
Template {
|
||||
name: AttrName::new(name),
|
||||
}
|
||||
}
|
||||
|
||||
// **< Template BUILDER >***********************************************************************
|
||||
|
||||
/// Establece o modifica el nombre de la plantilla seleccionada.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_value(name);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Template GETTERS >***********************************************************************
|
||||
|
||||
/// Devuelve el nombre de la plantilla seleccionada.
|
||||
pub fn name(&self) -> &AttrName {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Página de bienvenida predeterminada de PageTop.
|
||||
/// Página de bienvenida de PageTop.
|
||||
///
|
||||
/// Esta extensión se instala por defecto y muestra una página en la ruta raíz (`/`) cuando no se ha
|
||||
/// configurado ninguna página de inicio personalizada. Permite confirmar que el servidor está
|
||||
/// funcionando correctamente.
|
||||
/// Esta extensión se instala por defecto si el ajuste de configuración [`global::App::welcome`] es
|
||||
/// `true`. Muestra una página de bienvenida de PageTop en la ruta raíz (`/`).
|
||||
///
|
||||
/// No obstante, cualquier extensión puede sobrescribir este comportamiento si utiliza estas mismas
|
||||
/// rutas.
|
||||
///
|
||||
/// Resulta útil en demos o para comprobar rápidamente que el servidor ha arrancado correctamente.
|
||||
pub struct Welcome;
|
||||
|
||||
impl Extension for Welcome {
|
||||
|
|
@ -17,11 +21,11 @@ impl Extension for Welcome {
|
|||
}
|
||||
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(homepage));
|
||||
scfg.route("/", service::web::get().to(home));
|
||||
}
|
||||
}
|
||||
|
||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
async fn home(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
let app = &global::SETTINGS.app.name;
|
||||
|
||||
Page::new(request)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,19 @@ impl Extension for Basic {
|
|||
|
||||
impl Theme for Basic {
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_param("include_basic_assets", true);
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/basic.css")
|
||||
.with_version(PAGETOP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Carga las opciones de configuración.
|
||||
//! Carga las opciones de configuración de la aplicación.
|
||||
//!
|
||||
//! Estos ajustes se obtienen de archivos [TOML](https://toml.io) como pares `clave = valor` que se
|
||||
//! mapean a estructuras **fuertemente tipadas** y valores predefinidos.
|
||||
|
|
@ -125,7 +125,7 @@ const DEFAULT_CONFIG_DIR: &str = "config";
|
|||
// Modo de ejecución por defecto.
|
||||
const DEFAULT_RUN_MODE: &str = "default";
|
||||
|
||||
/// Valores originales cargados desde los archivos de configuración como pares `clave = valor`.
|
||||
/// Valores originales de los archivos de configuración como pares `clave = valor`.
|
||||
pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(|| {
|
||||
// CONFIG_DIR (si existe) o DEFAULT_CONFIG_DIR. Si no se puede resolver, se usa tal cual.
|
||||
let dir = env::var_os("CONFIG_DIR").unwrap_or_else(|| DEFAULT_CONFIG_DIR.into());
|
||||
|
|
@ -229,10 +229,11 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
|
|||
macro_rules! include_config {
|
||||
( $SETTINGS_NAME:ident : $Settings_Type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => {
|
||||
#[doc = concat!(
|
||||
"Instancia los ajustes de configuración para [`", stringify!($Settings_Type), "`]."
|
||||
"Ajustes de configuración y **valores por defecto** para ",
|
||||
"[`", stringify!($Settings_Type), "`]."
|
||||
)]
|
||||
#[doc = ""]
|
||||
#[doc = "Valores por defecto:"]
|
||||
#[doc = "Valores predeterminados que se aplican en ausencia de configuración:"]
|
||||
#[doc = "```text"]
|
||||
$(
|
||||
#[doc = concat!($k, " = ", stringify!($v))]
|
||||
|
|
|
|||
44
src/core.rs
44
src/core.rs
|
|
@ -30,28 +30,28 @@ impl TypeInfo {
|
|||
}
|
||||
}
|
||||
|
||||
// Extrae un rango de segmentos de `type_name` (tokens separados por `::`).
|
||||
//
|
||||
// Los argumentos `start` y `end` identifican los índices de los segmentos teniendo en cuenta:
|
||||
//
|
||||
// * Los índices positivos cuentan **desde la izquierda**, empezando en `0`.
|
||||
// * Los índices negativos cuentan **desde la derecha**, `-1` es el último.
|
||||
// * Si `end` es `None`, el corte llega hasta el último segmento.
|
||||
// * Si la selección resulta vacía por índices desordenados o segmento inexistente, se devuelve
|
||||
// la cadena vacía.
|
||||
//
|
||||
// Ejemplos (con `type_name = "alloc::vec::Vec<i32>"`):
|
||||
//
|
||||
// | Llamada | Resultado |
|
||||
// |------------------------------|--------------------------|
|
||||
// | `partial(..., 0, None)` | `"alloc::vec::Vec<i32>"` |
|
||||
// | `partial(..., 1, None)` | `"vec::Vec<i32>"` |
|
||||
// | `partial(..., -1, None)` | `"Vec<i32>"` |
|
||||
// | `partial(..., 0, Some(-2))` | `"alloc::vec"` |
|
||||
// | `partial(..., -5, None)` | `"alloc::vec::Vec<i32>"` |
|
||||
//
|
||||
// La porción devuelta vive tanto como `'static` porque `type_name` es `'static` y sólo se
|
||||
// presta.
|
||||
/// Extrae un rango de segmentos de `type_name` (tokens separados por `::`).
|
||||
///
|
||||
/// Los argumentos `start` y `end` identifican los índices de los segmentos teniendo en cuenta:
|
||||
///
|
||||
/// * Los índices positivos cuentan **desde la izquierda**, empezando en `0`.
|
||||
/// * Los índices negativos cuentan **desde la derecha**, `-1` es el último.
|
||||
/// * Si `end` es `None`, el corte llega hasta el último segmento.
|
||||
/// * Si la selección resulta vacía por índices desordenados o segmento inexistente, se devuelve
|
||||
/// la cadena vacía.
|
||||
///
|
||||
/// Ejemplos (con `type_name = "alloc::vec::Vec<i32>"`):
|
||||
///
|
||||
/// | Llamada | Resultado |
|
||||
/// |------------------------------|--------------------------|
|
||||
/// | `partial(..., 0, None)` | `"alloc::vec::Vec<i32>"` |
|
||||
/// | `partial(..., 1, None)` | `"vec::Vec<i32>"` |
|
||||
/// | `partial(..., -1, None)` | `"Vec<i32>"` |
|
||||
/// | `partial(..., 0, Some(-2))` | `"alloc::vec"` |
|
||||
/// | `partial(..., -5, None)` | `"alloc::vec::Vec<i32>"` |
|
||||
///
|
||||
/// La porción devuelta vive tanto como `'static` porque `type_name` es `'static` y sólo se
|
||||
/// presta.
|
||||
fn partial(type_name: &'static str, start: isize, end: Option<isize>) -> &'static str {
|
||||
let maxlen = type_name.len();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ static ACTIONS: LazyLock<RwLock<HashMap<ActionKey, ActionsList>>> =
|
|||
|
||||
// **< AÑADIR ACCIONES >****************************************************************************
|
||||
|
||||
// Registra una nueva acción en el sistema.
|
||||
//
|
||||
// Si ya existen acciones con la misma `ActionKey`, la acción se añade a la misma lista. Si no, se
|
||||
// crea una nueva lista.
|
||||
//
|
||||
// Las extensiones llamarán a esta función durante su inicialización para instalar acciones
|
||||
// personalizadas que modifiquen el comportamiento del *core* o de otros componentes.
|
||||
/// Registra una nueva acción en el sistema.
|
||||
///
|
||||
/// Si ya existen acciones con la misma `ActionKey`, la acción se añade a la misma lista. Si no, se
|
||||
/// crea una nueva lista.
|
||||
///
|
||||
/// Las extensiones llamarán a esta función durante su inicialización para instalar acciones
|
||||
/// personalizadas que modifiquen el comportamiento del *core* o de otros componentes.
|
||||
pub(crate) fn add_action(action: ActionBox) {
|
||||
let key = ActionKey::new(
|
||||
action.type_id(),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub struct ActionsList(RwLock<Vec<ActionBox>>);
|
|||
|
||||
impl ActionsList {
|
||||
pub fn new() -> Self {
|
||||
ActionsList::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, action: ActionBox) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
//! API para construir nuevos componentes.
|
||||
|
||||
use crate::html::RoutePath;
|
||||
|
||||
mod definition;
|
||||
pub use definition::{Component, ComponentRender};
|
||||
|
||||
|
|
@ -14,8 +16,8 @@ pub use context::{Context, ContextError, ContextOp, Contextual};
|
|||
/// Alias de función (*callback*) para **determinar si un componente se renderiza o no**.
|
||||
///
|
||||
/// Puede usarse para permitir que una instancia concreta de un tipo de componente dado decida
|
||||
/// dinámicamente durante el proceso de renderizado ([`Component::is_renderable()`]) si se renderiza
|
||||
/// o no.
|
||||
/// dinámicamente durante el proceso de renderizado ([`Component::is_renderable()`]), si se
|
||||
/// renderiza o no.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
|
|
@ -64,8 +66,40 @@ pub use context::{Context, ContextError, ContextOp, Contextual};
|
|||
/// ```
|
||||
pub type FnIsRenderable = fn(cx: &Context) -> bool;
|
||||
|
||||
/// Alias de función (*callback*) para **resolver una URL** según el contexto de renderizado.
|
||||
/// Alias de función (*callback*) para **resolver una ruta URL** según el contexto de renderizado.
|
||||
///
|
||||
/// Se usa para generar enlaces dinámicos en función del contexto (petición, idioma, etc.). Debe
|
||||
/// devolver una referencia válida durante el renderizado.
|
||||
pub type FnPathByContext = fn(cx: &Context) -> &str;
|
||||
/// Se usa para generar enlaces dinámicos en función del contexto (petición, idioma, parámetros,
|
||||
/// etc.). Devuelve una [`RoutePath`], que representa un *path* base junto con una lista opcional de
|
||||
/// parámetros de consulta.
|
||||
///
|
||||
/// El caso más común es construir rutas relativas dependientes del contexto, normalmente usando
|
||||
/// [`Context::route`](crate::core::component::Context::route):
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # let relative_route: FnPathByContext =
|
||||
/// |cx| cx.route("/path/to/page")
|
||||
/// # ;
|
||||
/// ```
|
||||
///
|
||||
/// También es posible usar rutas estáticas sin asignaciones adicionales:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # let external_route: FnPathByContext =
|
||||
/// |_| "https://www.example.com".into()
|
||||
/// # ;
|
||||
/// ```
|
||||
///
|
||||
/// O componer rutas dinámicas en tiempo de ejecución:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # let dynamic_route: FnPathByContext =
|
||||
/// |cx| RoutePath::new("/user").with_param("id", cx.param::<u64>("user_id").unwrap().to_string())
|
||||
/// # ;
|
||||
/// ```
|
||||
///
|
||||
/// Los componentes que acepten un [`FnPathByContext`] invocarán esta función durante el renderizado
|
||||
/// para obtener la URL final que se asignará al atributo HTML correspondiente.
|
||||
pub type FnPathByContext = fn(cx: &Context) -> RoutePath;
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ impl Child {
|
|||
|
||||
// **< Child HELPERS >**************************************************************************
|
||||
|
||||
// Devuelve el [`UniqueId`] del tipo del componente, si existe.
|
||||
/// Devuelve el [`UniqueId`] del tipo del componente, si existe.
|
||||
#[inline]
|
||||
fn type_id(&self) -> Option<UniqueId> {
|
||||
self.0.as_ref().map(|c| c.read().type_id())
|
||||
|
|
@ -156,7 +156,7 @@ impl<C: Component> Typed<C> {
|
|||
|
||||
// **< Typed HELPERS >**************************************************************************
|
||||
|
||||
// Método interno para convertir un componente tipado en un [`Child`].
|
||||
/// Método interno para convertir un componente tipado en un [`Child`].
|
||||
#[inline]
|
||||
fn into(self) -> Child {
|
||||
if let Some(c) = &self.0 {
|
||||
|
|
@ -172,6 +172,7 @@ impl<C: Component> Typed<C> {
|
|||
/// Operaciones para componentes hijo [`Child`] en una lista [`Children`].
|
||||
pub enum ChildOp {
|
||||
Add(Child),
|
||||
AddIfEmpty(Child),
|
||||
AddMany(Vec<Child>),
|
||||
InsertAfterId(&'static str, Child),
|
||||
InsertBeforeId(&'static str, Child),
|
||||
|
|
@ -185,6 +186,7 @@ pub enum ChildOp {
|
|||
/// Operaciones con un componente hijo tipado [`Typed<C>`] en una lista [`Children`].
|
||||
pub enum TypedOp<C: Component> {
|
||||
Add(Typed<C>),
|
||||
AddIfEmpty(Typed<C>),
|
||||
AddMany(Vec<Typed<C>>),
|
||||
InsertAfterId(&'static str, Typed<C>),
|
||||
InsertBeforeId(&'static str, Typed<C>),
|
||||
|
|
@ -206,15 +208,15 @@ pub struct Children(Vec<Child>);
|
|||
impl Children {
|
||||
/// Crea una lista vacía.
|
||||
pub fn new() -> Self {
|
||||
Children::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Crea una lista con un componente hijo inicial.
|
||||
pub fn with(child: Child) -> Self {
|
||||
Children::default().with_child(ChildOp::Add(child))
|
||||
Self::default().with_child(ChildOp::Add(child))
|
||||
}
|
||||
|
||||
// Fusiona varias listas de `Children` en una sola.
|
||||
/// Fusiona varias listas de `Children` en una sola.
|
||||
pub(crate) fn merge(mixes: &[Option<&Children>]) -> Self {
|
||||
let mut opt = Children::default();
|
||||
for m in mixes.iter().flatten() {
|
||||
|
|
@ -230,6 +232,7 @@ impl Children {
|
|||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
||||
match op {
|
||||
ChildOp::Add(any) => self.add(any),
|
||||
ChildOp::AddIfEmpty(any) => self.add_if_empty(any),
|
||||
ChildOp::AddMany(many) => self.add_many(many),
|
||||
ChildOp::InsertAfterId(id, any) => self.insert_after_id(id, any),
|
||||
ChildOp::InsertBeforeId(id, any) => self.insert_before_id(id, any),
|
||||
|
|
@ -246,6 +249,7 @@ impl Children {
|
|||
pub fn with_typed<C: Component>(mut self, op: TypedOp<C>) -> Self {
|
||||
match op {
|
||||
TypedOp::Add(typed) => self.add(typed.into()),
|
||||
TypedOp::AddIfEmpty(typed) => self.add_if_empty(typed.into()),
|
||||
TypedOp::AddMany(many) => self.add_many(many.into_iter().map(Typed::<C>::into)),
|
||||
TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.into()),
|
||||
TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.into()),
|
||||
|
|
@ -266,6 +270,15 @@ impl Children {
|
|||
self
|
||||
}
|
||||
|
||||
/// Añade un componente hijo en la lista sólo si está vacía.
|
||||
#[inline]
|
||||
pub fn add_if_empty(&mut self, child: Child) -> &mut Self {
|
||||
if self.0.is_empty() {
|
||||
self.0.push(child);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
// **< Children GETTERS >***********************************************************************
|
||||
|
||||
/// Devuelve el número de componentes hijo de la lista.
|
||||
|
|
@ -308,7 +321,7 @@ impl Children {
|
|||
|
||||
// **< Children HELPERS >***********************************************************************
|
||||
|
||||
// Añade más de un componente hijo al final de la lista (en el orden recibido).
|
||||
/// Añade más de un componente hijo al final de la lista (en el orden recibido).
|
||||
#[inline]
|
||||
fn add_many<I>(&mut self, iter: I) -> &mut Self
|
||||
where
|
||||
|
|
@ -318,7 +331,7 @@ impl Children {
|
|||
self
|
||||
}
|
||||
|
||||
// Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra.
|
||||
/// Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra.
|
||||
#[inline]
|
||||
fn insert_after_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
||||
let id = Some(id.as_ref());
|
||||
|
|
@ -329,7 +342,7 @@ impl Children {
|
|||
self
|
||||
}
|
||||
|
||||
// Inserta un hijo antes del componente con el `id` dado, o al principio si no se encuentra.
|
||||
/// Inserta un hijo antes del componente con el `id` dado, o al principio si no se encuentra.
|
||||
#[inline]
|
||||
fn insert_before_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
||||
let id = Some(id.as_ref());
|
||||
|
|
@ -340,14 +353,14 @@ impl Children {
|
|||
self
|
||||
}
|
||||
|
||||
// Inserta un hijo al principio de la colección.
|
||||
/// Inserta un hijo al principio de la colección.
|
||||
#[inline]
|
||||
fn prepend(&mut self, child: Child) -> &mut Self {
|
||||
self.0.insert(0, child);
|
||||
self
|
||||
}
|
||||
|
||||
// Inserta más de un componente hijo al principio de la lista (manteniendo el orden recibido).
|
||||
/// Inserta más de un componente hijo al principio de la lista (manteniendo el orden recibido).
|
||||
#[inline]
|
||||
fn prepend_many<I>(&mut self, iter: I) -> &mut Self
|
||||
where
|
||||
|
|
@ -358,7 +371,7 @@ impl Children {
|
|||
self
|
||||
}
|
||||
|
||||
// Elimina el primer hijo con el `id` dado.
|
||||
/// Elimina el primer hijo con el `id` dado.
|
||||
#[inline]
|
||||
fn remove_by_id(&mut self, id: impl AsRef<str>) -> &mut Self {
|
||||
let id = Some(id.as_ref());
|
||||
|
|
@ -368,7 +381,7 @@ impl Children {
|
|||
self
|
||||
}
|
||||
|
||||
// Sustituye el primer hijo con el `id` dado por otro componente.
|
||||
/// Sustituye el primer hijo con el `id` dado por otro componente.
|
||||
#[inline]
|
||||
fn replace_by_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
||||
let id = Some(id.as_ref());
|
||||
|
|
@ -381,7 +394,7 @@ impl Children {
|
|||
self
|
||||
}
|
||||
|
||||
// Elimina todos los componentes hijo de la lista.
|
||||
/// Elimina todos los componentes hijo de la lista.
|
||||
#[inline]
|
||||
fn reset(&mut self) -> &mut Self {
|
||||
self.0.clear();
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
use crate::base::component::Template;
|
||||
use crate::core::component::ChildOp;
|
||||
use crate::core::theme::all::DEFAULT_THEME;
|
||||
use crate::core::theme::{ChildrenInRegions, ThemeRef};
|
||||
use crate::core::theme::{ChildrenInRegions, RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::core::TypeInfo;
|
||||
use crate::html::{html, Markup};
|
||||
use crate::html::{html, Markup, RoutePath};
|
||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||
use crate::locale::{LangId, LangMatch, LanguageIdentifier, DEFAULT_LANGID, FALLBACK_LANGID};
|
||||
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||
use crate::service::HttpRequest;
|
||||
use crate::{builder_fn, join};
|
||||
use crate::{builder_fn, util, CowStr};
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Operaciones para modificar recursos asociados al contexto ([`Context`]) de un documento.
|
||||
/// Operaciones para modificar recursos asociados al [`Context`] de un documento.
|
||||
pub enum ContextOp {
|
||||
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
|
||||
SetFavicon(Option<Favicon>),
|
||||
|
|
@ -50,7 +49,7 @@ pub enum ContextError {
|
|||
///
|
||||
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
|
||||
///
|
||||
/// - Almacenar la **solicitud HTTP** de origen.
|
||||
/// - Almacenar la **petición HTTP** de origen.
|
||||
/// - Seleccionar el **tema** y la **plantilla** de renderizado.
|
||||
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
|
||||
/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`ContextOp`].
|
||||
|
|
@ -66,9 +65,9 @@ pub enum ContextError {
|
|||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_aliner::Aliner;
|
||||
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
||||
/// cx.with_langid(&LangMatch::resolve("es-ES"))
|
||||
/// cx.with_langid(&Locale::resolve("es-ES"))
|
||||
/// .with_theme(&Aliner)
|
||||
/// .with_template(Template::DEFAULT)
|
||||
/// .with_template(&DefaultTemplate::Standard)
|
||||
/// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||
/// .with_assets(ContextOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
||||
/// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
||||
|
|
@ -82,7 +81,7 @@ pub trait Contextual: LangId {
|
|||
#[builder_fn]
|
||||
fn with_langid(self, language: &impl LangId) -> Self;
|
||||
|
||||
/// Almacena la solicitud HTTP de origen en el contexto.
|
||||
/// Almacena la petición HTTP de origen en el contexto.
|
||||
#[builder_fn]
|
||||
fn with_request(self, request: Option<HttpRequest>) -> Self;
|
||||
|
||||
|
|
@ -92,7 +91,7 @@ pub trait Contextual: LangId {
|
|||
|
||||
/// Especifica la plantilla para renderizar el documento.
|
||||
#[builder_fn]
|
||||
fn with_template(self, template_name: &'static str) -> Self;
|
||||
fn with_template(self, template: TemplateRef) -> Self;
|
||||
|
||||
/// Añade o modifica un parámetro dinámico del contexto.
|
||||
#[builder_fn]
|
||||
|
|
@ -102,20 +101,20 @@ pub trait Contextual: LangId {
|
|||
#[builder_fn]
|
||||
fn with_assets(self, op: ContextOp) -> Self;
|
||||
|
||||
/// Opera con [`ChildOp`] en una región (`region_name`) del documento.
|
||||
/// Opera con [`ChildOp`] en una región del documento.
|
||||
#[builder_fn]
|
||||
fn with_child_in(self, region_name: impl AsRef<str>, op: ChildOp) -> Self;
|
||||
fn with_child_in(self, region_ref: RegionRef, op: ChildOp) -> Self;
|
||||
|
||||
// **< Contextual GETTERS >*********************************************************************
|
||||
|
||||
/// Devuelve una referencia a la solicitud HTTP asociada, si existe.
|
||||
/// Devuelve una referencia a la petición HTTP asociada, si existe.
|
||||
fn request(&self) -> Option<&HttpRequest>;
|
||||
|
||||
/// Devuelve el tema que se usará para renderizar el documento.
|
||||
fn theme(&self) -> ThemeRef;
|
||||
|
||||
/// Devuelve el nombre de la plantilla usada para renderizar el documento.
|
||||
fn template(&self) -> &str;
|
||||
/// Devuelve la plantilla configurada para renderizar el documento.
|
||||
fn template(&self) -> TemplateRef;
|
||||
|
||||
/// Recupera un parámetro como [`Option`].
|
||||
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>;
|
||||
|
|
@ -162,7 +161,7 @@ pub trait Contextual: LangId {
|
|||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// Crea un nuevo contexto asociado a una solicitud HTTP:
|
||||
/// Crea un nuevo contexto asociado a una petición HTTP:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -170,7 +169,7 @@ pub trait Contextual: LangId {
|
|||
/// fn new_context(request: HttpRequest) -> Context {
|
||||
/// Context::new(Some(request))
|
||||
/// // Establece el idioma del documento a español.
|
||||
/// .with_langid(&LangMatch::resolve("es-ES"))
|
||||
/// .with_langid(&Locale::resolve("es-ES"))
|
||||
/// // Establece el tema para renderizar.
|
||||
/// .with_theme(&Aliner)
|
||||
/// // Asigna un favicon.
|
||||
|
|
@ -205,10 +204,10 @@ pub trait Contextual: LangId {
|
|||
/// ```
|
||||
#[rustfmt::skip]
|
||||
pub struct Context {
|
||||
request : Option<HttpRequest>, // Solicitud HTTP de origen.
|
||||
langid : &'static LanguageIdentifier, // Identificador de idioma.
|
||||
request : Option<HttpRequest>, // Petición HTTP de origen.
|
||||
locale : RequestLocale, // Idioma asociado a la petición.
|
||||
theme : ThemeRef, // Referencia al tema usado para renderizar.
|
||||
template : &'static str, // Nombre de la plantilla usada para renderizar.
|
||||
template : TemplateRef, // Plantilla usada para renderizar.
|
||||
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
||||
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||
javascripts: Assets<JavaScript>, // Scripts JavaScript.
|
||||
|
|
@ -224,31 +223,18 @@ impl Default for Context {
|
|||
}
|
||||
|
||||
impl Context {
|
||||
/// Crea un nuevo contexto asociado a una solicitud HTTP.
|
||||
/// Crea un nuevo contexto asociado a una petición HTTP.
|
||||
///
|
||||
/// El contexto inicializa el idioma, el tema y la plantilla por defecto, sin favicon ni otros
|
||||
/// recursos cargados.
|
||||
#[rustfmt::skip]
|
||||
pub fn new(request: Option<HttpRequest>) -> Self {
|
||||
// Se intenta DEFAULT_LANGID.
|
||||
let langid = DEFAULT_LANGID
|
||||
// Si es None evalúa la cadena de extracción desde la cabecera HTTP.
|
||||
.or_else(|| {
|
||||
request
|
||||
// Se usa `as_ref()` sobre `Option<HttpRequest>` para no mover el valor.
|
||||
.as_ref()
|
||||
.and_then(|req| req.headers().get("Accept-Language"))
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|language| LangMatch::resolve(language).as_option())
|
||||
})
|
||||
// Si todo falla, se recurre a &FALLBACK_LANGID.
|
||||
.unwrap_or(&FALLBACK_LANGID);
|
||||
|
||||
let locale = RequestLocale::from_request(request.as_ref());
|
||||
Context {
|
||||
request,
|
||||
langid,
|
||||
locale,
|
||||
theme : *DEFAULT_THEME,
|
||||
template : Template::DEFAULT,
|
||||
template : DEFAULT_THEME.default_template(),
|
||||
favicon : None,
|
||||
stylesheets: Assets::<StyleSheet>::new(),
|
||||
javascripts: Assets::<JavaScript>::new(),
|
||||
|
|
@ -286,10 +272,10 @@ impl Context {
|
|||
markup
|
||||
}
|
||||
|
||||
/// Renderiza los componentes de la región `region_name`.
|
||||
pub fn render_region(&mut self, region_name: impl AsRef<str>) -> Markup {
|
||||
/// Renderiza los componentes de una región.
|
||||
pub fn render_region(&mut self, region_ref: RegionRef) -> Markup {
|
||||
self.regions
|
||||
.children_for(self.theme, region_name)
|
||||
.children_for(self.theme, region_ref)
|
||||
.render(self)
|
||||
}
|
||||
|
||||
|
|
@ -376,22 +362,40 @@ impl Context {
|
|||
pub fn remove_param(&mut self, key: &'static str) -> bool {
|
||||
self.params.remove(key).is_some()
|
||||
}
|
||||
|
||||
// **< Context HELPERS >************************************************************************
|
||||
|
||||
/// Construye una ruta aplicada al contexto actual.
|
||||
///
|
||||
/// La ruta resultante se envuelve en un [`RoutePath`], que permite añadir parámetros de
|
||||
/// consulta de forma tipada. Si la política de negociación de idioma actual
|
||||
/// [`LangNegotiation`](crate::global::LangNegotiation) indica que debe propagarse el idioma
|
||||
/// para esta petición, se añade o actualiza el parámetro de *query* `lang=...` con el
|
||||
/// identificador de idioma efectivo del contexto.
|
||||
///
|
||||
/// Esto garantiza que los enlaces generados desde el contexto preservan la preferencia de
|
||||
/// idioma del usuario cuando procede.
|
||||
pub fn route(&self, path: impl Into<CowStr>) -> RoutePath {
|
||||
let mut route = RoutePath::new(path);
|
||||
if self.locale.needs_lang_query() {
|
||||
route.alter_param("lang", self.locale.langid().to_string());
|
||||
}
|
||||
route
|
||||
}
|
||||
}
|
||||
|
||||
/// Permite a [`Context`](crate::core::component::Context) actuar como proveedor de idioma.
|
||||
///
|
||||
/// Devuelve un [`LanguageIdentifier`] siguiendo este orden de prioridad:
|
||||
/// Internamente delega en [`RequestLocale`], que tiene en cuenta la petición HTTP, la configuración
|
||||
/// global de idioma de la aplicación, la cabecera `Accept-Language` y/o el idioma de respaldo.
|
||||
///
|
||||
/// 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
|
||||
/// Todo ello según la negociación indicada en [`global::SETTINGS.app.lang_negotiation`]. Esto
|
||||
/// permite que el [`Context`] se use como fuente de idioma coherente en
|
||||
/// [`L10n::lookup()`](crate::locale::L10n::lookup) o [`L10n::using()`](crate::locale::L10n::using).
|
||||
impl LangId for Context {
|
||||
#[inline]
|
||||
fn langid(&self) -> &'static LanguageIdentifier {
|
||||
self.langid
|
||||
self.locale.langid()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -401,12 +405,14 @@ impl Contextual for Context {
|
|||
#[builder_fn]
|
||||
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
|
||||
self.request = request;
|
||||
// Recalcula el locale según la nueva petición y la política de negociación configurada.
|
||||
self.locale = RequestLocale::from_request(self.request.as_ref());
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
fn with_langid(mut self, language: &impl LangId) -> Self {
|
||||
self.langid = language.langid();
|
||||
self.locale.with_langid(language);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -417,8 +423,8 @@ impl Contextual for Context {
|
|||
}
|
||||
|
||||
#[builder_fn]
|
||||
fn with_template(mut self, template_name: &'static str) -> Self {
|
||||
self.template = template_name;
|
||||
fn with_template(mut self, template: TemplateRef) -> Self {
|
||||
self.template = template;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -474,8 +480,8 @@ impl Contextual for Context {
|
|||
}
|
||||
|
||||
#[builder_fn]
|
||||
fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
|
||||
self.regions.alter_child_in(region_name, op);
|
||||
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
||||
self.regions.alter_child_in(region_ref, op);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -489,7 +495,7 @@ impl Contextual for Context {
|
|||
self.theme
|
||||
}
|
||||
|
||||
fn template(&self) -> &str {
|
||||
fn template(&self) -> TemplateRef {
|
||||
self.template
|
||||
}
|
||||
|
||||
|
|
@ -560,7 +566,7 @@ impl Contextual for Context {
|
|||
prefix
|
||||
};
|
||||
self.id_counter += 1;
|
||||
join!(prefix, "-", self.id_counter.to_string())
|
||||
util::join!(prefix, "-", self.id_counter.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,10 @@ pub fn register_extensions(root_extension: Option<ExtensionRef>) {
|
|||
add_to_enabled(&mut enabled_list, extension);
|
||||
}
|
||||
|
||||
// Añade la página de bienvenida por defecto a la lista de extensiones habilitadas.
|
||||
add_to_enabled(&mut enabled_list, &crate::base::extension::Welcome);
|
||||
// Añade la página de bienvenida predefinida si se habilita en la configuración.
|
||||
if global::SETTINGS.app.welcome {
|
||||
add_to_enabled(&mut enabled_list, &crate::base::extension::Welcome);
|
||||
}
|
||||
|
||||
// Guarda la lista final de extensiones habilitadas.
|
||||
ENABLED_EXTENSIONS.write().append(&mut enabled_list);
|
||||
|
|
|
|||
|
|
@ -4,28 +4,27 @@ use crate::core::AnyInfo;
|
|||
use crate::locale::L10n;
|
||||
use crate::{actions_boxed, service};
|
||||
|
||||
/// Representa una referencia a una extensión.
|
||||
///
|
||||
/// Las extensiones se definen como instancias estáticas globales para poder acceder a ellas desde
|
||||
/// cualquier hilo de la ejecución sin necesidad de sincronización adicional.
|
||||
pub type ExtensionRef = &'static dyn Extension;
|
||||
|
||||
/// Interfaz común que debe implementar cualquier extensión de PageTop.
|
||||
///
|
||||
/// 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.
|
||||
/// Este *trait* es fácil de implementar, basta con declarar una estructura sin campos para la
|
||||
/// extensión y sobrescribir los métodos que sean necesarios. Por ejemplo:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// pub struct Blog;
|
||||
///
|
||||
/// impl Extension for Blog {
|
||||
/// fn name(&self) -> L10n { L10n::n("Blog") }
|
||||
/// fn description(&self) -> L10n { L10n::n("Blog system") }
|
||||
/// fn name(&self) -> L10n {
|
||||
/// L10n::n("Blog")
|
||||
/// }
|
||||
///
|
||||
/// fn description(&self) -> L10n {
|
||||
/// L10n::n("Blog system")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Extension: AnyInfo + Send + Sync {
|
||||
/// Nombre localizado de la extensión legible para el usuario.
|
||||
/// Nombre de la extensión como *texto localizado* legible para el usuario.
|
||||
///
|
||||
/// Predeterminado por el [`short_name()`](AnyInfo::short_name) del tipo asociado a la
|
||||
/// extensión.
|
||||
|
|
@ -33,15 +32,20 @@ pub trait Extension: AnyInfo + Send + Sync {
|
|||
L10n::n(self.short_name())
|
||||
}
|
||||
|
||||
/// Descripción corta localizada de la extensión para paneles, listados, etc.
|
||||
/// Descripción corta de la extensión como *texto localizado* para paneles, listados, etc.
|
||||
///
|
||||
/// Por defecto devuelve un valor vacío (`L10n::default()`).
|
||||
fn description(&self) -> L10n {
|
||||
L10n::default()
|
||||
}
|
||||
|
||||
/// Devuelve una referencia a esta misma extensión cuando se trata de un tema.
|
||||
/// Devuelve una referencia a esta misma extensión cuando actúa como un tema.
|
||||
///
|
||||
/// 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.
|
||||
/// Para ello, la implementación concreta debe ser una extensión que también implemente
|
||||
/// [`Theme`](crate::core::theme::Theme). Por defecto, asume que la extensión no es un tema y
|
||||
/// devuelve `None`.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -61,17 +65,17 @@ pub trait Extension: AnyInfo + Send + Sync {
|
|||
|
||||
/// Otras extensiones que deben habilitarse **antes** de esta.
|
||||
///
|
||||
/// PageTop las resolverá automáticamente respetando el orden durante el arranque de la
|
||||
/// aplicación.
|
||||
/// PageTop resolverá automáticamente estas dependencias respetando el orden durante el arranque
|
||||
/// de la aplicación.
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Devuelve la lista de acciones que la extensión va a registrar.
|
||||
/// Devuelve la lista de acciones que la extensión registra.
|
||||
///
|
||||
/// Estas [acciones](crate::core::action) se despachan por orden de registro o por
|
||||
/// [peso](crate::Weight), permitiendo personalizar el comportamiento de la aplicación en puntos
|
||||
/// específicos.
|
||||
/// [peso](crate::Weight) (ver [`actions_boxed!`](crate::actions_boxed)), permitiendo
|
||||
/// personalizar el comportamiento de la aplicación en puntos específicos.
|
||||
fn actions(&self) -> Vec<ActionBox> {
|
||||
actions_boxed![]
|
||||
}
|
||||
|
|
@ -85,6 +89,8 @@ pub trait Extension: AnyInfo + Send + Sync {
|
|||
/// Configura los servicios web de la extensión, como rutas, *middleware*, acceso a ficheros
|
||||
/// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use pagetop::prelude::*;
|
||||
/// pub struct ExtensionSample;
|
||||
|
|
@ -98,11 +104,15 @@ pub trait Extension: AnyInfo + Send + Sync {
|
|||
#[allow(unused_variables)]
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
|
||||
|
||||
/// Permite crear extensiones para deshabilitar y desinstalar recursos de otras de versiones
|
||||
/// anteriores de la aplicación.
|
||||
/// Permite declarar extensiones destinadas a deshabilitar o desinstalar recursos de otras
|
||||
/// extensiones asociadas a versiones anteriores de la aplicación.
|
||||
///
|
||||
/// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones.
|
||||
/// Actualmente PageTop no utiliza este método, pero se reserva como *placeholder* para futuras
|
||||
/// implementaciones.
|
||||
fn drop_extensions(&self) -> Vec<ExtensionRef> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
/// Representa una referencia a una extensión.
|
||||
pub type ExtensionRef = &'static dyn Extension;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,206 @@
|
|||
//! API para añadir y gestionar nuevos temas.
|
||||
//!
|
||||
//! En PageTop un tema es la *piel* de la aplicación. Es responsable último de los estilos,
|
||||
//! tipografías, espaciados y cualquier otro detalle visual o interactivo (animaciones, scripts de
|
||||
//! interfaz, etc.).
|
||||
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension) y
|
||||
//! también [`Theme`], de modo que [`Extension::theme()`](crate::core::extension::Extension::theme)
|
||||
//! permita identificar y registrar los temas disponibles.
|
||||
//!
|
||||
//! Un tema determina el aspecto final de un documento HTML sin alterar la lógica interna de los
|
||||
//! componentes ni la estructura del documento, que queda definida por la plantilla
|
||||
//! ([`Template`](crate::base::component::Template)) utilizada por cada página.
|
||||
//! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos
|
||||
//! interactivos. Para ello utiliza plantillas ([`Template`]) que describen cómo maquetar el cuerpo
|
||||
//! del documento a partir de varias regiones ([`Region`]). Cada región es un contenedor lógico
|
||||
//! identificado por un nombre, cuyo contenido se obtiene del [`Context`] de la página.
|
||||
//!
|
||||
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension), por
|
||||
//! lo que se instancian, declaran dependencias y se inician igual que cualquier otra extensión.
|
||||
//! También deben implementar [`Theme`] y sobrescribir el método
|
||||
//! [`Extension::theme()`](crate::core::extension::Extension::theme) para que PageTop pueda
|
||||
//! registrarlos como temas.
|
||||
//! Una página ([`Page`](crate::response::page::Page)) representa un documento HTML completo.
|
||||
//! Implementa [`Contextual`](crate::core::component::Contextual) para gestionar su propio
|
||||
//! [`Context`], donde mantiene el tema activo, la plantilla seleccionada y los componentes
|
||||
//! asociados a cada región.
|
||||
//!
|
||||
//! De este modo, temas y extensiones colaboran sobre una estructura común: las aplicaciones
|
||||
//! registran componentes en el [`Context`], las plantillas organizan las regiones y las páginas
|
||||
//! generan el documento HTML resultante.
|
||||
//!
|
||||
//! Los temas pueden definir sus propias implementaciones de [`Template`] y [`Region`] (por ejemplo,
|
||||
//! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas.
|
||||
|
||||
use crate::core::component::Context;
|
||||
use crate::html::{html, Markup};
|
||||
use crate::locale::L10n;
|
||||
use crate::{util, AutoDefault};
|
||||
|
||||
// **< Region >*************************************************************************************
|
||||
|
||||
/// Interfaz común para las regiones lógicas de un documento.
|
||||
///
|
||||
/// Una `Region` representa un contenedor lógico identificado por un nombre de región. Su contenido
|
||||
/// se obtiene del [`Context`], donde los componentes suelen registrarse usando implementaciones de
|
||||
/// métodos como [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in).
|
||||
///
|
||||
/// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas
|
||||
/// implementaciones de [`Region`] que devuelvan el mismo nombre compartirán el mismo conjunto de
|
||||
/// componentes registrados en el [`Context`], aunque cada región puede renderizar ese contenido de
|
||||
/// forma diferente. Por ejemplo, [`DefaultRegion::Header`] y `BootsierRegion::Header` mostrarían
|
||||
/// los mismos componentes si ambas devuelven el nombre `"header"`, pero podrían maquetarse de
|
||||
/// manera distinta.
|
||||
///
|
||||
/// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla
|
||||
/// ([`Template`]) al renderizar la página ([`Page`](crate::response::page::Page)).
|
||||
pub trait Region {
|
||||
/// Devuelve el nombre de la región.
|
||||
///
|
||||
/// Este nombre es el identificador lógico de la región y se usa como clave en el [`Context`]
|
||||
/// para recuperar y renderizar el contenido registrado bajo ese nombre. Cualquier
|
||||
/// implementación de [`Region`] que devuelva el mismo nombre compartirá el mismo conjunto de
|
||||
/// componentes.
|
||||
///
|
||||
/// En la implementación predeterminada de [`Self::render()`] también se utiliza para construir
|
||||
/// las clases del contenedor de la región (`"region region-<name>"`).
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Devuelve un *texto localizado* como etiqueta de accesibilidad asociada a la región.
|
||||
///
|
||||
/// En la implementación predeterminada de [`Self::render()`], este valor se usa como
|
||||
/// `aria-label` del contenedor de la región.
|
||||
fn label(&self) -> L10n;
|
||||
|
||||
/// Renderiza el contenedor de la región.
|
||||
///
|
||||
/// Por defecto, recupera del [`Context`] el contenido de la región y, si no está vacío, lo
|
||||
/// envuelve en un `<div>` con clases `"region region-<name>"` y un `aria-label` basado en el
|
||||
/// *texto localizado* de la etiqueta asociada a la región:
|
||||
///
|
||||
/// ```html
|
||||
/// <div class="region region-<name>" role="region" aria-label="<label>">
|
||||
/// <!-- Componentes de la región "name" -->
|
||||
/// </div>
|
||||
/// ```
|
||||
///
|
||||
/// Se puede sobrescribir este método para modificar la estructura del contenedor, las clases
|
||||
/// utilizadas o la semántica del marcado generado para cada región.
|
||||
fn render(&'static self, cx: &mut Context) -> Markup
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
html! {
|
||||
@let region = cx.render_region(self);
|
||||
@if !region.is_empty() {
|
||||
div
|
||||
class=(util::join!("region region-", self.name()))
|
||||
role="region"
|
||||
aria-label=[self.label().lookup(cx)]
|
||||
{
|
||||
(region)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Referencia estática a una región.
|
||||
pub type RegionRef = &'static dyn Region;
|
||||
|
||||
// **< DefaultRegion >******************************************************************************
|
||||
|
||||
/// Regiones básicas que PageTop proporciona por defecto.
|
||||
///
|
||||
/// Estas regiones comparten sus nombres (`"header"`, `"content"`, `"footer"`) con cualquier región
|
||||
/// equivalente definida por otros temas, por lo que comparten también el contenido registrado bajo
|
||||
/// esos nombres.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum DefaultRegion {
|
||||
/// Región estándar para la **cabecera** del documento, de nombre `"header"`.
|
||||
///
|
||||
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
|
||||
Header,
|
||||
|
||||
/// Región principal de **contenido**, de nombre `"content"`.
|
||||
///
|
||||
/// Es la región donde se renderiza el contenido principal del documento. En general será la
|
||||
/// región mínima imprescindible para que una página tenga sentido.
|
||||
#[default]
|
||||
Content,
|
||||
|
||||
/// Región estándar para el **pie de página**, de nombre `"footer"`.
|
||||
///
|
||||
/// Suele contener información legal, enlaces secundarios, créditos, etc.
|
||||
Footer,
|
||||
}
|
||||
|
||||
impl Region for DefaultRegion {
|
||||
#[inline]
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Header => "header",
|
||||
Self::Content => "content",
|
||||
Self::Footer => "footer",
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn label(&self) -> L10n {
|
||||
match self {
|
||||
Self::Header => L10n::l("region-header"),
|
||||
Self::Content => L10n::l("region-content"),
|
||||
Self::Footer => L10n::l("region-footer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Template >***********************************************************************************
|
||||
|
||||
/// Interfaz común para definir plantillas de contenido.
|
||||
///
|
||||
/// Una `Template` puede proporcionar una o más variantes para decidir la composición del `<body>`
|
||||
/// de una página ([`Page`](crate::response::page::Page)). El tema utiliza esta información para
|
||||
/// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden.
|
||||
pub trait Template {
|
||||
/// Renderiza el contenido de la plantilla.
|
||||
///
|
||||
/// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden:
|
||||
/// [`DefaultRegion::Header`], [`DefaultRegion::Content`] y [`DefaultRegion::Footer`].
|
||||
///
|
||||
/// Se puede sobrescribir este método para:
|
||||
///
|
||||
/// - Cambiar el conjunto de regiones que se renderizan según variantes de la plantilla.
|
||||
/// - Alterar el orden de dichas regiones.
|
||||
/// - Envolver las regiones en contenedores adicionales.
|
||||
/// - Implementar distribuciones específicas (por ejemplo, con barras laterales).
|
||||
///
|
||||
/// Este método se invoca normalmente desde [`Theme::render_page_body()`] para generar el
|
||||
/// contenido del `<body>` de una página según la plantilla devuelta por el contexto de la
|
||||
/// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())).
|
||||
fn render(&'static self, cx: &mut Context) -> Markup {
|
||||
html! {
|
||||
(DefaultRegion::Header.render(cx))
|
||||
(DefaultRegion::Content.render(cx))
|
||||
(DefaultRegion::Footer.render(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Referencia estática a una plantilla.
|
||||
pub type TemplateRef = &'static dyn Template;
|
||||
|
||||
// **< DefaultTemplate >****************************************************************************
|
||||
|
||||
/// Plantillas que PageTop proporciona por defecto.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum DefaultTemplate {
|
||||
/// Plantilla predeterminada.
|
||||
///
|
||||
/// Utiliza la implementación por defecto de [`Template::render()`] y se emplea cuando no se
|
||||
/// selecciona ninguna otra plantilla explícitamente.
|
||||
#[default]
|
||||
Standard,
|
||||
|
||||
/// Plantilla de error.
|
||||
///
|
||||
/// Se utiliza para páginas de error u otros estados excepcionales. Por defecto utiliza la misma
|
||||
/// implementación de [`Template::render()`] que [`Self::Standard`].
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Template for DefaultTemplate {}
|
||||
|
||||
// **< Definitions >********************************************************************************
|
||||
|
||||
mod definition;
|
||||
pub use definition::{Theme, ThemeRef};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub static DEFAULT_THEME: LazyLock<ThemeRef> =
|
|||
|
||||
// **< TEMA POR NOMBRE >****************************************************************************
|
||||
|
||||
// Devuelve el tema identificado por su [`short_name()`](AnyInfo::short_name).
|
||||
/// Devuelve el tema identificado por su [`short_name()`](AnyInfo::short_name).
|
||||
pub fn theme_by_short_name(short_name: &'static str) -> Option<ThemeRef> {
|
||||
let short_name = short_name.to_lowercase();
|
||||
match THEMES
|
||||
|
|
|
|||
|
|
@ -1,30 +1,28 @@
|
|||
use crate::base::component::Template;
|
||||
use crate::core::component::{ComponentRender, ContextOp, Contextual};
|
||||
use crate::base::component::{Html, Intro, IntroOpening};
|
||||
use crate::core::component::{Child, ChildOp, Component, Contextual};
|
||||
use crate::core::extension::Extension;
|
||||
use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef};
|
||||
use crate::global;
|
||||
use crate::html::{html, Markup, StyleSheet};
|
||||
use crate::html::{html, Markup};
|
||||
use crate::locale::L10n;
|
||||
use crate::response::page::Page;
|
||||
|
||||
/// Referencia estática a un tema.
|
||||
///
|
||||
/// Los temas son también extensiones. Por tanto, deben declararse como **instancias estáticas** que
|
||||
/// implementen [`Theme`] y, a su vez, [`Extension`]. Estas instancias se exponen usando
|
||||
/// [`Extension::theme()`](crate::core::extension::Extension::theme).
|
||||
pub type ThemeRef = &'static dyn Theme;
|
||||
use crate::service::http::StatusCode;
|
||||
|
||||
/// Interfaz común que debe implementar cualquier tema de PageTop.
|
||||
///
|
||||
/// Un tema es una [`Extension`](crate::core::extension::Extension) que define el aspecto general de
|
||||
/// las páginas: cómo se renderiza el `<head>`, cómo se presenta el `<body>` mediante plantillas
|
||||
/// ([`Template`]) y qué contenido mostrar en las páginas de error.
|
||||
/// las páginas: cómo se renderiza el `<head>`, cómo se presenta el `<body>` usando plantillas
|
||||
/// ([`Template`](crate::core::theme::Template)) que maquetan regiones
|
||||
/// ([`Region`](crate::core::theme::Region)) y qué contenido mostrar en las páginas de error. El
|
||||
/// contenido de cada región depende del [`Context`](crate::core::component::Context) y de su nombre
|
||||
/// lógico.
|
||||
///
|
||||
/// Todos los métodos de este *trait* tienen una implementación por defecto, por lo que pueden
|
||||
/// sobrescribirse selectivamente para crear nuevos temas con comportamientos distintos a los
|
||||
/// predeterminados.
|
||||
///
|
||||
/// El único método **obligatorio** de `Extension` para un tema es [`theme()`](Extension::theme),
|
||||
/// que debe devolver una referencia estática al propio tema:
|
||||
/// que debe devolver una referencia al propio tema:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -47,32 +45,55 @@ pub type ThemeRef = &'static dyn Theme;
|
|||
/// impl Theme for MyTheme {}
|
||||
/// ```
|
||||
pub trait Theme: Extension + Send + Sync {
|
||||
/// Devuelve la plantilla ([`Template`](crate::core::theme::Template)) que el propio tema
|
||||
/// propone como predeterminada.
|
||||
///
|
||||
/// Se utiliza al inicializar un [`Context`](crate::core::component::Context) o una página
|
||||
/// ([`Page`](crate::response::page::Page)) por si no se elige ninguna otra plantilla con
|
||||
/// [`Contextual::with_template()`](crate::core::component::Contextual::with_template).
|
||||
///
|
||||
/// La implementación por defecto devuelve la plantilla estándar ([`DefaultTemplate::Standard`])
|
||||
/// con una estructura básica para la página. Los temas pueden sobrescribir este método para
|
||||
/// seleccionar otra plantilla predeterminada o una plantilla propia.
|
||||
#[inline]
|
||||
fn default_template(&self) -> TemplateRef {
|
||||
&DefaultTemplate::Standard
|
||||
}
|
||||
|
||||
/// Acciones específicas del tema antes de renderizar el `<body>` de la página.
|
||||
///
|
||||
/// Se invoca antes de que se procese la plantilla ([`Template`]) asociada a la página
|
||||
/// ([`Page::template()`](crate::response::page::Page::template)). Es un buen lugar para
|
||||
/// inicializar o ajustar recursos en función del contexto de la página, por ejemplo:
|
||||
/// Es un buen lugar para inicializar o ajustar recursos en función del contexto de la página,
|
||||
/// por ejemplo:
|
||||
///
|
||||
/// - Añadir metadatos o propiedades a la página.
|
||||
/// - Añadir metadatos o propiedades a la cabecera de la página.
|
||||
/// - Preparar atributos compartidos.
|
||||
/// - Registrar *assets* condicionales en el contexto.
|
||||
///
|
||||
/// La implementación por defecto no realiza ninguna acción.
|
||||
#[allow(unused_variables)]
|
||||
fn before_render_page_body(&self, page: &mut Page) {}
|
||||
|
||||
/// Renderiza el contenido del `<body>` de la página.
|
||||
///
|
||||
/// Por defecto, delega en la plantilla ([`Template`]) asociada a la página
|
||||
/// ([`Page::template()`](crate::response::page::Page::template)). La plantilla se encarga de
|
||||
/// procesar las regiones y renderizar los componentes registrados en el contexto.
|
||||
/// La implementación predeterminada delega en la plantilla asociada a la página, obtenida desde
|
||||
/// su [`Context`](crate::core::component::Context), y llama a
|
||||
/// [`Template::render()`](crate::core::theme::Template::render) para componer el `<body>` a
|
||||
/// partir de las regiones.
|
||||
///
|
||||
/// Con la configuración por defecto, la plantilla estándar utiliza las regiones
|
||||
/// [`DefaultRegion::Header`](crate::core::theme::DefaultRegion::Header),
|
||||
/// [`DefaultRegion::Content`](crate::core::theme::DefaultRegion::Content) y
|
||||
/// [`DefaultRegion::Footer`](crate::core::theme::DefaultRegion::Footer) en ese orden.
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para:
|
||||
///
|
||||
/// - Forzar una plantilla concreta en determinadas páginas.
|
||||
/// - Envolver el contenido en marcadores adicionales.
|
||||
/// - Consultar la plantilla de la página y variar la composición según su nombre.
|
||||
/// - Envolver el contenido en contenedores adicionales.
|
||||
/// - Implementar lógicas de composición alternativas.
|
||||
#[inline]
|
||||
fn render_page_body(&self, page: &mut Page) -> Markup {
|
||||
Template::named(page.template()).render(page.context())
|
||||
page.template().render(page.context())
|
||||
}
|
||||
|
||||
/// Acciones específicas del tema después de renderizar el `<body>` de la página.
|
||||
|
|
@ -84,31 +105,9 @@ pub trait Theme: Extension + Send + Sync {
|
|||
/// - Aplicar ajustes finales al estado de la página antes de producir el `<head>` o la
|
||||
/// respuesta final.
|
||||
///
|
||||
/// La implementación por defecto añade una serie de hojas de estilo básicas (`normalize.css`,
|
||||
/// `root.css`, `basic.css`) cuando el parámetro `include_basic_assets` de la página está
|
||||
/// activado.
|
||||
/// La implementación por defecto no realiza ninguna acción.
|
||||
#[allow(unused_variables)]
|
||||
fn after_render_page_body(&self, page: &mut Page) {
|
||||
if page.param_or("include_basic_assets", false) {
|
||||
let pkg_version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/root.css")
|
||||
.with_version(pkg_version)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/basic.css")
|
||||
.with_version(pkg_version)
|
||||
.with_weight(-99),
|
||||
));
|
||||
}
|
||||
}
|
||||
fn after_render_page_body(&self, page: &mut Page) {}
|
||||
|
||||
/// Renderiza el contenido del `<head>` de la página.
|
||||
///
|
||||
|
|
@ -125,11 +124,10 @@ pub trait Theme: Extension + Send + Sync {
|
|||
/// - La etiqueta `viewport` básica para diseño adaptable.
|
||||
/// - Los metadatos (`name`/`content`) y propiedades (`property`/`content`) declarados en la
|
||||
/// página.
|
||||
/// - Todos los *assets* registrados en el contexto de la página.
|
||||
/// - Los *assets* registrados en el contexto de la página.
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo,
|
||||
/// *favicons* personalizados, manifest, etiquetas de analítica, etc.).
|
||||
#[inline]
|
||||
fn render_page_head(&self, page: &mut Page) -> Markup {
|
||||
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
|
||||
html! {
|
||||
|
|
@ -159,19 +157,78 @@ pub trait Theme: Extension + Send + Sync {
|
|||
}
|
||||
}
|
||||
|
||||
/// Contenido predeterminado para la página de error "*403 - Forbidden*".
|
||||
/// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado).
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la
|
||||
/// página de error, manteniendo o no el mensaje de los textos localizados.
|
||||
fn error403(&self, page: &mut Page) -> Markup {
|
||||
html! { div { h1 { (L10n::l("error403_notice").using(page)) } } }
|
||||
/// página de error.
|
||||
fn error_403(&self, page: &mut Page) {
|
||||
page.alter_title(L10n::l("error403_title"))
|
||||
.alter_template(&DefaultTemplate::Error)
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Content,
|
||||
ChildOp::Prepend(Child::with(Html::with(move |cx| {
|
||||
html! {
|
||||
div {
|
||||
h1 { (L10n::l("error403_alert").using(cx)) }
|
||||
p { (L10n::l("error403_help").using(cx)) }
|
||||
}
|
||||
}
|
||||
}))),
|
||||
);
|
||||
}
|
||||
|
||||
/// Contenido predeterminado para la página de error "*404 - Not Found*".
|
||||
/// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado).
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la
|
||||
/// página de error, manteniendo o no el mensaje de los textos localizados.
|
||||
fn error404(&self, page: &mut Page) -> Markup {
|
||||
html! { div { h1 { (L10n::l("error404_notice").using(page)) } } }
|
||||
/// página de error.
|
||||
fn error_404(&self, page: &mut Page) {
|
||||
page.alter_title(L10n::l("error404_title"))
|
||||
.alter_template(&DefaultTemplate::Error)
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Content,
|
||||
ChildOp::Prepend(Child::with(Html::with(move |cx| {
|
||||
html! {
|
||||
div {
|
||||
h1 { (L10n::l("error404_alert").using(cx)) }
|
||||
p { (L10n::l("error404_help").using(cx)) }
|
||||
}
|
||||
}
|
||||
}))),
|
||||
);
|
||||
}
|
||||
|
||||
/// Permite al tema preparar y componer una página de error fatal.
|
||||
///
|
||||
/// Por defecto, asigna el título al documento (`title`) y muestra un componente [`Intro`] con
|
||||
/// el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y `help`) como
|
||||
/// descripción del error.
|
||||
///
|
||||
/// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni
|
||||
/// [`Self::error_404()`], que definen su propio contenido específico.
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la
|
||||
/// página de error.
|
||||
fn error_fatal(&self, page: &mut Page, code: StatusCode, title: L10n, alert: L10n, help: L10n) {
|
||||
page.alter_title(title)
|
||||
.alter_template(&DefaultTemplate::Error)
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Content,
|
||||
ChildOp::Prepend(Child::with(
|
||||
Intro::new()
|
||||
.with_title(L10n::l("error_code").with_arg("code", code.to_string()))
|
||||
.with_slogan(L10n::n(code.to_string()))
|
||||
.with_button(None)
|
||||
.with_opening(IntroOpening::Custom)
|
||||
.add_child(Html::with(move |cx| {
|
||||
html! {
|
||||
h1 { (alert.using(cx)) }
|
||||
p { (help.using(cx)) }
|
||||
}
|
||||
})),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Referencia estática a un tema.
|
||||
pub type ThemeRef = &'static dyn Theme;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use crate::base::component::Region;
|
||||
use crate::core::component::{Child, ChildOp, Children};
|
||||
use crate::core::theme::ThemeRef;
|
||||
use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef};
|
||||
use crate::{builder_fn, AutoDefault, UniqueId};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
|
@ -21,24 +20,23 @@ static COMMON_REGIONS: LazyLock<RwLock<ChildrenInRegions>> =
|
|||
pub(crate) struct ChildrenInRegions(HashMap<String, Children>);
|
||||
|
||||
impl ChildrenInRegions {
|
||||
pub fn with(region_name: impl AsRef<str>, child: Child) -> Self {
|
||||
Self::default().with_child_in(region_name, ChildOp::Add(child))
|
||||
pub fn with(region_ref: RegionRef, child: Child) -> Self {
|
||||
Self::default().with_child_in(region_ref, ChildOp::Add(child))
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
|
||||
let name = region_name.as_ref();
|
||||
if let Some(region) = self.0.get_mut(name) {
|
||||
pub fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
||||
if let Some(region) = self.0.get_mut(region_ref.name()) {
|
||||
region.alter_child(op);
|
||||
} else {
|
||||
self.0
|
||||
.insert(name.to_owned(), Children::new().with_child(op));
|
||||
.insert(region_ref.name().to_owned(), Children::new().with_child(op));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn children_for(&self, theme_ref: ThemeRef, region_name: impl AsRef<str>) -> Children {
|
||||
let name = region_name.as_ref();
|
||||
pub fn children_for(&self, theme_ref: ThemeRef, region_ref: RegionRef) -> Children {
|
||||
let name = region_ref.name();
|
||||
let common = COMMON_REGIONS.read();
|
||||
let themed = THEME_REGIONS.read();
|
||||
|
||||
|
|
@ -50,20 +48,36 @@ impl ChildrenInRegions {
|
|||
}
|
||||
}
|
||||
|
||||
/// Permite añadir componentes a regiones globales o específicas de un tema.
|
||||
/// Añade componentes a regiones globales o específicas de un tema.
|
||||
///
|
||||
/// Según la variante, se pueden añadir componentes ([`add()`](Self::add)) que permanecerán
|
||||
/// disponibles durante toda la ejecución.
|
||||
///
|
||||
/// Estos componentes se renderizarán automáticamente al procesar los documentos HTML que incluyen
|
||||
/// estas regiones, como las páginas de contenido ([`Page`](crate::response::page::Page)).
|
||||
/// Cada variante indica la región en la que se añade el componente usando [`Self::add()`]. Los
|
||||
/// componentes añadidos se mantienen durante toda la ejecución y se inyectan automáticamente al
|
||||
/// renderizar los documentos HTML que utilizan esas regiones, como las páginas de contenido
|
||||
/// ([`Page`](crate::response::page::Page)).
|
||||
pub enum InRegion {
|
||||
/// Región de contenido por defecto.
|
||||
Default,
|
||||
/// Región identificada por el nombre proporcionado.
|
||||
Named(&'static str),
|
||||
/// Región identificada por su nombre para un tema concreto.
|
||||
OfTheme(&'static str, ThemeRef),
|
||||
/// Región principal de **contenido** por defecto.
|
||||
///
|
||||
/// Añade el componente a la región lógica de contenido principal de la aplicación. Por
|
||||
/// convención, esta región corresponde a [`DefaultRegion::Content`], cuyo nombre es
|
||||
/// `"content"`. Cualquier tema que renderice esa misma región de contenido, ya sea usando
|
||||
/// directamente [`DefaultRegion::Content`] o cualquier otra implementación de
|
||||
/// [`Region`](crate::core::theme::Region) que devuelva ese mismo nombre, mostrará los
|
||||
/// componentes registrados aquí, aunque lo harán según su propio método de renderizado
|
||||
/// ([`Region::render()`](crate::core::theme::Region::render)).
|
||||
Content,
|
||||
/// Región global compartida por todos los temas.
|
||||
///
|
||||
/// Los componentes añadidos aquí se asocian al nombre de la región indicado por [`RegionRef`],
|
||||
/// es decir, al valor devuelto por [`Region::name()`](crate::core::theme::Region::name) para
|
||||
/// esa región. Se mostrarán en cualquier tema cuya plantilla renderice una región que devuelva
|
||||
/// ese mismo nombre.
|
||||
Global(RegionRef),
|
||||
/// Región asociada a un tema concreto.
|
||||
///
|
||||
/// Los componentes sólo se renderizarán cuando el documento se procese con el tema indicado y
|
||||
/// se utilice la región referenciada. Resulta útil para añadir contenido específico en un tema
|
||||
/// sin afectar a otros.
|
||||
ForTheme(ThemeRef, RegionRef),
|
||||
}
|
||||
|
||||
impl InRegion {
|
||||
|
|
@ -73,28 +87,33 @@ impl InRegion {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// // Banner global, en la región por defecto de cualquier página.
|
||||
/// InRegion::Default.add(Child::with(Html::with(|_|
|
||||
/// html! { ("🎉 ¡Bienvenido!") }
|
||||
/// )));
|
||||
/// // Banner global en la región por defecto.
|
||||
/// InRegion::Content.add(Child::with(Html::with(|_| {
|
||||
/// html! { "🎉 ¡Bienvenido!" }
|
||||
/// })));
|
||||
///
|
||||
/// // Texto en la región "sidebar".
|
||||
/// InRegion::Named("sidebar").add(Child::with(Html::with(|_|
|
||||
/// html! { ("Publicidad") }
|
||||
/// )));
|
||||
/// // Texto en la cabecera.
|
||||
/// InRegion::Global(&DefaultRegion::Header).add(Child::with(Html::with(|_| {
|
||||
/// html! { "Publicidad" }
|
||||
/// })));
|
||||
///
|
||||
/// // Contenido sólo para la región del pie de página en un tema concreto.
|
||||
/// InRegion::ForTheme(&theme::Basic, &DefaultRegion::Footer).add(Child::with(Html::with(|_| {
|
||||
/// html! { "Aviso legal" }
|
||||
/// })));
|
||||
/// ```
|
||||
pub fn add(&self, child: Child) -> &Self {
|
||||
match self {
|
||||
InRegion::Default => Self::add_to_common(Region::DEFAULT, child),
|
||||
InRegion::Named(region_name) => Self::add_to_common(region_name, child),
|
||||
InRegion::OfTheme(region_name, theme_ref) => {
|
||||
InRegion::Content => Self::add_to_common(&DefaultRegion::Content, child),
|
||||
InRegion::Global(region_ref) => Self::add_to_common(*region_ref, child),
|
||||
InRegion::ForTheme(theme_ref, region_ref) => {
|
||||
let mut regions = THEME_REGIONS.write();
|
||||
if let Some(r) = regions.get_mut(&theme_ref.type_id()) {
|
||||
r.alter_child_in(region_name, ChildOp::Add(child));
|
||||
r.alter_child_in(*region_ref, ChildOp::Add(child));
|
||||
} else {
|
||||
regions.insert(
|
||||
theme_ref.type_id(),
|
||||
ChildrenInRegions::with(region_name, child),
|
||||
ChildrenInRegions::with(*region_ref, child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -103,9 +122,9 @@ impl InRegion {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
fn add_to_common(region_name: &str, child: Child) {
|
||||
fn add_to_common(region_ref: RegionRef, child: Child) {
|
||||
COMMON_REGIONS
|
||||
.write()
|
||||
.alter_child_in(region_name, ChildOp::Add(child));
|
||||
.alter_child_in(region_ref, ChildOp::Add(child));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,28 @@ use crate::include_config;
|
|||
|
||||
use serde::Deserialize;
|
||||
|
||||
mod lang_negotiation;
|
||||
pub use lang_negotiation::LangNegotiation;
|
||||
|
||||
mod startup_banner;
|
||||
pub use startup_banner::StartupBanner;
|
||||
|
||||
mod log_rolling;
|
||||
pub use log_rolling::LogRolling;
|
||||
|
||||
mod log_format;
|
||||
pub use log_format::LogFormat;
|
||||
|
||||
// **< SETTINGS >***********************************************************************************
|
||||
|
||||
include_config!(SETTINGS: Settings => [
|
||||
// [app]
|
||||
"app.name" => "PageTop App",
|
||||
"app.description" => "Developed with the amazing PageTop framework.",
|
||||
"app.theme" => "Basic",
|
||||
"app.language" => "",
|
||||
"app.lang_negotiation" => "Full",
|
||||
"app.startup_banner" => "Slant",
|
||||
"app.welcome" => true,
|
||||
|
||||
// [dev]
|
||||
"dev.pagetop_static_dir" => "",
|
||||
|
|
@ -29,6 +44,8 @@ include_config!(SETTINGS: Settings => [
|
|||
"server.session_lifetime" => 604_800,
|
||||
]);
|
||||
|
||||
// **< Settings >***********************************************************************************
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Tipos para las secciones globales [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log) y
|
||||
/// [`[server]`](Server) de [`SETTINGS`].
|
||||
|
|
@ -48,24 +65,38 @@ pub struct App {
|
|||
pub description: String,
|
||||
/// Tema predeterminado.
|
||||
pub theme: String,
|
||||
/// Idioma por defecto para la aplicación.
|
||||
/// Idioma predeterminado de la aplicación (p. ej., *"es-ES"* o *"en-US"*).
|
||||
///
|
||||
/// Si no está definido o no es válido, [`LangId`](crate::locale::LangId) determinará el idioma
|
||||
/// efectivo para el renderizado en este orden: primero intentará usar el establecido mediante
|
||||
/// [`Contextual::with_langid()`](crate::core::component::Contextual::with_langid); si no se ha
|
||||
/// definido explícitamente, probará el indicado en la cabecera `Accept-Language` del navegador;
|
||||
/// y, si ninguno aplica, se empleará el idioma de respaldo ("en-US").
|
||||
pub language: String,
|
||||
/// Cuando tiene un valor validado por [`Locale`](crate::locale::Locale), se usa como candidato
|
||||
/// para resolver el idioma efectivo de cada petición según la estrategia definida en
|
||||
/// [`lang_negotiation`](Self::lang_negotiation) y aplicada por
|
||||
/// [`RequestLocale`](crate::locale::RequestLocale).
|
||||
///
|
||||
/// Si es `None` o no contiene un valor válido, la negociación del idioma pasa a depender de
|
||||
/// otras fuentes como la cabecera `Accept-Language` de la petición o, en último término, del
|
||||
/// idioma de respaldo configurado en el sistema.
|
||||
pub language: Option<String>,
|
||||
/// Estrategia para resolver el idioma usado en la petición: *"Full"*, *"NoQuery"* o
|
||||
/// *"ConfigOnly"*.
|
||||
///
|
||||
/// Define las fuentes que intervienen en la negociación del idioma para el renderizado de los
|
||||
/// documentos y la generación de URLs. Ver [`LangNegotiation`] para los modos disponibles.
|
||||
pub lang_negotiation: LangNegotiation,
|
||||
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
|
||||
/// *"Starwars"*.
|
||||
pub startup_banner: String,
|
||||
pub startup_banner: StartupBanner,
|
||||
/// Activa la página de bienvenida de PageTop.
|
||||
///
|
||||
/// Si está activada, se instala la extensión [`Welcome`](crate::base::extension::Welcome), que
|
||||
/// ofrece una página de bienvenida predefinida en `"/"`.
|
||||
pub welcome: bool,
|
||||
/// Modo de ejecución, dado por la variable de entorno `PAGETOP_RUN_MODE`, o *"default"* si no
|
||||
/// está definido.
|
||||
pub run_mode: String,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// Directorio desde el que servir los archivos estáticos de PageTop.
|
||||
///
|
||||
|
|
@ -81,21 +112,21 @@ pub struct Dev {
|
|||
#[derive(Debug, Deserialize)]
|
||||
/// Sección `[log]` de la configuración. Forma parte de [`Settings`].
|
||||
pub struct Log {
|
||||
/// Gestión de trazas y registro de eventos activado (`true`) o desactivado (`false`).
|
||||
/// Gestión de trazas y registro de eventos activada (*true*) o desactivada (*false*).
|
||||
pub enabled: bool,
|
||||
/// Opciones, o combinación de opciones separadas por comas, para filtrar las trazas: *"Error"*,
|
||||
/// *"Warn"*, *"Info"*, *"Debug"* o *"Trace"*.
|
||||
/// Ejemplo: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
|
||||
/// Ejemplo: *"Error,actix_server::builder=Info,tracing_actix_web=Debug"*.
|
||||
pub tracing: String,
|
||||
/// Muestra los mensajes de traza en el terminal (*"Stdout"*) o las registra en archivos con
|
||||
/// Muestra los mensajes de traza en el terminal (*"Stdout"*) o los vuelca en archivos con
|
||||
/// rotación: *"Daily"*, *"Hourly"*, *"Minutely"* o *"Endless"*.
|
||||
pub rolling: String,
|
||||
/// Directorio para los archivos de traza (si `rolling` ≠ *"Stdout"*).
|
||||
pub rolling: LogRolling,
|
||||
/// Directorio para los archivos de traza (si [`rolling`](Self::rolling) ≠ *"Stdout"*).
|
||||
pub path: String,
|
||||
/// Prefijo para los archivos de traza (si `rolling` ≠ *"Stdout"*).
|
||||
/// Prefijo para los archivos de traza (si [`rolling`](Self::rolling) ≠ *"Stdout"*).
|
||||
pub prefix: String,
|
||||
/// Formato de salida de las trazas. Opciones: *"Full"*, *"Compact"*, *"Pretty"* o *"Json"*.
|
||||
pub format: String,
|
||||
pub format: LogFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
|||
56
src/global/lang_negotiation.rs
Normal file
56
src/global/lang_negotiation.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use crate::AutoDefault;
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
/// Modos disponibles para negociar el idioma de una petición HTTP.
|
||||
///
|
||||
/// El ajuste [`global::SETTINGS.app.lang_negotiation`](crate::global::App::lang_negotiation)
|
||||
/// determina qué fuentes intervienen en la resolución del idioma efectivo utilizado por
|
||||
/// [`RequestLocale`](crate::locale::RequestLocale) y en la generación de URLs mediante
|
||||
/// [`Context::route()`](crate::core::component::Context::route).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum LangNegotiation {
|
||||
/// Usa todas las fuentes disponibles para determinar el idioma, en este orden: comprueba el
|
||||
/// parámetro `?lang` de la URL; si no está presente o no es válido, usa la cabecera HTTP
|
||||
/// `Accept-Language`; si tampoco está disponible o no es válido, usa el idioma configurado en
|
||||
/// [`global::SETTINGS.app.language`](crate::global::App::language) o, en su defecto, el idioma
|
||||
/// de respaldo. Es el comportamiento por defecto.
|
||||
#[default]
|
||||
Full,
|
||||
|
||||
/// Igual que `LangNegotiation::Full`, pero sin tener en cuenta el parámetro `?lang` de la URL.
|
||||
/// El idioma depende únicamente de la cabecera `Accept-Language` del navegador y, en última
|
||||
/// instancia, de la configuración o idioma de respaldo.
|
||||
NoQuery,
|
||||
|
||||
/// Usa sólo la configuración o, en su defecto, el idioma de respaldo; ignora la cabecera
|
||||
/// `Accept-Language` y el parámetro de la URL. Este modo proporciona un comportamiento estable
|
||||
/// con idioma fijo.
|
||||
ConfigOnly,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for LangNegotiation {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let raw = String::deserialize(deserializer)?;
|
||||
let result = match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"full" => Self::Full,
|
||||
"noquery" => Self::NoQuery,
|
||||
"configonly" => Self::ConfigOnly,
|
||||
_ => {
|
||||
let default = Self::default();
|
||||
println!(
|
||||
concat!(
|
||||
"\nInvalid value \"{}\" for [app].lang_negotiation. ",
|
||||
"Using \"{:?}\". Check settings.",
|
||||
),
|
||||
raw, default,
|
||||
);
|
||||
default
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue