Compare commits
No commits in common. "main" and "legacy/tera_support" have entirely different histories.
main
...
legacy/ter
404 changed files with 11554 additions and 21449 deletions
|
|
@ -1,69 +0,0 @@
|
|||
# cliff.toml
|
||||
|
||||
[changelog]
|
||||
header = """
|
||||
# CHANGELOG
|
||||
|
||||
Este archivo documenta los cambios más relevantes realizados en cada versión. El formato está basado
|
||||
en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y las versiones se numeran siguiendo
|
||||
las reglas del [Versionado Semántico](https://semver.org/lang/es/).
|
||||
|
||||
Resume la evolución del proyecto para usuarios y colaboradores, destacando nuevas funcionalidades,
|
||||
correcciones, mejoras durante el desarrollo o cambios en la documentación. Cambios menores o
|
||||
internos pueden omitirse si no afectan al uso del proyecto.
|
||||
"""
|
||||
trim = true
|
||||
render_always = true
|
||||
|
||||
body = """
|
||||
{% if version %}
|
||||
## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }})
|
||||
{% else %}
|
||||
## Pendiente de publicación
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
|
||||
{% for commit in commits %}
|
||||
{%- set msg = commit.message
|
||||
| split(pat="\n")
|
||||
| first
|
||||
| replace(from="✨ ", to="")
|
||||
| replace(from="🐛 ", to="")
|
||||
| replace(from="🚑 ", to="")
|
||||
| replace(from="⬆️ ", to="")
|
||||
| replace(from="🚧 ", to="")
|
||||
| replace(from="♻️ ", to="")
|
||||
| replace(from="✏️ ", to="")
|
||||
| replace(from="🏷️ ", to="")
|
||||
| replace(from="🧑💻 ", to="")
|
||||
| replace(from="🍱 ", to="")
|
||||
| replace(from="📝 ", to="")
|
||||
| replace(from="💡 ", to="")
|
||||
-%}
|
||||
|
||||
- {{ msg | trim }} {% if commit.author.name != "Manuel Cillero" %} - {{ commit.author.name }}{% endif %}
|
||||
{% endfor %}{% endfor %}
|
||||
"""
|
||||
|
||||
[git]
|
||||
conventional_commits = false
|
||||
filter_unconventional = false
|
||||
topo_order_commits = true
|
||||
sort_commits = "oldest"
|
||||
|
||||
commit_parsers = [
|
||||
{ message = "^✨", group = "Añadido" },
|
||||
{ message = "^🐛", group = "Corregido" },
|
||||
{ message = "^🚑", group = "Corregido" },
|
||||
{ message = "^🚧", group = "Cambiado" },
|
||||
{ message = "^♻️", group = "Cambiado" },
|
||||
{ message = "^✏️", group = "Cambiado" },
|
||||
{ message = "^🏷️", group = "Cambiado" },
|
||||
{ message = "^🧑💻", group = "Cambiado" },
|
||||
{ message = "^🍱", group = "Cambiado" },
|
||||
{ message = "^⬆️", group = "Dependencias" },
|
||||
{ message = "^📝", group = "Documentado" },
|
||||
{ message = "^💡", group = "Documentado" },
|
||||
{ message = "^.*", group = "Otros cambios" },
|
||||
]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
[alias]
|
||||
ts = ["test", "--features", "testing"] # cargo ts
|
||||
tw = ["test", "--workspace", "--features", "testing"] # cargo tw
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# release.toml
|
||||
|
||||
# Etiqueta por crate: `pagetop-macros-v0.2.0`
|
||||
tag-prefix = "{{crate_name}}-"
|
||||
|
||||
# Confirmaciones firmadas (no requeridas)
|
||||
sign-commit = false
|
||||
sign-tag = false
|
||||
|
||||
# Empuja etiquetas y commits
|
||||
push = true
|
||||
|
||||
# Publica en crates.io (puedes desactivarlo para pruebas)
|
||||
publish = true
|
||||
|
||||
# Solo permite publicar estos crates (los que forman parte del workspace)
|
||||
allow-branch = ["main"]
|
||||
consolidate-commits = false
|
||||
|
||||
# Mensaje personalizado para el commit de versión
|
||||
pre-release-commit-message = "🔖 Prepara publicación de {{crate_name}} {{version}}"
|
||||
|
||||
pre-release-hook = [
|
||||
"sh", "-c", "ROOT=$(git rev-parse --show-toplevel) && \"$ROOT/tools/changelog.sh\" {{crate_name}} {{version}} --stage"
|
||||
]
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,10 +1,4 @@
|
|||
# Ignora directorios de compilación
|
||||
**/target
|
||||
|
||||
# Archivos de log
|
||||
**/log/*.log*
|
||||
|
||||
# Archivos de configuración locales
|
||||
**/local.*.toml
|
||||
**/local.toml
|
||||
.env
|
||||
**/*.local.*.toml
|
||||
workdir
|
||||
|
|
|
|||
86
CHANGELOG.md
86
CHANGELOG.md
|
|
@ -1,86 +0,0 @@
|
|||
# CHANGELOG
|
||||
|
||||
Este archivo documenta los cambios más relevantes realizados en cada versión. El formato está basado
|
||||
en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y las versiones se numeran siguiendo
|
||||
las reglas del [Versionado Semántico](https://semver.org/lang/es/).
|
||||
|
||||
Resume la evolución del proyecto para usuarios y colaboradores, destacando nuevas funcionalidades,
|
||||
correcciones, mejoras durante el desarrollo o cambios en la documentación. Cambios menores o
|
||||
internos pueden omitirse si no afectan al uso del proyecto.
|
||||
|
||||
## 0.4.0 (2025-09-20)
|
||||
|
||||
### Añadido
|
||||
|
||||
- [app] Añade manejo de rutas no encontradas
|
||||
- [context] Añade métodos auxiliares de parámetros
|
||||
- [util] Añade `indoc` para indentar código bien
|
||||
- Añade componente `PoweredBy` para copyright
|
||||
|
||||
### Cambiado
|
||||
|
||||
- [html] Cambia tipos `Option...` por `Attr...`
|
||||
- [html] Implementa `Default` en `Context`
|
||||
- [welcome] Crea página de bienvenida desde intro
|
||||
- [context] Generaliza los parámetros de contexto
|
||||
- [context] Define un `trait` común de contexto
|
||||
- Modifica tipos para atributos HTML a minúsculas
|
||||
- Renombra `with_component` por `add_child`
|
||||
|
||||
### Corregido
|
||||
|
||||
- [welcome] Corrige giro botón con ancho estrecho
|
||||
- [welcome] Corrige centrado del pie de página
|
||||
- Corrige nombre de función en prueba de `Html`
|
||||
- Corrige doc y código por cambios en Page
|
||||
|
||||
### Dependencias
|
||||
|
||||
- Actualiza dependencias para 0.4.0
|
||||
|
||||
### Documentado
|
||||
|
||||
- [component] Amplía documentación de preparación
|
||||
- Normaliza referencias al nombre PageTop
|
||||
- Simplifica documentación de obsoletos
|
||||
- Mejora la documentación de recursos y contexto
|
||||
|
||||
### Otros cambios
|
||||
|
||||
- 🎨 [theme] Mejora gestión de regiones en páginas
|
||||
- ✅ [tests] Amplía pruebas para `PrepareMarkup'
|
||||
- 🎨 [locale] Mejora el uso de `lookup` / `using`
|
||||
- 🔨 [tools] Fuerza pulsar intro para confirmar input
|
||||
- 💄 Aplica BEM a estilos de bienvenida y componente
|
||||
- 🎨 Unifica conversiones a String con `to_string()`
|
||||
- 🔥 Elimina `Render` para usar siempre el contexto
|
||||
|
||||
## 0.3.0 (2025-08-16)
|
||||
|
||||
### Cambiado
|
||||
|
||||
- Redefine función para directorios absolutos
|
||||
- Mejora la integración de archivos estáticos
|
||||
|
||||
### Documentado
|
||||
|
||||
- Cambia el formato para la documentación
|
||||
|
||||
## 0.2.0 (2025-08-09)
|
||||
|
||||
### Añadido
|
||||
|
||||
- Añade librería para gestionar recursos estáticos
|
||||
- Añade soporte a changelog de `pagetop-statics`
|
||||
|
||||
### Documentado
|
||||
|
||||
- Corrige enlace del botón de licencia en la documentación
|
||||
|
||||
### Otros cambios
|
||||
|
||||
- Afina Cargo.toml para buscar la mejor categoría
|
||||
|
||||
## 0.1.0 (2025-08-06)
|
||||
|
||||
- Versión inicial
|
||||
64
CREDITS.md
64
CREDITS.md
|
|
@ -1,33 +1,53 @@
|
|||
# 🔃 Dependencias
|
||||
# 🔃 Dependencies
|
||||
|
||||
PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
|
||||
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs) como son:
|
||||
PageTop is developed using the [Rust programming language](https://www.rust-lang.org/) and stands on
|
||||
the shoulders of giants, leveraging some of the most stable and renowned libraries (*crates*) from
|
||||
the [Rust ecosystem](https://lib.rs), including:
|
||||
|
||||
* [Actix Web](https://actix.rs/) para los servicios web.
|
||||
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
|
||||
* [Tracing](https://github.com/tokio-rs/tracing) para la gestión de trazas y registro de eventos
|
||||
de la aplicación.
|
||||
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
|
||||
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
|
||||
* Además de otros *crates* adicionales que se pueden explorar en los archivos `Cargo.toml` de
|
||||
PageTop y sus extensiones.
|
||||
* [Actix Web](https://actix.rs/) for web services and server management.
|
||||
* [Tracing](https://github.com/tokio-rs/tracing) for diagnostics and structured logging.
|
||||
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), which integrate
|
||||
[Fluent](https://projectfluent.org/) for internationalization.
|
||||
* Additional crates, which you can explore in the `Cargo.toml` files of PageTop and its packages.
|
||||
|
||||
# ⌨️ Code
|
||||
|
||||
PageTop incorporates code from several well-regarded crates to enhance its functionality:
|
||||
|
||||
* **[Config (v0.11.0)](https://github.com/mehcode/config-rs/tree/0.11.0)**: Includes code from
|
||||
[config-rs](https://crates.io/crates/config) by [Ryan Leckey](https://crates.io/users/mehcode),
|
||||
chosen for its advantages in reading configuration settings and delegating assignment to safe
|
||||
types, tailored to the specific needs of each package, theme, or application.
|
||||
|
||||
* **[Maud (v0.25.0)](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud)**: An adapted version
|
||||
of the excellent [maud](https://crates.io/crates/maud) crate by
|
||||
[Chris Wong](https://crates.io/users/lambda-fairy) is integrated, enabling its functionalities
|
||||
without requiring a direct dependency in the `Cargo.toml` files.
|
||||
|
||||
* **SmartDefault (v0.7.1)**: The [SmartDefault](https://crates.io/crates/smart_default) crate by
|
||||
[Jane Doe](https://crates.io/users/jane-doe) has been embedded as `AutoDefault`, simplifying
|
||||
`Default` implementations and eliminating the need to explicitly reference `smart_default` in
|
||||
the `Cargo.toml` files.
|
||||
|
||||
# 🗚 FIGfonts
|
||||
|
||||
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
|
||||
para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
|
||||
[FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
|
||||
PageTop uses the [figlet-rs](https://crates.io/crates/figlet-rs) package by *yuanbohan* to display a
|
||||
presentation banner in the terminal featuring the application's name in
|
||||
[FIGlet](http://www.figlet.org) characters. The fonts included in `pagetop/src/app` are:
|
||||
|
||||
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) de *Glenn Chappell*
|
||||
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) de *Glenn Chappell*
|
||||
(predeterminada)
|
||||
* [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) de *Claude Martins*
|
||||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
|
||||
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) by *Glenn Chappell*
|
||||
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) by *Glenn Chappell* (default)
|
||||
* [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) by *Claude Martins*
|
||||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) by *Ryan Youck*
|
||||
|
||||
# 📰 Templates
|
||||
|
||||
# 🎨 Icono
|
||||
The default welcome homepage design is inspired by a tutorial for creating a unique
|
||||
[Neobrutalism](https://www.codewithfaraz.com/content/109/creating-a-unique-neobrutalism-portfolio-page-with-html-css-and-javascript)
|
||||
portfolio page by [Faraz](https://www.codewithfaraz.com/).
|
||||
|
||||
"La Criatura" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||
Forma parte de su colección [Nasty Icons](https://www.iconfinder.com/iconsets/nasty), disponible en
|
||||
# 🎨 Icon
|
||||
|
||||
"The Creature" smiling is a playful creation by [Webalys](https://www.iconfinder.com/webalys). It is
|
||||
part of their [Nasty Icons](https://www.iconfinder.com/iconsets/nasty) collection, available on
|
||||
[ICONFINDER](https://www.iconfinder.com).
|
||||
|
|
|
|||
3153
Cargo.lock
generated
3153
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
98
Cargo.toml
98
Cargo.toml
|
|
@ -1,90 +1,40 @@
|
|||
[package]
|
||||
name = "pagetop"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.
|
||||
"""
|
||||
categories = ["web-programming::http-server"]
|
||||
keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
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"
|
||||
itoa = "1.0"
|
||||
parking_lot = "0.12"
|
||||
paste = { package = "pastey", version = "0.1" }
|
||||
substring = "1.4"
|
||||
terminal_size = "0.4"
|
||||
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
|
||||
tracing-actix-web = "0.7"
|
||||
|
||||
fluent-templates = "0.13"
|
||||
unic-langid = { version = "0.9", features = ["macros"] }
|
||||
|
||||
actix-web = { workspace = true, default-features = true }
|
||||
actix-session = { version = "0.11", features = ["cookie-session"] }
|
||||
actix-web-files = { package = "actix-files", version = "0.6" }
|
||||
|
||||
serde.workspace = true
|
||||
|
||||
pagetop-macros.workspace = true
|
||||
pagetop-statics.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
testing = []
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.23"
|
||||
serde_json = "1.0"
|
||||
pagetop-aliner.workspace = true
|
||||
pagetop-bootsier.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
pagetop-build.workspace = true
|
||||
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
# Helpers
|
||||
"helpers/pagetop-build",
|
||||
"helpers/pagetop-macros",
|
||||
"helpers/pagetop-statics",
|
||||
# Extensions
|
||||
"extensions/pagetop-aliner",
|
||||
"extensions/pagetop-bootsier",
|
||||
|
||||
# PageTop
|
||||
"pagetop",
|
||||
|
||||
# Packages
|
||||
"packages/pagetop-aliner",
|
||||
"packages/pagetop-bootsier",
|
||||
"packages/pagetop-seaorm",
|
||||
|
||||
# App
|
||||
"drust",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
repository = "https://git.cillero.es/manuelcillero/pagetop"
|
||||
homepage = "https://pagetop.cillero.es"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/manuelcillero/pagetop"
|
||||
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
actix-web = { version = "4.11", default-features = false }
|
||||
include_dir = "0.7.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
static-files = "0.2.4"
|
||||
|
||||
# Helpers
|
||||
pagetop-build = { version = "0.3", path = "helpers/pagetop-build" }
|
||||
pagetop-macros = { version = "0.2", path = "helpers/pagetop-macros" }
|
||||
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
||||
# Extensions
|
||||
pagetop-aliner = { version = "0.0", path = "extensions/pagetop-aliner" }
|
||||
pagetop-bootsier = { version = "0.0", path = "extensions/pagetop-bootsier" }
|
||||
pagetop-build = { version = "0.0", path = "helpers/pagetop-build" }
|
||||
pagetop-macros = { version = "0.0", path = "helpers/pagetop-macros" }
|
||||
|
||||
# PageTop
|
||||
pagetop = { version = "0.4", path = "." }
|
||||
pagetop = { version = "0.0", path = "pagetop" }
|
||||
|
||||
# Packages
|
||||
pagetop-aliner = { version = "0.0", path = "packages/pagetop-aliner" }
|
||||
pagetop-bootsier = { version = "0.0", path = "packages/pagetop-bootsier" }
|
||||
|
|
|
|||
151
README.md
151
README.md
|
|
@ -1,58 +1,41 @@
|
|||
<div align="center">
|
||||
|
||||
<img src="https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/banner.png" />
|
||||
<img src="https://raw.githubusercontent.com/manuelcillero/pagetop/main/static/banner.png" />
|
||||
|
||||
<h1>PageTop</h1>
|
||||
|
||||
<p>Un entorno para el desarrollo de soluciones web modulares, extensibles y configurables.</p>
|
||||
<p>An opinionated web framework to build modular <em>Server-Side Rendering</em> web solutions.</p>
|
||||
|
||||
[](https://docs.rs/pagetop)
|
||||
[](#-license)
|
||||
[](https://docs.rs/pagetop)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
||||
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
||||
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
|
||||
según las necesidades de cada proyecto, incluyendo:
|
||||
## Overview
|
||||
|
||||
* **Acciones** (*actions*): alteran la lógica interna de una funcionalidad interceptando su flujo
|
||||
de ejecución.
|
||||
* **Componentes** (*components*): encapsulan HTML, CSS y JavaScript en unidades funcionales,
|
||||
configurables y reutilizables.
|
||||
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
|
||||
de PageTop o de terceros.
|
||||
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
||||
componentes sin comprometer su funcionalidad.
|
||||
The PageTop core API provides a comprehensive toolkit for extending its functionalities to specific
|
||||
requirements and application scenarios through actions, components, packages, and themes:
|
||||
|
||||
* **Actions** serve as a mechanism to customize PageTop's internal behavior by intercepting its
|
||||
execution flow.
|
||||
* **Components** encapsulate HTML, CSS, and JavaScript into functional, configurable, and
|
||||
well-defined units.
|
||||
* **Packages** extend or customize existing functionality by interacting with PageTop APIs or
|
||||
third-party package APIs.
|
||||
* **Themes** enable developers to alter the appearance of pages and components without affecting
|
||||
their functionality.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
# ⚡️ Quick start
|
||||
|
||||
La aplicación más sencilla de PageTop se ve así:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::new().run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
|
||||
bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
|
||||
|
||||
Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera:
|
||||
|
||||
```rust,no_run
|
||||
```rust
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct HelloWorld;
|
||||
|
||||
impl Extension for HelloWorld {
|
||||
impl PackageTrait for HelloWorld {
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(hello_world));
|
||||
}
|
||||
|
|
@ -60,7 +43,7 @@ impl Extension for HelloWorld {
|
|||
|
||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||
.with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } }))
|
||||
.render()
|
||||
}
|
||||
|
||||
|
|
@ -70,82 +53,42 @@ async fn main() -> std::io::Result<()> {
|
|||
}
|
||||
```
|
||||
|
||||
Este programa implementa una extensión llamada `HelloWorld` que sirve una página web en la ruta raíz
|
||||
(`/`) mostrando el texto "Hello world!" dentro de un elemento HTML `<h1>`.
|
||||
This program features a `HelloWorld` package, providing a service that serves a greeting web page
|
||||
accessible via `http://localhost:8088` under default settings.
|
||||
|
||||
|
||||
# 📂 Repositorio
|
||||
# 📂 Helpers
|
||||
|
||||
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
||||
* [pagetop-macros](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-macros):
|
||||
A collection of macros that enhance the development experience within PageTop.
|
||||
|
||||
* **[pagetop](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/src)**, con el código
|
||||
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.
|
||||
|
||||
* **[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.
|
||||
|
||||
## 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.
|
||||
|
||||
* **[pagetop-bootsier](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier)**,
|
||||
tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y
|
||||
componentes flexibles.
|
||||
* [pagetop-build](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-build):
|
||||
Simplifies the process of embedding resources directly into binary files for PageTop applications.
|
||||
|
||||
|
||||
# 🧪 Pruebas
|
||||
# 🚧 Warning
|
||||
|
||||
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:
|
||||
|
||||
| Comando | Descripción |
|
||||
| ------- | ----------- |
|
||||
| `cargo ts` | Ejecuta los tests de `pagetop` (*unit + integration*) con la *feature* `testing`. |
|
||||
| `cargo ts --test util` | Lanza sólo las pruebas de integración del módulo `util`. |
|
||||
| `cargo ts --doc locale` | Lanza las pruebas de la documentación del módulo `locale`. |
|
||||
| `cargo tw` | Ejecuta los tests de **todos los paquetes** del *workspace*. |
|
||||
|
||||
> **Nota**
|
||||
> Estos alias ya compilan con la configuración adecuada. No requieren `--no-default-features`.
|
||||
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
|
||||
**PageTop** framework is currently in active development. The API is unstable and subject to
|
||||
frequent changes. Production use is not recommended until version **0.1.0**.
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
# 📜 License
|
||||
|
||||
**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**.
|
||||
PageTop is free, open source and permissively licensed! Except where noted (below and/or in
|
||||
individual files), all code in this project is dual-licensed under either:
|
||||
|
||||
* MIT License
|
||||
([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
|
||||
|
||||
* Apache License, Version 2.0,
|
||||
([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
at your option. This means you can select the license you prefer! This dual-licensing approach is
|
||||
the de-facto standard in the Rust ecosystem.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
# ✨ Contributions
|
||||
|
||||
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.
|
||||
|
||||
|
||||
# ✨ Contribuir
|
||||
|
||||
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
|
||||
licencia *Apache v2.0*.
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the
|
||||
work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
|
||||
additional terms or conditions.
|
||||
|
|
|
|||
6
config/common.toml
Normal file
6
config/common.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[app]
|
||||
name = "Drust"
|
||||
description = "A modern web Content Management System to share your world."
|
||||
|
||||
[database]
|
||||
db_type = "mysql"
|
||||
|
|
@ -1,2 +1,7 @@
|
|||
[app]
|
||||
theme = "Aliner"
|
||||
#theme = "Bootsier"
|
||||
language = "es-ES"
|
||||
|
||||
[log]
|
||||
tracing = "Info,pagetop=Debug"
|
||||
tracing = "Info,pagetop=Debug,sqlx::query=Warn"
|
||||
|
|
|
|||
7
config/local.default.toml
Normal file
7
config/local.default.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[database]
|
||||
db_name = "drust"
|
||||
db_user = "drust"
|
||||
db_pass = "demo"
|
||||
|
||||
[dev]
|
||||
pagetop_project_dir = "/home/manuelcillero/Proyectos/pagetop"
|
||||
37
config/predefined-settings.toml
Normal file
37
config/predefined-settings.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[app]
|
||||
name = "My App"
|
||||
description = "Developed with the amazing PageTop framework."
|
||||
# Default theme.
|
||||
theme = ""
|
||||
# Default language (localization).
|
||||
language = "en-US"
|
||||
# Default text direction: "ltr", "rtl", or "auto".
|
||||
text_direction = "ltr"
|
||||
# Banner displayed at startup: "Off", "Slant", "Small", "Speed", or "Starwars".
|
||||
startup_banner = "Slant"
|
||||
|
||||
[dev]
|
||||
# During development, serve static files from the project's root directory to
|
||||
# avoid recompilation.
|
||||
pagetop_project_dir = ""
|
||||
|
||||
[log]
|
||||
# Execution trace level: "Error", "Warn", "Info", "Debug", or "Trace".
|
||||
# Example: tracing = "Error,actix_server::builder=Info,tracing_actix_web=Debug"
|
||||
tracing = "Info"
|
||||
# In terminal ("Stdout") or files "Daily", "Hourly", "Minutely", or "Endless".
|
||||
rolling = "Stdout"
|
||||
# Directory for trace files (if rolling != "Stdout").
|
||||
path = "log"
|
||||
# Prefix for trace files (if rolling != "Stdout").
|
||||
prefix = "tracing.log"
|
||||
# Traces format: "Full", "Compact", "Pretty", or "Json".
|
||||
format = "Full"
|
||||
|
||||
[server]
|
||||
# Web server config.
|
||||
bind_address = "localhost"
|
||||
bind_port = 8088
|
||||
# If cookies are used, specify the session cookie duration (in seconds). A value
|
||||
# of 0 means "until the browser is closed". Default: one week.
|
||||
session_lifetime = 604800
|
||||
36
drust/Cargo.toml
Normal file
36
drust/Cargo.toml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
[package]
|
||||
name = "drust"
|
||||
version = "0.0.3"
|
||||
edition = "2021"
|
||||
|
||||
description = """\
|
||||
A modern web Content Management System to share your world.\
|
||||
"""
|
||||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
pagetop.workspace = true
|
||||
|
||||
# Packages.
|
||||
pagetop-bootsier.workspace = true
|
||||
#pagetop-admin = { version = "0.0", path = "../pagetop-admin" }
|
||||
#pagetop-user = { version = "0.0", path = "../pagetop-user" }
|
||||
#pagetop-node = { version = "0.0", path = "../pagetop-node" }
|
||||
|
||||
#[features]
|
||||
#default = [ "mysql" ]
|
||||
#mysql = [
|
||||
# "pagetop-user/mysql",
|
||||
# "pagetop-node/mysql",
|
||||
#]
|
||||
#postgres = [
|
||||
# "pagetop-user/postgres",
|
||||
# "pagetop-node/postgres",
|
||||
#]
|
||||
#sqlite = [
|
||||
# "pagetop-user/sqlite",
|
||||
# "pagetop-node/sqlite",
|
||||
#]
|
||||
20
drust/src/main.rs
Normal file
20
drust/src/main.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
struct Drust;
|
||||
|
||||
impl PackageTrait for Drust {
|
||||
fn dependencies(&self) -> Vec<PackageRef> {
|
||||
vec![
|
||||
// Packages.
|
||||
&pagetop_bootsier::Bootsier,
|
||||
//&pagetop_admin::Admin,
|
||||
//&pagetop_user::User,
|
||||
//&pagetop_node::Node,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&Drust).run()?.await
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use pagetop_bootsier::prelude::*;
|
||||
|
||||
struct SuperMenu;
|
||||
|
||||
impl Extension for SuperMenu {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
||||
}
|
||||
|
||||
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)))
|
||||
.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_blank(
|
||||
L10n::l("sample_menus_item_blank"),
|
||||
|_| "https://docs.rs/pagetop",
|
||||
))
|
||||
.add_item(nav::Item::dropdown(
|
||||
Dropdown::new()
|
||||
.with_title(L10n::l("sample_menus_test_title"))
|
||||
.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",
|
||||
))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_dev_guides"),
|
||||
|_| "/dev/guides",
|
||||
))
|
||||
.add_item(dropdown::Item::link_blank(
|
||||
L10n::l("sample_menus_dev_forum"),
|
||||
|_| "https://forum.example.dev",
|
||||
))
|
||||
.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",
|
||||
))
|
||||
.add_item(dropdown::Item::link(L10n::l("sample_menus_sdk_js"), |_| {
|
||||
"/dev/sdks/js"
|
||||
}))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_sdk_python"),
|
||||
|_| "/dev/sdks/python",
|
||||
))
|
||||
.add_item(dropdown::Item::divider())
|
||||
.add_item(dropdown::Item::header(L10n::l(
|
||||
"sample_menus_plugin_header",
|
||||
)))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_plugin_auth"),
|
||||
|_| "/dev/sdks/rust/plugins/auth",
|
||||
))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_plugin_cache"),
|
||||
|_| "/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"),
|
||||
|_| "#",
|
||||
)),
|
||||
))
|
||||
.add_item(nav::Item::link_disabled(
|
||||
L10n::l("sample_menus_item_disabled"),
|
||||
|_| "#",
|
||||
)),
|
||||
))
|
||||
.add_item(navbar::Item::nav(
|
||||
Nav::new()
|
||||
.with_classes(
|
||||
ClassesOp::Add,
|
||||
classes::Margin::with(Side::Start, ScaleSize::Auto).to_class(),
|
||||
)
|
||||
.add_item(nav::Item::link(
|
||||
L10n::l("sample_menus_item_sign_up"),
|
||||
|_| "/auth/sign-up",
|
||||
))
|
||||
.add_item(nav::Item::link(L10n::l("sample_menus_item_login"), |_| {
|
||||
"/auth/login"
|
||||
})),
|
||||
));
|
||||
|
||||
InRegion::Named("header").add(Child::with(
|
||||
Container::new()
|
||||
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
||||
.add_child(navbar_menu),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&SuperMenu).run()?.await
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
[package]
|
||||
name = "pagetop-aliner"
|
||||
version = "0.0.9"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Tema de PageTop que muestra esquemáticamente la composición de las páginas HTML
|
||||
"""
|
||||
categories = ["web-programming", "gui"]
|
||||
keywords = ["pagetop", "theme", "css"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pagetop.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
pagetop-build.workspace = true
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Aliner</h1>
|
||||
|
||||
<p>Tema de <strong>PageTop</strong> que muestra esquemáticamente la composición de las páginas HTML.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
|
||||
|
||||
<br>
|
||||
</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.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-aliner = "..."
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
// ...
|
||||
&pagetop_aliner::Aliner,
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&MyApp).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Y **selecciona el tema en la configuración** de la aplicación:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
theme = "Aliner"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_aliner::Aliner;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_theme(&Aliner)
|
||||
.add_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.add_child(Html::with(|cx| html! {
|
||||
p { (L10n::l("sample_content").using(cx)) }
|
||||
})),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# 🚧 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.
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop Aliner</h1>
|
||||
|
||||
<p>Tema para <strong>PageTop</strong> que muestra esquemáticamente la composición de las páginas HTML.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
|
||||
|
||||
<br>
|
||||
</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.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-aliner = "..."
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
// ...
|
||||
&pagetop_aliner::Aliner,
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&MyApp).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Y **selecciona el tema en la configuración** de la aplicación:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
theme = "Aliner"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_aliner::Aliner;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_theme(&Aliner)
|
||||
.add_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.add_child(Html::with(|cx| html! {
|
||||
p { (L10n::l("sample_content").using(cx)) }
|
||||
})),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
/// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML.
|
||||
///
|
||||
/// Define un tema mínimo útil para:
|
||||
///
|
||||
/// - Comprobar el funcionamiento de temas, plantillas y regiones.
|
||||
/// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos.
|
||||
/// - Realizar pruebas de renderizado rápido con salida estable y predecible.
|
||||
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.
|
||||
pub struct Aliner;
|
||||
|
||||
impl Extension for Aliner {
|
||||
fn theme(&self) -> Option<ThemeRef> {
|
||||
Some(&Self)
|
||||
}
|
||||
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
static_files_service!(scfg, [aliner] => "/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),
|
||||
));
|
||||
}
|
||||
}
|
||||
1
extensions/pagetop-bootsier/.gitattributes
vendored
1
extensions/pagetop-bootsier/.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
|||
static/** linguist-vendored
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
[package]
|
||||
name = "pagetop-bootsier"
|
||||
version = "0.0.18"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.
|
||||
"""
|
||||
categories = ["web-programming", "gui"]
|
||||
keywords = ["pagetop", "theme", "bootstrap", "css", "js"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pagetop.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
pagetop-build.workspace = true
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Bootsier</h1>
|
||||
|
||||
<p>Tema de <strong>PageTop</strong> basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
|
||||
|
||||
<br>
|
||||
</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.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-bootsier = "..."
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
// ...
|
||||
&pagetop_bootsier::Bootsier,
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&MyApp).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Y **selecciona el tema en la configuración** de la aplicación:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
theme = "Bootsier"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_bootsier::Bootsier;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_theme(&Bootsier)
|
||||
.add_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.add_child(Html::with(|cx| html! {
|
||||
p { (L10n::l("sample_content").using(cx)) }
|
||||
})),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# 🚧 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.
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
//! Opciones de configuración del tema.
|
||||
//!
|
||||
//! Ejemplo:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [bootsier]
|
||||
//! max_width = "90rem"
|
||||
//! ```
|
||||
//!
|
||||
//! Uso:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! use pagetop_bootsier::config;
|
||||
//!
|
||||
//! assert_eq!(config::SETTINGS.bootsier.max_width, UnitValue::Px(1440));
|
||||
//! ```
|
||||
//!
|
||||
//! Consulta [`pagetop::config`] para ver cómo PageTop lee los archivos de configuración y aplica
|
||||
//! los valores a los ajustes.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
include_config!(SETTINGS: Settings => [
|
||||
// [bootsier]
|
||||
"bootsier.max_width" => "1440px",
|
||||
]);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Tipos para la sección [`[bootsier]`](Bootsier) de [`SETTINGS`].
|
||||
pub struct Settings {
|
||||
pub bootsier: Bootsier,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Sección `[bootsier]` de la configuración. Forma parte de [`Settings`].
|
||||
pub struct Bootsier {
|
||||
/// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem".
|
||||
pub max_width: UnitValue,
|
||||
}
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop Bootsier</h1>
|
||||
|
||||
<p>Tema de <strong>PageTop</strong> basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
|
||||
|
||||
<br>
|
||||
</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.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-bootsier = "..."
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
// ...
|
||||
&pagetop_bootsier::Bootsier,
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&MyApp).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Y **selecciona el tema en la configuración** de la aplicación:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
theme = "Bootsier"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_bootsier::Bootsier;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_theme(&Bootsier)
|
||||
.add_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.add_child(Html::with(|cx| html! {
|
||||
p { (L10n::l("sample_content").using(cx)) }
|
||||
})),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
||||
#![doc(
|
||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||
)]
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
include_locales!(LOCALES_BOOTSIER);
|
||||
|
||||
// Versión de la librería Bootstrap.
|
||||
const BOOTSTRAP_VERSION: &str = "5.3.8";
|
||||
|
||||
pub mod config;
|
||||
|
||||
pub mod theme;
|
||||
|
||||
/// *Prelude* del tema.
|
||||
pub mod prelude {
|
||||
pub use crate::config::*;
|
||||
pub use crate::theme::*;
|
||||
}
|
||||
|
||||
/// Implementa el tema.
|
||||
pub struct Bootsier;
|
||||
|
||||
impl Extension for Bootsier {
|
||||
fn theme(&self) -> Option<ThemeRef> {
|
||||
Some(&Self)
|
||||
}
|
||||
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
static_files_service!(scfg, [bootsier_bs] => "/bootsier/bs");
|
||||
static_files_service!(scfg, [bootsier_js] => "/bootsier/js");
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme for Bootsier {
|
||||
fn after_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/bootsier/bs/bootstrap.min.css")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
))
|
||||
.alter_assets(ContextOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Toggle Dropdown
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Close
|
||||
|
||||
# Navbar
|
||||
toggle = Toggle navigation
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Mostrar/ocultar menú
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Cerrar
|
||||
|
||||
# Navbar
|
||||
toggle = Mostrar/ocultar navegación
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
//! Definiciones y componentes del tema.
|
||||
//!
|
||||
//! En esta página, el apartado **Modules** incluye las definiciones necesarias para los componentes
|
||||
//! que se muestran en el apartado **Structs**, mientras que en **Enums** se listan los elementos
|
||||
//! auxiliares del tema utilizados en clases y componentes.
|
||||
|
||||
mod aux;
|
||||
pub use aux::*;
|
||||
|
||||
pub mod classes;
|
||||
|
||||
// Container.
|
||||
pub mod container;
|
||||
#[doc(inline)]
|
||||
pub use container::Container;
|
||||
|
||||
// Dropdown.
|
||||
pub mod dropdown;
|
||||
#[doc(inline)]
|
||||
pub use dropdown::Dropdown;
|
||||
|
||||
// Image.
|
||||
pub mod image;
|
||||
#[doc(inline)]
|
||||
pub use image::Image;
|
||||
|
||||
// Nav.
|
||||
pub mod nav;
|
||||
#[doc(inline)]
|
||||
pub use nav::Nav;
|
||||
|
||||
// Navbar.
|
||||
pub mod navbar;
|
||||
#[doc(inline)]
|
||||
pub use navbar::Navbar;
|
||||
|
||||
// Offcanvas.
|
||||
pub mod offcanvas;
|
||||
#[doc(inline)]
|
||||
pub use offcanvas::Offcanvas;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
//! Colección de elementos auxiliares de Bootstrap para Bootsier.
|
||||
|
||||
mod breakpoint;
|
||||
pub use breakpoint::BreakPoint;
|
||||
|
||||
mod color;
|
||||
pub use color::{Color, Opacity};
|
||||
pub use color::{ColorBg, ColorText};
|
||||
|
||||
mod layout;
|
||||
pub use layout::{ScaleSize, Side};
|
||||
|
||||
mod border;
|
||||
pub use border::BorderColor;
|
||||
|
||||
mod rounded;
|
||||
pub use rounded::RoundedRadius;
|
||||
|
||||
mod button;
|
||||
pub use button::{ButtonColor, ButtonSize};
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::aux::Color;
|
||||
|
||||
/// Colores `border-*` para los bordes ([`classes::Border`](crate::theme::classes::Border)).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum BorderColor {
|
||||
/// No define ninguna clase.
|
||||
#[default]
|
||||
Default,
|
||||
/// Genera internamente clases `border-{color}`.
|
||||
Theme(Color),
|
||||
/// Genera internamente clases `border-{color}-subtle` (un tono suavizado del color).
|
||||
Subtle(Color),
|
||||
/// Color negro.
|
||||
Black,
|
||||
/// Color blanco.
|
||||
White,
|
||||
}
|
||||
|
||||
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.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Default => None,
|
||||
Self::Theme(_) => Some(""),
|
||||
Self::Subtle(_) => Some("-subtle"),
|
||||
Self::Black => Some("-black"),
|
||||
Self::White => Some("-white"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
match self {
|
||||
Self::Theme(c) | Self::Subtle(c) => {
|
||||
classes.push_str(Self::BORDER_PREFIX);
|
||||
classes.push_str(c.as_str());
|
||||
}
|
||||
_ => classes.push_str(Self::BORDER),
|
||||
}
|
||||
classes.push_str(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
/// Devuelve la clase `border-*` correspondiente al color de borde.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(BorderColor::Theme(Color::Primary).to_class(), "border-primary");
|
||||
/// assert_eq!(BorderColor::Subtle(Color::Warning).to_class(), "border-warning-subtle");
|
||||
/// 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 {
|
||||
Self::Theme(c) | Self::Subtle(c) => Self::BORDER_PREFIX.len() + c.as_str().len(),
|
||||
_ => Self::BORDER.len(),
|
||||
};
|
||||
let mut class = String::with_capacity(base_len + suffix.len());
|
||||
match self {
|
||||
Self::Theme(c) | Self::Subtle(c) => {
|
||||
class.push_str(Self::BORDER_PREFIX);
|
||||
class.push_str(c.as_str());
|
||||
}
|
||||
_ => class.push_str(Self::BORDER),
|
||||
}
|
||||
class.push_str(suffix);
|
||||
return class;
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Define los puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum BreakPoint {
|
||||
/// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical.
|
||||
#[default]
|
||||
None,
|
||||
/// **576px o más** - Dispositivos pequeños: teléfonos en modo horizontal.
|
||||
SM,
|
||||
/// **768px o más** - Dispositivos medianos: tabletas.
|
||||
MD,
|
||||
/// **992px o más** - Dispositivos grandes: puestos de escritorio.
|
||||
LG,
|
||||
/// **1200px o más** - Dispositivos muy grandes: puestos de escritorio grandes.
|
||||
XL,
|
||||
/// **1400px o más** - Dispositivos extragrandes: puestos de escritorio más grandes.
|
||||
XXL,
|
||||
}
|
||||
|
||||
impl BreakPoint {
|
||||
// Devuelve la identificación del punto de ruptura.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "",
|
||||
Self::SM => "sm",
|
||||
Self::MD => "md",
|
||||
Self::LG => "lg",
|
||||
Self::XL => "xl",
|
||||
Self::XXL => "xxl",
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
match self {
|
||||
Self::None => classes.push_str(prefix),
|
||||
_ => {
|
||||
classes.push_str(prefix);
|
||||
classes.push('-');
|
||||
classes.push_str(self.as_str());
|
||||
}
|
||||
}
|
||||
if !suffix.is_empty() {
|
||||
classes.push('-');
|
||||
classes.push_str(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if prefix.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let bp = self.as_str();
|
||||
let has_bp = !bp.is_empty();
|
||||
let has_suffix = !suffix.is_empty();
|
||||
|
||||
let mut len = prefix.len();
|
||||
if has_bp {
|
||||
len += 1 + bp.len();
|
||||
}
|
||||
if has_suffix {
|
||||
len += 1 + suffix.len();
|
||||
}
|
||||
let mut class = String::with_capacity(len);
|
||||
class.push_str(prefix);
|
||||
if has_bp {
|
||||
class.push('-');
|
||||
class.push_str(bp);
|
||||
}
|
||||
if has_suffix {
|
||||
class.push('-');
|
||||
class.push_str(suffix);
|
||||
}
|
||||
class
|
||||
}
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::aux::Color;
|
||||
|
||||
// **< ButtonColor >********************************************************************************
|
||||
|
||||
/// Variantes de color `btn-*` para botones.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ButtonColor {
|
||||
/// No define ninguna clase.
|
||||
#[default]
|
||||
Default,
|
||||
/// Genera internamente clases `btn-{color}` (botón relleno).
|
||||
Background(Color),
|
||||
/// Genera `btn-outline-{color}` (fondo transparente y contorno con borde).
|
||||
Outline(Color),
|
||||
/// Aplica estilo de los enlaces (`btn-link`), sin caja ni fondo, heredando el color de texto.
|
||||
Link,
|
||||
}
|
||||
|
||||
impl ButtonColor {
|
||||
const BTN_PREFIX: &str = "btn-";
|
||||
const BTN_OUTLINE_PREFIX: &str = "btn-outline-";
|
||||
const BTN_LINK: &str = "btn-link";
|
||||
|
||||
// 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 {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
match self {
|
||||
Self::Default => unreachable!(),
|
||||
Self::Background(c) => {
|
||||
classes.push_str(Self::BTN_PREFIX);
|
||||
classes.push_str(c.as_str());
|
||||
}
|
||||
Self::Outline(c) => {
|
||||
classes.push_str(Self::BTN_OUTLINE_PREFIX);
|
||||
classes.push_str(c.as_str());
|
||||
}
|
||||
Self::Link => {
|
||||
classes.push_str(Self::BTN_LINK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Devuelve la clase `btn-*` correspondiente al color del botón.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(
|
||||
/// ButtonColor::Background(Color::Primary).to_class(),
|
||||
/// "btn-primary"
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// ButtonColor::Outline(Color::Danger).to_class(),
|
||||
/// "btn-outline-danger"
|
||||
/// );
|
||||
/// 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(),
|
||||
Self::Background(c) => {
|
||||
let color = c.as_str();
|
||||
let mut class = String::with_capacity(Self::BTN_PREFIX.len() + color.len());
|
||||
class.push_str(Self::BTN_PREFIX);
|
||||
class.push_str(color);
|
||||
class
|
||||
}
|
||||
Self::Outline(c) => {
|
||||
let color = c.as_str();
|
||||
let mut class = String::with_capacity(Self::BTN_OUTLINE_PREFIX.len() + color.len());
|
||||
class.push_str(Self::BTN_OUTLINE_PREFIX);
|
||||
class.push_str(color);
|
||||
class
|
||||
}
|
||||
Self::Link => Self::BTN_LINK.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< ButtonSize >*********************************************************************************
|
||||
|
||||
/// Tamaño visual de un botón.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ButtonSize {
|
||||
/// Tamaño por defecto del tema (no añade clase).
|
||||
#[default]
|
||||
Default,
|
||||
/// Botón compacto.
|
||||
Small,
|
||||
/// Botón destacado/grande.
|
||||
Large,
|
||||
}
|
||||
|
||||
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.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Self::Default = self {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
match self {
|
||||
Self::Default => unreachable!(),
|
||||
Self::Small => classes.push_str(Self::BTN_SM),
|
||||
Self::Large => classes.push_str(Self::BTN_LG),
|
||||
}
|
||||
}
|
||||
|
||||
/// Devuelve la clase `btn-sm` o `btn-lg` correspondiente al tamaño del botón.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(ButtonSize::Small.to_class(), "btn-sm");
|
||||
/// 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(),
|
||||
Self::Small => Self::BTN_SM.to_string(),
|
||||
Self::Large => Self::BTN_LG.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,375 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Color >**************************************************************************************
|
||||
|
||||
/// Paleta de colores temáticos.
|
||||
///
|
||||
/// Equivalen a los nombres estándar definidos por Bootstrap (`primary`, `secondary`, `success`,
|
||||
/// etc.). Este tipo enumerado sirve de base para componer las clases de color para fondo
|
||||
/// ([`classes::Background`](crate::theme::classes::Background)), bordes
|
||||
/// ([`classes::Border`](crate::theme::classes::Border)) y texto
|
||||
/// ([`classes::Text`](crate::theme::classes::Text)).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Color {
|
||||
#[default]
|
||||
Primary,
|
||||
Secondary,
|
||||
Success,
|
||||
Info,
|
||||
Warning,
|
||||
Danger,
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
// Devuelve el nombre del color.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Primary => "primary",
|
||||
Self::Secondary => "secondary",
|
||||
Self::Success => "success",
|
||||
Self::Info => "info",
|
||||
Self::Warning => "warning",
|
||||
Self::Danger => "danger",
|
||||
Self::Light => "light",
|
||||
Self::Dark => "dark",
|
||||
}
|
||||
}
|
||||
|
||||
/* Añade el nombre del color a la cadena de clases (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(self.as_str());
|
||||
} */
|
||||
|
||||
/// Devuelve la clase correspondiente al color.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(Color::Primary.to_class(), "primary");
|
||||
/// assert_eq!(Color::Danger.to_class(), "danger");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
self.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Opacity >************************************************************************************
|
||||
|
||||
/// Niveles de opacidad (`opacity-*`).
|
||||
///
|
||||
/// Se usa normalmente para graduar la transparencia del color de fondo `bg-opacity-*`
|
||||
/// ([`classes::Background`](crate::theme::classes::Background)), de los bordes `border-opacity-*`
|
||||
/// ([`classes::Border`](crate::theme::classes::Border)) o del texto `text-opacity-*`
|
||||
/// ([`classes::Text`](crate::theme::classes::Text)).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Opacity {
|
||||
/// No define ninguna clase.
|
||||
#[default]
|
||||
Default,
|
||||
/// Permite generar clases `*-opacity-100` (100% de opacidad).
|
||||
Opaque,
|
||||
/// Permite generar clases `*-opacity-75` (75%).
|
||||
SemiOpaque,
|
||||
/// Permite generar clases `*-opacity-50` (50%).
|
||||
Half,
|
||||
/// Permite generar clases `*-opacity-25` (25%).
|
||||
SemiTransparent,
|
||||
/// Permite generar clases `*-opacity-10` (10%).
|
||||
AlmostTransparent,
|
||||
/// Permite generar clases `*-opacity-0` (0%, totalmente transparente).
|
||||
Transparent,
|
||||
}
|
||||
|
||||
impl Opacity {
|
||||
const OPACITY: &str = "opacity";
|
||||
const OPACITY_PREFIX: &str = "-opacity";
|
||||
|
||||
// Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Default => None,
|
||||
Self::Opaque => Some("-100"),
|
||||
Self::SemiOpaque => Some("-75"),
|
||||
Self::Half => Some("-50"),
|
||||
Self::SemiTransparent => Some("-25"),
|
||||
Self::AlmostTransparent => Some("-10"),
|
||||
Self::Transparent => Some("-0"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
if prefix.is_empty() {
|
||||
classes.push_str(Self::OPACITY);
|
||||
} else {
|
||||
classes.push_str(prefix);
|
||||
classes.push_str(Self::OPACITY_PREFIX);
|
||||
}
|
||||
classes.push_str(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = if prefix.is_empty() {
|
||||
Self::OPACITY.len()
|
||||
} else {
|
||||
prefix.len() + Self::OPACITY_PREFIX.len()
|
||||
};
|
||||
let mut class = String::with_capacity(base_len + suffix.len());
|
||||
if prefix.is_empty() {
|
||||
class.push_str(Self::OPACITY);
|
||||
} else {
|
||||
class.push_str(prefix);
|
||||
class.push_str(Self::OPACITY_PREFIX);
|
||||
}
|
||||
class.push_str(suffix);
|
||||
return class;
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Devuelve la clase de opacidad `opacity-*`.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(Opacity::Opaque.to_class(), "opacity-100");
|
||||
/// assert_eq!(Opacity::Half.to_class(), "opacity-50");
|
||||
/// assert_eq!(Opacity::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
self.class_with("")
|
||||
}
|
||||
}
|
||||
|
||||
// **< ColorBg >************************************************************************************
|
||||
|
||||
/// Colores `bg-*` para el fondo.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ColorBg {
|
||||
/// No define ninguna clase.
|
||||
#[default]
|
||||
Default,
|
||||
/// Fondo predefinido del tema (`bg-body`).
|
||||
Body,
|
||||
/// Fondo predefinido del tema (`bg-body-secondary`).
|
||||
BodySecondary,
|
||||
/// Fondo predefinido del tema (`bg-body-tertiary`).
|
||||
BodyTertiary,
|
||||
/// Genera internamente clases `bg-{color}` (p. ej., `bg-primary`).
|
||||
Theme(Color),
|
||||
/// Genera internamente clases `bg-{color}-subtle` (un tono suavizado del color).
|
||||
Subtle(Color),
|
||||
/// Color negro.
|
||||
Black,
|
||||
/// Color blanco.
|
||||
White,
|
||||
/// No aplica ningún color de fondo (`bg-transparent`).
|
||||
Transparent,
|
||||
}
|
||||
|
||||
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.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Default => None,
|
||||
Self::Body => Some("-body"),
|
||||
Self::BodySecondary => Some("-body-secondary"),
|
||||
Self::BodyTertiary => Some("-body-tertiary"),
|
||||
Self::Theme(_) => Some(""),
|
||||
Self::Subtle(_) => Some("-subtle"),
|
||||
Self::Black => Some("-black"),
|
||||
Self::White => Some("-white"),
|
||||
Self::Transparent => Some("-transparent"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
match self {
|
||||
Self::Theme(c) | Self::Subtle(c) => {
|
||||
classes.push_str(Self::BG_PREFIX);
|
||||
classes.push_str(c.as_str());
|
||||
}
|
||||
_ => classes.push_str(Self::BG),
|
||||
}
|
||||
classes.push_str(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
/// Devuelve la clase `bg-*` correspondiente al fondo.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(ColorBg::Body.to_class(), "bg-body");
|
||||
/// assert_eq!(ColorBg::Theme(Color::Primary).to_class(), "bg-primary");
|
||||
/// assert_eq!(ColorBg::Subtle(Color::Warning).to_class(), "bg-warning-subtle");
|
||||
/// 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 {
|
||||
Self::Theme(c) | Self::Subtle(c) => Self::BG_PREFIX.len() + c.as_str().len(),
|
||||
_ => Self::BG.len(),
|
||||
};
|
||||
let mut class = String::with_capacity(base_len + suffix.len());
|
||||
match self {
|
||||
Self::Theme(c) | Self::Subtle(c) => {
|
||||
class.push_str(Self::BG_PREFIX);
|
||||
class.push_str(c.as_str());
|
||||
}
|
||||
_ => class.push_str(Self::BG),
|
||||
}
|
||||
class.push_str(suffix);
|
||||
return class;
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
// **< ColorText >**********************************************************************************
|
||||
|
||||
/// Colores `text-*` para el texto.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ColorText {
|
||||
/// No define ninguna clase.
|
||||
#[default]
|
||||
Default,
|
||||
/// Color predefinido del tema (`text-body`).
|
||||
Body,
|
||||
/// Color predefinido del tema (`text-body-emphasis`).
|
||||
BodyEmphasis,
|
||||
/// Color predefinido del tema (`text-body-secondary`).
|
||||
BodySecondary,
|
||||
/// Color predefinido del tema (`text-body-tertiary`).
|
||||
BodyTertiary,
|
||||
/// Genera internamente clases `text-{color}`.
|
||||
Theme(Color),
|
||||
/// Genera internamente clases `text-{color}-emphasis` (mayor contraste acorde al tema).
|
||||
Emphasis(Color),
|
||||
/// Color negro.
|
||||
Black,
|
||||
/// Color blanco.
|
||||
White,
|
||||
}
|
||||
|
||||
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.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Default => None,
|
||||
Self::Body => Some("-body"),
|
||||
Self::BodyEmphasis => Some("-body-emphasis"),
|
||||
Self::BodySecondary => Some("-body-secondary"),
|
||||
Self::BodyTertiary => Some("-body-tertiary"),
|
||||
Self::Theme(_) => Some(""),
|
||||
Self::Emphasis(_) => Some("-emphasis"),
|
||||
Self::Black => Some("-black"),
|
||||
Self::White => Some("-white"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
match self {
|
||||
Self::Theme(c) | Self::Emphasis(c) => {
|
||||
classes.push_str(Self::TEXT_PREFIX);
|
||||
classes.push_str(c.as_str());
|
||||
}
|
||||
_ => classes.push_str(Self::TEXT),
|
||||
}
|
||||
classes.push_str(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
/// Devuelve la clase `text-*` correspondiente al color del texto.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(ColorText::Body.to_class(), "text-body");
|
||||
/// assert_eq!(ColorText::Theme(Color::Primary).to_class(), "text-primary");
|
||||
/// assert_eq!(ColorText::Emphasis(Color::Danger).to_class(), "text-danger-emphasis");
|
||||
/// 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 {
|
||||
Self::Theme(c) | Self::Emphasis(c) => Self::TEXT_PREFIX.len() + c.as_str().len(),
|
||||
_ => Self::TEXT.len(),
|
||||
};
|
||||
let mut class = String::with_capacity(base_len + suffix.len());
|
||||
match self {
|
||||
Self::Theme(c) | Self::Emphasis(c) => {
|
||||
class.push_str(Self::TEXT_PREFIX);
|
||||
class.push_str(c.as_str());
|
||||
}
|
||||
_ => class.push_str(Self::TEXT),
|
||||
}
|
||||
class.push_str(suffix);
|
||||
return class;
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< ScaleSize >**********************************************************************************
|
||||
|
||||
/// Escala discreta de tamaños para definir clases utilitarias.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ScaleSize {
|
||||
/// Sin tamaño (no define ninguna clase).
|
||||
#[default]
|
||||
None,
|
||||
/// Tamaño automático.
|
||||
Auto,
|
||||
/// Escala cero.
|
||||
Zero,
|
||||
/// Escala uno.
|
||||
One,
|
||||
/// Escala dos.
|
||||
Two,
|
||||
/// Escala tres.
|
||||
Three,
|
||||
/// Escala cuatro.
|
||||
Four,
|
||||
/// Escala cinco.
|
||||
Five,
|
||||
}
|
||||
|
||||
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.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::None => None,
|
||||
Self::Auto => Some(""),
|
||||
Self::Zero => Some("-0"),
|
||||
Self::One => Some("-1"),
|
||||
Self::Two => Some("-2"),
|
||||
Self::Three => Some("-3"),
|
||||
Self::Four => Some("-4"),
|
||||
Self::Five => Some("-5"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(prefix);
|
||||
classes.push_str(suffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
if !prefix.is_empty() {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let mut class = String::with_capacity(prefix.len() + suffix.len());
|
||||
class.push_str(prefix);
|
||||
class.push_str(suffix);
|
||||
return class;
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
} */
|
||||
}
|
||||
|
||||
// **< Side >***************************************************************************************
|
||||
|
||||
/// Lados sobre los que aplicar una clase utilitaria (respetando LTR/RTL).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Side {
|
||||
/// Todos los lados.
|
||||
#[default]
|
||||
All,
|
||||
/// Lado superior.
|
||||
Top,
|
||||
/// Lado inferior.
|
||||
Bottom,
|
||||
/// Lado lógico de inicio (respetando RTL).
|
||||
Start,
|
||||
/// Lado lógico de fin (respetando RTL).
|
||||
End,
|
||||
/// Lados lógicos laterales (abreviatura *x*).
|
||||
LeftAndRight,
|
||||
/// Lados superior e inferior (abreviatura *y*).
|
||||
TopAndBottom,
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Radio para el redondeo de esquinas ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum RoundedRadius {
|
||||
/// No define ninguna clase.
|
||||
#[default]
|
||||
None,
|
||||
/// Genera `rounded` (radio por defecto del tema).
|
||||
Default,
|
||||
/// Genera `rounded-0` (sin redondeo).
|
||||
Zero,
|
||||
/// Genera `rounded-1`.
|
||||
Scale1,
|
||||
/// Genera `rounded-2`.
|
||||
Scale2,
|
||||
/// Genera `rounded-3`.
|
||||
Scale3,
|
||||
/// Genera `rounded-4`.
|
||||
Scale4,
|
||||
/// Genera `rounded-5`.
|
||||
Scale5,
|
||||
/// Genera `rounded-circle`.
|
||||
Circle,
|
||||
/// Genera `rounded-pill`.
|
||||
Pill,
|
||||
}
|
||||
|
||||
impl RoundedRadius {
|
||||
const ROUNDED: &str = "rounded";
|
||||
|
||||
// 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> {
|
||||
match self {
|
||||
Self::None => None,
|
||||
Self::Default => Some(""),
|
||||
Self::Zero => Some("-0"),
|
||||
Self::Scale1 => Some("-1"),
|
||||
Self::Scale2 => Some("-2"),
|
||||
Self::Scale3 => Some("-3"),
|
||||
Self::Scale4 => Some("-4"),
|
||||
Self::Scale5 => Some("-5"),
|
||||
Self::Circle => Some("-circle"),
|
||||
Self::Pill => Some("-pill"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
if prefix.is_empty() {
|
||||
classes.push_str(Self::ROUNDED);
|
||||
} else {
|
||||
classes.push_str(prefix);
|
||||
}
|
||||
classes.push_str(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = if prefix.is_empty() {
|
||||
Self::ROUNDED.len()
|
||||
} else {
|
||||
prefix.len()
|
||||
};
|
||||
let mut class = String::with_capacity(base_len + suffix.len());
|
||||
if prefix.is_empty() {
|
||||
class.push_str(Self::ROUNDED);
|
||||
} else {
|
||||
class.push_str(prefix);
|
||||
}
|
||||
class.push_str(suffix);
|
||||
return class;
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Devuelve la clase `rounded-*` para el redondeo de esquinas.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(RoundedRadius::Default.to_class(), "rounded");
|
||||
/// assert_eq!(RoundedRadius::Zero.to_class(), "rounded-0");
|
||||
/// assert_eq!(RoundedRadius::Scale3.to_class(), "rounded-3");
|
||||
/// assert_eq!(RoundedRadius::Circle.to_class(), "rounded-circle");
|
||||
/// assert_eq!(RoundedRadius::None.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
self.class_with("")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
//! Conjunto de clases para aplicar en componentes del tema.
|
||||
|
||||
mod color;
|
||||
pub use color::{Background, Text};
|
||||
|
||||
mod border;
|
||||
pub use border::Border;
|
||||
|
||||
mod rounded;
|
||||
pub use rounded::Rounded;
|
||||
|
||||
mod layout;
|
||||
pub use layout::{Margin, Padding};
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::aux::{BorderColor, Opacity, ScaleSize, Side};
|
||||
|
||||
/// Clases para crear **bordes**.
|
||||
///
|
||||
/// Permite:
|
||||
///
|
||||
/// - Iniciar un borde sin tamaño inicial (`Border::default()`).
|
||||
/// - Crear un borde con tamaño por defecto (`Border::new()`).
|
||||
/// - Ajustar el tamaño de cada **lado lógico** (`side`, respetando LTR/RTL).
|
||||
/// - Definir un tamaño **global** para todo el borde (`size`).
|
||||
/// - Aplicar un **color** al borde (`BorderColor`).
|
||||
/// - Aplicar un nivel de **opacidad** (`Opacity`).
|
||||
///
|
||||
/// # Comportamiento aditivo / sustractivo
|
||||
///
|
||||
/// - **Aditivo**: basta con crear un borde sin tamaño con `classes::Border::default()` para ir
|
||||
/// añadiendo cada lado lógico con el tamaño deseado usando `ScaleSize::{One..Five}`.
|
||||
///
|
||||
/// - **Sustractivo**: se crea un borde con tamaño predefinido, p. ej. usando
|
||||
/// `classes::Border::new()` o `classes::Border::with(ScaleSize::Two)` y eliminar los lados
|
||||
/// deseados con `ScaleSize::Zero`.
|
||||
///
|
||||
/// - **Anchos diferentes por lado**: usando `ScaleSize::{Zero..Five}` en cada lado deseado.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// **Borde global:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let b = classes::Border::with(ScaleSize::Two);
|
||||
/// assert_eq!(b.to_class(), "border-2");
|
||||
/// ```
|
||||
///
|
||||
/// **Aditivo (solo borde superior):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let b = classes::Border::default().with_side(Side::Top, ScaleSize::One);
|
||||
/// assert_eq!(b.to_class(), "border-top-1");
|
||||
/// ```
|
||||
///
|
||||
/// **Sustractivo (borde global menos el superior):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let b = classes::Border::new().with_side(Side::Top, ScaleSize::Zero);
|
||||
/// assert_eq!(b.to_class(), "border border-top-0");
|
||||
/// ```
|
||||
///
|
||||
/// **Ancho por lado (lado lógico inicial a 2 y final a 4):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let b = classes::Border::default()
|
||||
/// .with_side(Side::Start, ScaleSize::Two)
|
||||
/// .with_side(Side::End, ScaleSize::Four);
|
||||
/// assert_eq!(b.to_class(), "border-end-4 border-start-2");
|
||||
/// ```
|
||||
///
|
||||
/// **Combinado (ejemplo completo):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let b = classes::Border::new() // Borde por defecto.
|
||||
/// .with_side(Side::Top, ScaleSize::Zero) // Quita borde superior.
|
||||
/// .with_side(Side::End, ScaleSize::Three) // Ancho 3 para el lado lógico final.
|
||||
/// .with_color(BorderColor::Theme(Color::Primary))
|
||||
/// .with_opacity(Opacity::Half);
|
||||
///
|
||||
/// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50");
|
||||
/// ```
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Border {
|
||||
all : ScaleSize,
|
||||
top : ScaleSize,
|
||||
end : ScaleSize,
|
||||
bottom : ScaleSize,
|
||||
start : ScaleSize,
|
||||
color : BorderColor,
|
||||
opacity: Opacity,
|
||||
}
|
||||
|
||||
impl Border {
|
||||
/// Prepara un borde del tamaño predefinido. Equivale a `border` (ancho por defecto del tema).
|
||||
pub fn new() -> Self {
|
||||
Self::with(ScaleSize::Auto)
|
||||
}
|
||||
|
||||
/// Crea un borde **con un tamaño global** (`size`).
|
||||
pub fn with(size: ScaleSize) -> Self {
|
||||
Self::default().with_side(Side::All, size)
|
||||
}
|
||||
|
||||
// **< Border BUILDER >*************************************************************************
|
||||
|
||||
pub fn with_side(mut self, side: Side, size: ScaleSize) -> Self {
|
||||
match side {
|
||||
Side::All => self.all = size,
|
||||
Side::Top => self.top = size,
|
||||
Side::Bottom => self.bottom = size,
|
||||
Side::Start => self.start = size,
|
||||
Side::End => self.end = size,
|
||||
Side::LeftAndRight => {
|
||||
self.start = size;
|
||||
self.end = size;
|
||||
}
|
||||
Side::TopAndBottom => {
|
||||
self.top = size;
|
||||
self.bottom = size;
|
||||
}
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el color del borde.
|
||||
pub fn with_color(mut self, color: BorderColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la opacidad del borde.
|
||||
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
|
||||
self.opacity = opacity;
|
||||
self
|
||||
}
|
||||
|
||||
// **< Border HELPERS >*************************************************************************
|
||||
|
||||
/// Añade las clases de borde a la cadena de clases.
|
||||
///
|
||||
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
|
||||
/// *color* y *opacidad*; respetando LTR/RTL y omitiendo las definiciones vacías.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
self.all .push_class(classes, "border");
|
||||
self.top .push_class(classes, "border-top");
|
||||
self.end .push_class(classes, "border-end");
|
||||
self.bottom .push_class(classes, "border-bottom");
|
||||
self.start .push_class(classes, "border-start");
|
||||
self.color .push_class(classes);
|
||||
self.opacity.push_class(classes, "border");
|
||||
}
|
||||
|
||||
/// Devuelve las clases de borde como cadena (`"border-2"`,
|
||||
/// `"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);
|
||||
classes
|
||||
}
|
||||
}
|
||||
|
||||
/// Atajo para crear un [`classes::Border`](crate::theme::classes::Border) a partir de un tamaño
|
||||
/// [`ScaleSize`] aplicado a todo el borde.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Convertir explícitamente con `From::from`:
|
||||
/// let b = classes::Border::from(ScaleSize::Two);
|
||||
/// assert_eq!(b.to_class(), "border-2");
|
||||
///
|
||||
/// // Convertir implícitamente con `into()`:
|
||||
/// let b: classes::Border = ScaleSize::Auto.into();
|
||||
/// assert_eq!(b.to_class(), "border");
|
||||
/// ```
|
||||
impl From<ScaleSize> for Border {
|
||||
fn from(size: ScaleSize) -> Self {
|
||||
Self::with(size)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::aux::{ColorBg, ColorText, Opacity};
|
||||
|
||||
// **< Background >*********************************************************************************
|
||||
|
||||
/// Clases para establecer **color/opacidad del fondo**.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Sin clases.
|
||||
/// let s = classes::Background::new();
|
||||
/// assert_eq!(s.to_class(), "");
|
||||
///
|
||||
/// // Sólo color de fondo.
|
||||
/// let s = classes::Background::with(ColorBg::Theme(Color::Primary));
|
||||
/// assert_eq!(s.to_class(), "bg-primary");
|
||||
///
|
||||
/// // Color más opacidad.
|
||||
/// let s = classes::Background::with(ColorBg::BodySecondary).with_opacity(Opacity::Half);
|
||||
/// assert_eq!(s.to_class(), "bg-body-secondary bg-opacity-50");
|
||||
///
|
||||
/// // Usando `From<ColorBg>`.
|
||||
/// let s: classes::Background = ColorBg::Black.into();
|
||||
/// assert_eq!(s.to_class(), "bg-black");
|
||||
///
|
||||
/// // Usando `From<(ColorBg, Opacity)>`.
|
||||
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
|
||||
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Background {
|
||||
color: ColorBg,
|
||||
opacity: Opacity,
|
||||
}
|
||||
|
||||
impl Background {
|
||||
/// Prepara un nuevo estilo para aplicar al fondo.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Crea un estilo fijando el color de fondo (`bg-*`).
|
||||
pub fn with(color: ColorBg) -> Self {
|
||||
Self::default().with_color(color)
|
||||
}
|
||||
|
||||
// **< Background BUILDER >*********************************************************************
|
||||
|
||||
/// Establece el color de fondo (`bg-*`).
|
||||
pub fn with_color(mut self, color: ColorBg) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la opacidad del fondo (`bg-opacity-*`).
|
||||
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
|
||||
self.opacity = opacity;
|
||||
self
|
||||
}
|
||||
|
||||
// **< Background HELPERS >*********************************************************************
|
||||
|
||||
/// Añade las clases de fondo a la cadena de clases.
|
||||
///
|
||||
/// Concatena, en este orden, color del fondo (`bg-*`) y opacidad (`bg-opacity-*`),
|
||||
/// omitiendo los fragmentos vacíos.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
self.color.push_class(classes);
|
||||
self.opacity.push_class(classes, "bg");
|
||||
}
|
||||
|
||||
/// 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);
|
||||
classes
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(ColorBg, Opacity)> for Background {
|
||||
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo y
|
||||
/// la opacidad.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
|
||||
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
|
||||
/// ```
|
||||
fn from((color, opacity): (ColorBg, Opacity)) -> Self {
|
||||
Background::with(color).with_opacity(opacity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorBg> for Background {
|
||||
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let s: classes::Background = ColorBg::Black.into();
|
||||
/// assert_eq!(s.to_class(), "bg-black");
|
||||
/// ```
|
||||
fn from(color: ColorBg) -> Self {
|
||||
Background::with(color)
|
||||
}
|
||||
}
|
||||
|
||||
// **< Text >***************************************************************************************
|
||||
|
||||
/// Clases para establecer **color/opacidad del texto**.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Sin clases.
|
||||
/// let s = classes::Text::new();
|
||||
/// assert_eq!(s.to_class(), "");
|
||||
///
|
||||
/// // Sólo color del texto.
|
||||
/// let s = classes::Text::with(ColorText::Theme(Color::Primary));
|
||||
/// assert_eq!(s.to_class(), "text-primary");
|
||||
///
|
||||
/// // Color del texto y opacidad.
|
||||
/// let s = classes::Text::new().with_color(ColorText::White).with_opacity(Opacity::SemiTransparent);
|
||||
/// assert_eq!(s.to_class(), "text-white text-opacity-25");
|
||||
///
|
||||
/// // Usando `From<ColorText>`.
|
||||
/// let s: classes::Text = ColorText::Black.into();
|
||||
/// assert_eq!(s.to_class(), "text-black");
|
||||
///
|
||||
/// // Usando `From<(ColorText, Opacity)>`.
|
||||
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
|
||||
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Text {
|
||||
color: ColorText,
|
||||
opacity: Opacity,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
/// Prepara un nuevo estilo para aplicar al texto.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Crea un estilo fijando el color del texto (`text-*`).
|
||||
pub fn with(color: ColorText) -> Self {
|
||||
Self::default().with_color(color)
|
||||
}
|
||||
|
||||
// **< Text BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el color del texto (`text-*`).
|
||||
pub fn with_color(mut self, color: ColorText) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la opacidad del texto (`text-opacity-*`).
|
||||
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
|
||||
self.opacity = opacity;
|
||||
self
|
||||
}
|
||||
|
||||
// **< Text HELPERS >***************************************************************************
|
||||
|
||||
/// Añade las clases de texto a la cadena de clases.
|
||||
///
|
||||
/// Concatena, en este orden, `text-*` y `text-opacity-*`, omitiendo los fragmentos vacíos.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
self.color.push_class(classes);
|
||||
self.opacity.push_class(classes, "text");
|
||||
}
|
||||
|
||||
/// Devuelve las clases de texto como cadena (`"text-primary"`, `"text-white text-opacity-25"`,
|
||||
/// 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);
|
||||
classes
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(ColorText, Opacity)> for Text {
|
||||
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
|
||||
/// texto y su opacidad.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
|
||||
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
|
||||
/// ```
|
||||
fn from((color, opacity): (ColorText, Opacity)) -> Self {
|
||||
Text::with(color).with_opacity(opacity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorText> for Text {
|
||||
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
|
||||
/// texto.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let s: classes::Text = ColorText::Black.into();
|
||||
/// assert_eq!(s.to_class(), "text-black");
|
||||
/// ```
|
||||
fn from(color: ColorText) -> Self {
|
||||
Text::with(color)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::aux::{ScaleSize, Side};
|
||||
use crate::theme::BreakPoint;
|
||||
|
||||
// **< Margin >*************************************************************************************
|
||||
|
||||
/// Clases para establecer **margin** por lado, tamaño y punto de ruptura.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let m = classes::Margin::with(Side::Top, ScaleSize::Three);
|
||||
/// assert_eq!(m.to_class(), "mt-3");
|
||||
///
|
||||
/// let m = classes::Margin::with(Side::Start, ScaleSize::Auto).with_breakpoint(BreakPoint::LG);
|
||||
/// assert_eq!(m.to_class(), "ms-lg-auto");
|
||||
///
|
||||
/// let m = classes::Margin::with(Side::All, ScaleSize::None);
|
||||
/// assert_eq!(m.to_class(), "");
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Margin {
|
||||
side: Side,
|
||||
size: ScaleSize,
|
||||
breakpoint: BreakPoint,
|
||||
}
|
||||
|
||||
impl Margin {
|
||||
/// Crea un **margin** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de
|
||||
/// ruptura.
|
||||
pub fn with(side: Side, size: ScaleSize) -> Self {
|
||||
Margin {
|
||||
side,
|
||||
size,
|
||||
breakpoint: BreakPoint::None,
|
||||
}
|
||||
}
|
||||
|
||||
// **< Margin BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el punto de ruptura a partir del cual se empieza a aplicar el **margin**.
|
||||
pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self {
|
||||
self.breakpoint = breakpoint;
|
||||
self
|
||||
}
|
||||
|
||||
// **< Margin HELPERS >*************************************************************************
|
||||
|
||||
// Devuelve el prefijo `m*` según el lado.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn side_prefix(&self) -> &'static str {
|
||||
match self.side {
|
||||
Side::All => "m",
|
||||
Side::Top => "mt",
|
||||
Side::Bottom => "mb",
|
||||
Side::Start => "ms",
|
||||
Side::End => "me",
|
||||
Side::LeftAndRight => "mx",
|
||||
Side::TopAndBottom => "my",
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
match self.size {
|
||||
ScaleSize::None => None,
|
||||
ScaleSize::Auto => Some("auto"),
|
||||
ScaleSize::Zero => Some("0"),
|
||||
ScaleSize::One => Some("1"),
|
||||
ScaleSize::Two => Some("2"),
|
||||
ScaleSize::Three => Some("3"),
|
||||
ScaleSize::Four => Some("4"),
|
||||
ScaleSize::Five => Some("5"),
|
||||
}
|
||||
}
|
||||
|
||||
/* Añade la clase de **margin** a la cadena de clases (reservado).
|
||||
//
|
||||
// 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 {
|
||||
return;
|
||||
};
|
||||
self.breakpoint
|
||||
.push_class(classes, self.side_prefix(), size);
|
||||
} */
|
||||
|
||||
/// 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();
|
||||
};
|
||||
self.breakpoint.class_with(self.side_prefix(), size)
|
||||
}
|
||||
}
|
||||
|
||||
// **< Padding >************************************************************************************
|
||||
|
||||
/// Clases para establecer **padding** por lado, tamaño y punto de ruptura.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two);
|
||||
/// assert_eq!(p.to_class(), "px-2");
|
||||
///
|
||||
/// let p = classes::Padding::with(Side::End, ScaleSize::Four).with_breakpoint(BreakPoint::SM);
|
||||
/// assert_eq!(p.to_class(), "pe-sm-4");
|
||||
///
|
||||
/// let p = classes::Padding::with(Side::All, ScaleSize::Auto);
|
||||
/// assert_eq!(p.to_class(), ""); // `Auto` no aplica a padding.
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Padding {
|
||||
side: Side,
|
||||
size: ScaleSize,
|
||||
breakpoint: BreakPoint,
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
/// Crea un **padding** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de
|
||||
/// ruptura.
|
||||
pub fn with(side: Side, size: ScaleSize) -> Self {
|
||||
Padding {
|
||||
side,
|
||||
size,
|
||||
breakpoint: BreakPoint::None,
|
||||
}
|
||||
}
|
||||
|
||||
// **< Padding BUILDER >************************************************************************
|
||||
|
||||
/// Establece el punto de ruptura a partir del cual se empieza a aplicar el **padding**.
|
||||
pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self {
|
||||
self.breakpoint = breakpoint;
|
||||
self
|
||||
}
|
||||
|
||||
// **< Padding HELPERS >************************************************************************
|
||||
|
||||
// Devuelve el prefijo `p*` según el lado.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn prefix(&self) -> &'static str {
|
||||
match self.side {
|
||||
Side::All => "p",
|
||||
Side::Top => "pt",
|
||||
Side::Bottom => "pb",
|
||||
Side::Start => "ps",
|
||||
Side::End => "pe",
|
||||
Side::LeftAndRight => "px",
|
||||
Side::TopAndBottom => "py",
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
match self.size {
|
||||
ScaleSize::None => None,
|
||||
ScaleSize::Auto => None,
|
||||
ScaleSize::Zero => Some("0"),
|
||||
ScaleSize::One => Some("1"),
|
||||
ScaleSize::Two => Some("2"),
|
||||
ScaleSize::Three => Some("3"),
|
||||
ScaleSize::Four => Some("4"),
|
||||
ScaleSize::Five => Some("5"),
|
||||
}
|
||||
}
|
||||
|
||||
/* Añade la clase de **padding** a la cadena de clases (reservado).
|
||||
//
|
||||
// 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 {
|
||||
return;
|
||||
};
|
||||
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]
|
||||
pub fn to_class(self) -> String {
|
||||
let Some(size) = self.suffix() else {
|
||||
return String::new();
|
||||
};
|
||||
self.breakpoint.class_with(self.prefix(), size)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::aux::RoundedRadius;
|
||||
|
||||
/// Clases para definir **esquinas redondeadas**.
|
||||
///
|
||||
/// Permite:
|
||||
///
|
||||
/// - Definir un radio **global para todas las esquinas** (`radius`).
|
||||
/// - Ajustar el radio asociado a las **esquinas de cada lado lógico** (`top`, `end`, `bottom`,
|
||||
/// `start`, **en este orden**, respetando LTR/RTL).
|
||||
/// - Ajustar el radio de las **esquinas concretas** (`top-start`, `top-end`, `bottom-start`,
|
||||
/// `bottom-end`, **en este orden**, respetando LTR/RTL).
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// **Radio global:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let r = classes::Rounded::with(RoundedRadius::Default);
|
||||
/// assert_eq!(r.to_class(), "rounded");
|
||||
/// ```
|
||||
///
|
||||
/// **Sin redondeo:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let r = classes::Rounded::new();
|
||||
/// assert_eq!(r.to_class(), "");
|
||||
/// ```
|
||||
///
|
||||
/// **Radio en las esquinas de un lado lógico:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2);
|
||||
/// assert_eq!(r.to_class(), "rounded-end-2");
|
||||
/// ```
|
||||
///
|
||||
/// **Radio en una esquina concreta:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3);
|
||||
/// assert_eq!(r.to_class(), "rounded-top-start-3");
|
||||
/// ```
|
||||
///
|
||||
/// **Combinado (ejemplo completo):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let r = classes::Rounded::new()
|
||||
/// .with_top(RoundedRadius::Default) // Añade redondeo arriba.
|
||||
/// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
|
||||
/// .with_bottom_end(RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
|
||||
///
|
||||
/// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
|
||||
/// ```
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Rounded {
|
||||
radius : RoundedRadius,
|
||||
top : RoundedRadius,
|
||||
end : RoundedRadius,
|
||||
bottom : RoundedRadius,
|
||||
start : RoundedRadius,
|
||||
top_start : RoundedRadius,
|
||||
top_end : RoundedRadius,
|
||||
bottom_start: RoundedRadius,
|
||||
bottom_end : RoundedRadius,
|
||||
}
|
||||
|
||||
impl Rounded {
|
||||
/// Prepara las esquinas **sin redondeo global** de partida.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Crea las esquinas **con redondeo global** (`radius`).
|
||||
pub fn with(radius: RoundedRadius) -> Self {
|
||||
Self::default().with_radius(radius)
|
||||
}
|
||||
|
||||
// **< Rounded BUILDER >************************************************************************
|
||||
|
||||
/// Establece el radio global de las esquinas (`rounded*`).
|
||||
pub fn with_radius(mut self, radius: RoundedRadius) -> Self {
|
||||
self.radius = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el radio en las esquinas del lado superior (`rounded-top-*`).
|
||||
pub fn with_top(mut self, radius: RoundedRadius) -> Self {
|
||||
self.top = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el radio en las esquinas del lado lógico final (`rounded-end-*`). Respeta LTR/RTL.
|
||||
pub fn with_end(mut self, radius: RoundedRadius) -> Self {
|
||||
self.end = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el radio en las esquinas del lado inferior (`rounded-bottom-*`).
|
||||
pub fn with_bottom(mut self, radius: RoundedRadius) -> Self {
|
||||
self.bottom = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el radio en las esquinas del lado lógico inicial (`rounded-start-*`). Respeta
|
||||
/// LTR/RTL.
|
||||
pub fn with_start(mut self, radius: RoundedRadius) -> Self {
|
||||
self.start = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el radio en la esquina superior-inicial (`rounded-top-start-*`). Respeta LTR/RTL.
|
||||
pub fn with_top_start(mut self, radius: RoundedRadius) -> Self {
|
||||
self.top_start = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el radio en la esquina superior-final (`rounded-top-end-*`). Respeta LTR/RTL.
|
||||
pub fn with_top_end(mut self, radius: RoundedRadius) -> Self {
|
||||
self.top_end = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el radio en la esquina inferior-inicial (`rounded-bottom-start-*`). Respeta
|
||||
/// LTR/RTL.
|
||||
pub fn with_bottom_start(mut self, radius: RoundedRadius) -> Self {
|
||||
self.bottom_start = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el radio en la esquina inferior-final (`rounded-bottom-end-*`). Respeta LTR/RTL.
|
||||
pub fn with_bottom_end(mut self, radius: RoundedRadius) -> Self {
|
||||
self.bottom_end = radius;
|
||||
self
|
||||
}
|
||||
|
||||
// **< Rounded HELPERS >************************************************************************
|
||||
|
||||
/// Añade las clases de redondeo a la cadena de clases.
|
||||
///
|
||||
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
|
||||
/// `top-start`, `top-end`, `bottom-start` y `bottom-end`; respetando LTR/RTL y omitiendo las
|
||||
/// definiciones vacías.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
self.radius .push_class(classes, "");
|
||||
self.top .push_class(classes, "rounded-top");
|
||||
self.end .push_class(classes, "rounded-end");
|
||||
self.bottom .push_class(classes, "rounded-bottom");
|
||||
self.start .push_class(classes, "rounded-start");
|
||||
self.top_start .push_class(classes, "rounded-top-start");
|
||||
self.top_end .push_class(classes, "rounded-top-end");
|
||||
self.bottom_start.push_class(classes, "rounded-bottom-start");
|
||||
self.bottom_end .push_class(classes, "rounded-bottom-end");
|
||||
}
|
||||
|
||||
/// Devuelve las clases de redondeo como cadena (`"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);
|
||||
classes
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
//! Definiciones para crear contenedores de componentes ([`Container`]).
|
||||
//!
|
||||
//! Cada contenedor envuelve contenido usando la etiqueta semántica indicada por
|
||||
//! [`container::Kind`](crate::theme::container::Kind).
|
||||
//!
|
||||
//! Con [`container::Width`](crate::theme::container::Width) se puede definir el ancho y el
|
||||
//! comportamiento *responsive* del contenedor. También permite aplicar utilidades de estilo para el
|
||||
//! fondo, texto, borde o esquinas redondeadas.
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let main = Container::main()
|
||||
//! .with_id("main-page")
|
||||
//! .with_width(container::Width::From(BreakPoint::LG));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Kind, Width};
|
||||
|
||||
mod component;
|
||||
pub use component::Container;
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Componente para crear un **contenedor de componentes**.
|
||||
///
|
||||
/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza
|
||||
/// si existen componentes hijos (*children*).
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Container {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
container_kind : container::Kind,
|
||||
container_width: container::Width,
|
||||
children : Children,
|
||||
}
|
||||
|
||||
impl Component for Container {
|
||||
fn new() -> Self {
|
||||
Container::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, self.width().to_class());
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let output = self.children().render(cx);
|
||||
if output.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
let style = match self.width() {
|
||||
container::Width::FluidMax(w) if w.is_measurable() => {
|
||||
Some(join!("max-width: ", w.to_string(), ";"))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match self.container_kind() {
|
||||
container::Kind::Default => PrepareMarkup::With(html! {
|
||||
div id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Main => PrepareMarkup::With(html! {
|
||||
main id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Header => PrepareMarkup::With(html! {
|
||||
header id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Footer => PrepareMarkup::With(html! {
|
||||
footer id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Section => PrepareMarkup::With(html! {
|
||||
section id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Article => PrepareMarkup::With(html! {
|
||||
article id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
/// Crea un contenedor de tipo `Main` (`<main>`).
|
||||
pub fn main() -> Self {
|
||||
Container {
|
||||
container_kind: container::Kind::Main,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un contenedor de tipo `Header` (`<header>`).
|
||||
pub fn header() -> Self {
|
||||
Container {
|
||||
container_kind: container::Kind::Header,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un contenedor de tipo `Footer` (`<footer>`).
|
||||
pub fn footer() -> Self {
|
||||
Container {
|
||||
container_kind: container::Kind::Footer,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un contenedor de tipo `Section` (`<section>`).
|
||||
pub fn section() -> Self {
|
||||
Container {
|
||||
container_kind: container::Kind::Section,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un contenedor de tipo `Article` (`<article>`).
|
||||
pub fn article() -> Self {
|
||||
Container {
|
||||
container_kind: container::Kind::Article,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Container BUILDER >**********************************************************************
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor.
|
||||
///
|
||||
/// También acepta clases predefinidas para:
|
||||
///
|
||||
/// - Modificar el color de fondo ([`classes::Background`]).
|
||||
/// - Definir la apariencia del texto ([`classes::Text`]).
|
||||
/// - Establecer bordes ([`classes::Border`]).
|
||||
/// - 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
|
||||
}
|
||||
|
||||
/// Establece el comportamiento del ancho para el contenedor.
|
||||
#[builder_fn]
|
||||
pub fn with_width(mut self, width: container::Width) -> Self {
|
||||
self.container_width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente hijo al contenedor.
|
||||
#[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
|
||||
}
|
||||
|
||||
// **< 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::aux::BreakPoint;
|
||||
|
||||
// **< Kind >***************************************************************************************
|
||||
|
||||
/// Tipo de contenedor ([`Container`](crate::theme::Container)).
|
||||
///
|
||||
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
|
||||
/// común a todos los contenedores.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Kind {
|
||||
/// Contenedor genérico (`<div>`).
|
||||
#[default]
|
||||
Default,
|
||||
/// Contenido principal de la página (`<main>`).
|
||||
Main,
|
||||
/// Encabezado de la página o de sección (`<header>`).
|
||||
Header,
|
||||
/// Pie de la página o de sección (`<footer>`).
|
||||
Footer,
|
||||
/// Sección de contenido (`<section>`).
|
||||
Section,
|
||||
/// Artículo de contenido (`<article>`).
|
||||
Article,
|
||||
}
|
||||
|
||||
// **< Width >**************************************************************************************
|
||||
|
||||
/// Define cómo se comporta el ancho de un contenedor ([`Container`](crate::theme::Container)).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Width {
|
||||
/// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de
|
||||
/// ruptura. Por debajo del menor punto de ruptura ocupa el 100% del ancho disponible.
|
||||
#[default]
|
||||
Default,
|
||||
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
|
||||
/// ese punto de ruptura ocupa el 100% del ancho disponible.
|
||||
From(BreakPoint),
|
||||
/// Ocupa el 100% del ancho disponible siempre.
|
||||
Fluid,
|
||||
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
|
||||
FluidMax(UnitValue),
|
||||
}
|
||||
|
||||
impl Width {
|
||||
const CONTAINER: &str = "container";
|
||||
|
||||
/* Añade el comportamiento del contenedor a la cadena de clases según ancho (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
match self {
|
||||
Self::Default => BreakPoint::None.push_class(classes, Self::CONTAINER, ""),
|
||||
Self::From(bp) => bp.push_class(classes, Self::CONTAINER, ""),
|
||||
Self::Fluid | Self::FluidMax(_) => {
|
||||
BreakPoint::None.push_class(classes, Self::CONTAINER, "fluid")
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
/// 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, ""),
|
||||
Self::From(bp) => bp.class_with(Self::CONTAINER, ""),
|
||||
Self::Fluid | Self::FluidMax(_) => {
|
||||
BreakPoint::None.class_with(Self::CONTAINER, "fluid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
//! Definiciones para crear menús desplegables [`Dropdown`].
|
||||
//!
|
||||
//! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del
|
||||
//! desplegable [`Dropdown`], con distintos comportamientos según su finalidad, como enlaces de
|
||||
//! navegación, botones de acción, encabezados o divisores visuales.
|
||||
//!
|
||||
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
||||
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let dd = Dropdown::new()
|
||||
//! .with_title(L10n::n("Menu"))
|
||||
//! .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::divider())
|
||||
//! .add_item(dropdown::Item::header(L10n::n("User session")))
|
||||
//! .add_item(dropdown::Item::button(L10n::n("Sign out")));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};
|
||||
|
||||
mod component;
|
||||
pub use component::Dropdown;
|
||||
|
||||
mod item;
|
||||
pub use item::{Item, ItemKind};
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
/// Componente para crear un **menú desplegable**.
|
||||
///
|
||||
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split))
|
||||
/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra/oculta según la
|
||||
/// interacción del usuario. Admite variaciones de tamaño/color del botón, también dirección de
|
||||
/// apertura, alineación o política de cierre.
|
||||
///
|
||||
/// Si no tiene título (ver [`with_title()`](Self::with_title)) se muestra únicamente la lista de
|
||||
/// elementos sin ningún botón para interactuar.
|
||||
///
|
||||
/// Si este componente se usa en un menú [`Nav`] (ver [`nav::Item::dropdown()`]) sólo se tendrán en
|
||||
/// cuenta **el título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el
|
||||
/// resto de propiedades no afectarán a su representación en [`Nav`].
|
||||
///
|
||||
/// Ver ejemplo en el módulo [`dropdown`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Dropdown {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
title : L10n,
|
||||
button_size : ButtonSize,
|
||||
button_color : ButtonColor,
|
||||
button_split : bool,
|
||||
button_grouped: bool,
|
||||
auto_close : dropdown::AutoClose,
|
||||
direction : dropdown::Direction,
|
||||
menu_align : dropdown::MenuAlign,
|
||||
menu_position : dropdown::MenuPosition,
|
||||
items : Children,
|
||||
}
|
||||
|
||||
impl Component for Dropdown {
|
||||
fn new() -> Self {
|
||||
Dropdown::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(
|
||||
ClassesOp::Prepend,
|
||||
self.direction().class_with(self.button_grouped()),
|
||||
);
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
// Si no hay elementos en el menú, no se prepara.
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
|
||||
// Título opcional para el menú desplegable.
|
||||
let title = self.title().using(cx);
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
div id=[self.id()] class=[self.classes().get()] {
|
||||
@if !title.is_empty() {
|
||||
@let mut btn_classes = AttrClasses::new({
|
||||
let mut classes = "btn".to_string();
|
||||
self.button_size().push_class(&mut classes);
|
||||
self.button_color().push_class(&mut classes);
|
||||
classes
|
||||
});
|
||||
@let pos = self.menu_position();
|
||||
@let offset = pos.data_offset();
|
||||
@let reference = pos.data_reference();
|
||||
@let auto_close = self.auto_close.as_str();
|
||||
@let menu_classes = AttrClasses::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() {
|
||||
// Botón principal (acción/etiqueta).
|
||||
@let btn = html! {
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.get()]
|
||||
{
|
||||
(title)
|
||||
}
|
||||
};
|
||||
// Botón *toggle* que abre/cierra el menú asociado.
|
||||
@let btn_toggle = html! {
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.alter_value(
|
||||
ClassesOp::Add, "dropdown-toggle dropdown-toggle-split"
|
||||
).get()]
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-offset=[offset]
|
||||
data-bs-reference=[reference]
|
||||
data-bs-auto-close=[auto_close]
|
||||
aria-expanded="false"
|
||||
{
|
||||
span class="visually-hidden" {
|
||||
(L10n::t("dropdown_toggle", &LOCALES_BOOTSIER).using(cx))
|
||||
}
|
||||
}
|
||||
};
|
||||
// Orden según dirección (en `dropstart` el *toggle* se sitúa antes).
|
||||
@match self.direction() {
|
||||
dropdown::Direction::Dropstart => {
|
||||
(btn_toggle)
|
||||
ul class=[menu_classes.get()] { (items) }
|
||||
(btn)
|
||||
}
|
||||
_ => {
|
||||
(btn)
|
||||
(btn_toggle)
|
||||
ul class=[menu_classes.get()] { (items) }
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
// Botón único con funcionalidad de *toggle*.
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.alter_value(
|
||||
ClassesOp::Add, "dropdown-toggle"
|
||||
).get()]
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-offset=[offset]
|
||||
data-bs-reference=[reference]
|
||||
data-bs-auto-close=[auto_close]
|
||||
aria-expanded="false"
|
||||
{
|
||||
(title)
|
||||
}
|
||||
ul class=[menu_classes.get()] { (items) }
|
||||
}
|
||||
} @else {
|
||||
// Sin botón: sólo el listado como menú contextual.
|
||||
ul class="dropdown-menu" { (items) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Dropdown {
|
||||
// **< Dropdown BUILDER >***********************************************************************
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Establece el título del menú desplegable.
|
||||
#[builder_fn]
|
||||
pub fn with_title(mut self, title: L10n) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
/// Ajusta el tamaño del botón.
|
||||
#[builder_fn]
|
||||
pub fn with_button_size(mut self, size: ButtonSize) -> Self {
|
||||
self.button_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Define el color/estilo del botón.
|
||||
#[builder_fn]
|
||||
pub fn with_button_color(mut self, color: ButtonColor) -> Self {
|
||||
self.button_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Activa/desactiva el modo *split* (botón de acción + *toggle*).
|
||||
#[builder_fn]
|
||||
pub fn with_button_split(mut self, split: bool) -> Self {
|
||||
self.button_split = split;
|
||||
self
|
||||
}
|
||||
|
||||
/// Indica si el botón del menú está integrado en un grupo de botones.
|
||||
#[builder_fn]
|
||||
pub fn with_button_grouped(mut self, grouped: bool) -> Self {
|
||||
self.button_grouped = grouped;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la política de cierre automático del menú desplegable.
|
||||
#[builder_fn]
|
||||
pub fn with_auto_close(mut self, auto_close: dropdown::AutoClose) -> Self {
|
||||
self.auto_close = auto_close;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la dirección de despliegue del menú.
|
||||
#[builder_fn]
|
||||
pub fn with_direction(mut self, direction: dropdown::Direction) -> Self {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configura la alineación horizontal (con posible comportamiento *responsive* adicional).
|
||||
#[builder_fn]
|
||||
pub fn with_menu_align(mut self, align: dropdown::MenuAlign) -> Self {
|
||||
self.menu_align = align;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configura la posición del menú.
|
||||
#[builder_fn]
|
||||
pub fn with_menu_position(mut self, position: dropdown::MenuPosition) -> Self {
|
||||
self.menu_position = position;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo elemento hijo al menú.
|
||||
#[inline]
|
||||
pub fn add_item(mut self, item: dropdown::Item) -> Self {
|
||||
self.items.add(Child::with(item));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< ItemKind >***********************************************************************************
|
||||
|
||||
/// Tipos de [`dropdown::Item`](crate::theme::dropdown::Item) disponibles en un menú desplegable
|
||||
/// [`Dropdown`](crate::theme::Dropdown).
|
||||
///
|
||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||
/// con él.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum ItemKind {
|
||||
/// Elemento vacío, no produce salida.
|
||||
#[default]
|
||||
Void,
|
||||
/// Etiqueta sin comportamiento interactivo.
|
||||
Label(L10n),
|
||||
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
|
||||
/// inicialmente deshabilitado.
|
||||
Link {
|
||||
label: L10n,
|
||||
path: FnPathByContext,
|
||||
blank: bool,
|
||||
disabled: bool,
|
||||
},
|
||||
/// Acción ejecutable en la propia página, sin navegación asociada. Inicialmente puede estar
|
||||
/// deshabilitado.
|
||||
Button { label: L10n, disabled: bool },
|
||||
/// Título o encabezado que separa grupos de opciones.
|
||||
Header(L10n),
|
||||
/// Separador visual entre bloques de elementos.
|
||||
Divider,
|
||||
}
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Representa un **elemento individual** de un menú desplegable
|
||||
/// [`Dropdown`](crate::theme::Dropdown).
|
||||
///
|
||||
/// Cada instancia de [`dropdown::Item`](crate::theme::dropdown::Item) se traduce en un componente
|
||||
/// 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)]
|
||||
pub struct Item {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
item_kind: ItemKind,
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Item::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.item_kind() {
|
||||
ItemKind::Void => PrepareMarkup::None,
|
||||
|
||||
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
span class="dropdown-item-text" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let path = path(cx);
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = !*disabled && (current_path == Some(path));
|
||||
|
||||
let mut classes = "dropdown-item".to_string();
|
||||
if is_current {
|
||||
classes.push_str(" active");
|
||||
}
|
||||
if *disabled {
|
||||
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 aria_current = (href.is_some() && is_current).then_some("page");
|
||||
let aria_disabled = disabled.then_some("true");
|
||||
let tabindex = disabled.then_some("-1");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
a
|
||||
class=(classes)
|
||||
href=[href]
|
||||
target=[target]
|
||||
rel=[rel]
|
||||
aria-current=[aria_current]
|
||||
aria-disabled=[aria_disabled]
|
||||
tabindex=[tabindex]
|
||||
{
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ItemKind::Button { label, disabled } => {
|
||||
let mut classes = "dropdown-item".to_string();
|
||||
if *disabled {
|
||||
classes.push_str(" disabled");
|
||||
}
|
||||
|
||||
let aria_disabled = disabled.then_some("true");
|
||||
let disabled_attr = disabled.then_some("disabled");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
button
|
||||
class=(classes)
|
||||
type="button"
|
||||
aria-disabled=[aria_disabled]
|
||||
disabled=[disabled_attr]
|
||||
{
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ItemKind::Header(label) => PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
h6 class="dropdown-header" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
ItemKind::Divider => PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] { hr class="dropdown-divider" {} }
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||
pub fn label(label: L10n) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: false,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
||||
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: true,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: true,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un botón de acción local, sin navegación asociada.
|
||||
pub fn button(label: L10n) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Button {
|
||||
label,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un botón deshabilitado.
|
||||
pub fn button_disabled(label: L10n) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Button {
|
||||
label,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un encabezado para un grupo de elementos dentro del menú.
|
||||
pub fn header(label: L10n) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Header(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un separador visual entre bloques de elementos.
|
||||
pub fn divider() -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Divider,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
// **< 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
// **< AutoClose >**********************************************************************************
|
||||
|
||||
/// Estrategia para el cierre automático de un menú [`Dropdown`].
|
||||
///
|
||||
/// Define cuándo se cierra el menú desplegado según la interacción del usuario.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum AutoClose {
|
||||
/// Comportamiento por defecto, se cierra con clics dentro y fuera del menú, o pulsando `Esc`.
|
||||
#[default]
|
||||
Default,
|
||||
/// Sólo se cierra con clics dentro del menú.
|
||||
ClickableInside,
|
||||
/// Sólo se cierra con clics fuera del menú.
|
||||
ClickableOutside,
|
||||
/// Cierre manual, no se cierra con clics; sólo al pulsar nuevamente el botón del menú
|
||||
/// (*toggle*), o pulsando `Esc`.
|
||||
ManualClose,
|
||||
}
|
||||
|
||||
impl AutoClose {
|
||||
// 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> {
|
||||
match self {
|
||||
Self::Default => None,
|
||||
Self::ClickableInside => Some("inside"),
|
||||
Self::ClickableOutside => Some("outside"),
|
||||
Self::ManualClose => Some("false"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Direction >**********************************************************************************
|
||||
|
||||
/// Dirección de despliegue de un menú [`Dropdown`].
|
||||
///
|
||||
/// Controla desde qué posición se muestra el menú respecto al botón.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Direction {
|
||||
/// Comportamiento por defecto (despliega el menú hacia abajo desde la posición inicial,
|
||||
/// respetando LTR/RTL).
|
||||
#[default]
|
||||
Default,
|
||||
/// Centra horizontalmente el menú respecto al botón.
|
||||
Centered,
|
||||
/// Despliega el menú hacia arriba.
|
||||
Dropup,
|
||||
/// Despliega el menú hacia arriba y centrado.
|
||||
DropupCentered,
|
||||
/// Despliega el menú desde el lateral final, respetando LTR/RTL.
|
||||
Dropend,
|
||||
/// Despliega el menú desde el lateral inicial, respetando LTR/RTL.
|
||||
Dropstart,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
// 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 {
|
||||
match self {
|
||||
Self::Default if grouped => "",
|
||||
Self::Default => "dropdown",
|
||||
Self::Centered => "dropdown-center",
|
||||
Self::Dropup => "dropup",
|
||||
Self::DropupCentered => "dropup-center",
|
||||
Self::Dropend => "dropend",
|
||||
Self::Dropstart => "dropstart",
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str("btn-group");
|
||||
}
|
||||
let class = self.as_str(grouped);
|
||||
if !class.is_empty() {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes, grouped);
|
||||
classes
|
||||
}
|
||||
}
|
||||
|
||||
// **< MenuAlign >**********************************************************************************
|
||||
|
||||
/// Alineación horizontal del menú desplegable [`Dropdown`].
|
||||
///
|
||||
/// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una
|
||||
/// alineación diferente a partir de un punto de ruptura ([`BreakPoint`]).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum MenuAlign {
|
||||
/// Alineación al inicio (comportamiento por defecto).
|
||||
#[default]
|
||||
Start,
|
||||
/// Alineación al inicio a partir del punto de ruptura indicado.
|
||||
StartAt(BreakPoint),
|
||||
/// Alineación al inicio por defecto, y al final a partir de un punto de ruptura válido.
|
||||
StartAndEnd(BreakPoint),
|
||||
/// Alineación al final.
|
||||
End,
|
||||
/// Alineación al final a partir del punto de ruptura indicado.
|
||||
EndAt(BreakPoint),
|
||||
/// Alineación al final por defecto, y al inicio a partir de un punto de ruptura válido.
|
||||
EndAndStart(BreakPoint),
|
||||
}
|
||||
|
||||
impl MenuAlign {
|
||||
#[inline]
|
||||
fn push_one(classes: &mut String, class: &str) {
|
||||
if class.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Alineación por defecto (start), no añade clases extra.
|
||||
Self::Start => {}
|
||||
|
||||
// `dropdown-menu-{bp}-start`
|
||||
Self::StartAt(bp) => {
|
||||
let class = bp.class_with("dropdown-menu", "start");
|
||||
Self::push_one(classes, &class);
|
||||
}
|
||||
|
||||
// `dropdown-menu-start` + `dropdown-menu-{bp}-end`
|
||||
Self::StartAndEnd(bp) => {
|
||||
Self::push_one(classes, "dropdown-menu-start");
|
||||
let bp_class = bp.class_with("dropdown-menu", "end");
|
||||
Self::push_one(classes, &bp_class);
|
||||
}
|
||||
|
||||
// `dropdown-menu-end`
|
||||
Self::End => {
|
||||
Self::push_one(classes, "dropdown-menu-end");
|
||||
}
|
||||
|
||||
// `dropdown-menu-{bp}-end`
|
||||
Self::EndAt(bp) => {
|
||||
let class = bp.class_with("dropdown-menu", "end");
|
||||
Self::push_one(classes, &class);
|
||||
}
|
||||
|
||||
// `dropdown-menu-end` + `dropdown-menu-{bp}-start`
|
||||
Self::EndAndStart(bp) => {
|
||||
Self::push_one(classes, "dropdown-menu-end");
|
||||
let bp_class = bp.class_with("dropdown-menu", "start");
|
||||
Self::push_one(classes, &bp_class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Devuelve las clases de alineación sin incluir `dropdown-menu` (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
classes
|
||||
} */
|
||||
}
|
||||
|
||||
// **< MenuPosition >*******************************************************************************
|
||||
|
||||
/// Posición relativa del menú desplegable [`Dropdown`].
|
||||
///
|
||||
/// Permite indicar un desplazamiento (*offset*) manual o referenciar al elemento padre para el
|
||||
/// cálculo de la posición.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum MenuPosition {
|
||||
/// Posicionamiento automático por defecto.
|
||||
#[default]
|
||||
Default,
|
||||
/// Desplazamiento manual en píxeles `(x, y)` aplicado al menú. Se admiten valores negativos.
|
||||
Offset(i8, i8),
|
||||
/// Posiciona el menú tomando como referencia el botón padre. Especialmente útil cuando
|
||||
/// [`button_split()`](crate::theme::Dropdown::button_split) es `true`.
|
||||
Parent,
|
||||
}
|
||||
|
||||
impl MenuPosition {
|
||||
// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
|
||||
#[inline]
|
||||
pub(crate) fn data_offset(self) -> Option<String> {
|
||||
match self {
|
||||
Self::Offset(x, y) => Some(format!("{x},{y}")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
|
||||
#[inline]
|
||||
pub(crate) fn data_reference(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Parent => Some("parent"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
pub enum IconKind {
|
||||
#[default]
|
||||
None,
|
||||
Font(FontSize),
|
||||
Svg {
|
||||
shapes: Markup,
|
||||
viewbox: AttrValue,
|
||||
},
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Icon {
|
||||
classes : AttrClasses,
|
||||
icon_kind : IconKind,
|
||||
aria_label: AttrL10n,
|
||||
}
|
||||
|
||||
impl Component for Icon {
|
||||
fn new() -> Self {
|
||||
Icon::default()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
if !matches!(self.icon_kind(), IconKind::None) {
|
||||
self.alter_classes(ClassesOp::Prepend, "icon");
|
||||
}
|
||||
if let IconKind::Font(font_size) = self.icon_kind() {
|
||||
self.alter_classes(ClassesOp::Add, font_size.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.icon_kind() {
|
||||
IconKind::None => PrepareMarkup::None,
|
||||
IconKind::Font(_) => {
|
||||
let aria_label = self.aria_label().lookup(cx);
|
||||
let has_label = aria_label.is_some();
|
||||
PrepareMarkup::With(html! {
|
||||
i
|
||||
class=[self.classes().get()]
|
||||
role=[has_label.then_some("img")]
|
||||
aria-label=[aria_label]
|
||||
aria-hidden=[(!has_label).then_some("true")]
|
||||
{}
|
||||
})
|
||||
}
|
||||
IconKind::Svg { shapes, viewbox } => {
|
||||
let aria_label = self.aria_label().lookup(cx);
|
||||
let has_label = aria_label.is_some();
|
||||
let viewbox = viewbox.get().unwrap_or_else(|| DEFAULT_VIEWBOX.to_string());
|
||||
PrepareMarkup::With(html! {
|
||||
svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox=(viewbox)
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
class=[self.classes().get()]
|
||||
role=[has_label.then_some("img")]
|
||||
aria-label=[aria_label]
|
||||
aria-hidden=[(!has_label).then_some("true")]
|
||||
{
|
||||
(shapes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn font() -> Self {
|
||||
Icon::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))
|
||||
}
|
||||
|
||||
pub fn svg(shapes: Markup) -> Self {
|
||||
Icon::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 {
|
||||
shapes,
|
||||
viewbox: AttrValue::new(viewbox),
|
||||
})
|
||||
}
|
||||
|
||||
// **< Icon BUILDER >***************************************************************************
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al icono.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_icon_kind(mut self, icon_kind: IconKind) -> Self {
|
||||
self.icon_kind = icon_kind;
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_aria_label(mut self, label: L10n) -> Self {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
//! Definiciones para renderizar imágenes ([`Image`]).
|
||||
|
||||
mod props;
|
||||
pub use props::{Size, Source};
|
||||
|
||||
mod component;
|
||||
pub use component::Image;
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Componente para renderizar una **imagen**.
|
||||
///
|
||||
/// - Ajusta su disposición según el origen definido en [`image::Source`].
|
||||
/// - Permite configurar **dimensiones** ([`with_size()`](Self::with_size)), **borde**
|
||||
/// ([`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)]
|
||||
pub struct Image {
|
||||
id : AttrId,
|
||||
classes: AttrClasses,
|
||||
size : image::Size,
|
||||
source : image::Source,
|
||||
alt : AttrL10n,
|
||||
}
|
||||
|
||||
impl Component for Image {
|
||||
fn new() -> Self {
|
||||
Image::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, self.source().to_class());
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let dimensions = self.size().to_style();
|
||||
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
|
||||
let is_decorative = alt_text.is_empty();
|
||||
let source = match self.source() {
|
||||
image::Source::Logo(logo) => {
|
||||
return PrepareMarkup::With(html! {
|
||||
span
|
||||
id=[self.id()]
|
||||
class=[self.classes().get()]
|
||||
style=[dimensions]
|
||||
role=[(!is_decorative).then_some("img")]
|
||||
aria-label=[(!is_decorative).then_some(alt_text)]
|
||||
aria-hidden=[is_decorative.then_some("true")]
|
||||
{
|
||||
(logo.render(cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
image::Source::Responsive(source) => Some(source),
|
||||
image::Source::Thumbnail(source) => Some(source),
|
||||
image::Source::Plain(source) => Some(source),
|
||||
};
|
||||
PrepareMarkup::With(html! {
|
||||
img
|
||||
src=[source]
|
||||
alt=(alt_text)
|
||||
id=[self.id()]
|
||||
class=[self.classes().get()]
|
||||
style=[dimensions] {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// Crea rápidamente una imagen especificando su origen.
|
||||
pub fn with(source: image::Source) -> Self {
|
||||
Image::default().with_source(source)
|
||||
}
|
||||
|
||||
// **< Image BUILDER >**************************************************************************
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas a la imagen.
|
||||
///
|
||||
/// También acepta clases predefinidas para:
|
||||
///
|
||||
/// - Establecer bordes ([`classes::Border`]).
|
||||
/// - 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
|
||||
}
|
||||
|
||||
/// Define las dimensiones de la imagen (auto, ancho/alto, ambos).
|
||||
#[builder_fn]
|
||||
pub fn with_size(mut self, size: image::Size) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el origen de la imagen, influyendo en su disposición en el contenido.
|
||||
#[builder_fn]
|
||||
pub fn with_source(mut self, source: image::Source) -> Self {
|
||||
self.source = source;
|
||||
self
|
||||
}
|
||||
|
||||
/// Define el texto alternativo localizado ([`L10n`]) 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
|
||||
}
|
||||
|
||||
// **< 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Size >***************************************************************************************
|
||||
|
||||
/// Define las **dimensiones** de una imagen ([`Image`](crate::theme::Image)).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Size {
|
||||
/// Ajuste automático por defecto.
|
||||
///
|
||||
/// La imagen usa su tamaño natural o se ajusta al contenedor donde se publica.
|
||||
#[default]
|
||||
Auto,
|
||||
/// Establece explícitamente el **ancho y alto** de la imagen.
|
||||
///
|
||||
/// Útil cuando se desea fijar ambas dimensiones de forma exacta. Ten en cuenta que la imagen
|
||||
/// puede distorsionarse si no se mantiene la proporción original.
|
||||
Dimensions(UnitValue, UnitValue),
|
||||
/// Establece sólo el **ancho** de la imagen.
|
||||
///
|
||||
/// La altura se ajusta proporcionalmente de manera automática.
|
||||
Width(UnitValue),
|
||||
/// Establece sólo la **altura** de la imagen.
|
||||
///
|
||||
/// El ancho se ajusta proporcionalmente de manera automática.
|
||||
Height(UnitValue),
|
||||
/// Establece **el mismo valor** para el ancho y el alto de la imagen.
|
||||
///
|
||||
/// Práctico para forzar rápidamente un área cuadrada. Ten en cuenta que la imagen puede
|
||||
/// distorsionarse si la original no es cuadrada.
|
||||
Both(UnitValue),
|
||||
}
|
||||
|
||||
impl Size {
|
||||
// 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 {
|
||||
Self::Auto => None,
|
||||
Self::Dimensions(w, h) => Some(format!("width: {w}; height: {h};")),
|
||||
Self::Width(w) => Some(format!("width: {w};")),
|
||||
Self::Height(h) => Some(format!("height: {h};")),
|
||||
Self::Both(v) => Some(format!("width: {v}; height: {v};")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Source >*************************************************************************************
|
||||
|
||||
/// Especifica la **fuente** para publicar una imagen ([`Image`](crate::theme::Image)).
|
||||
#[derive(AutoDefault, Clone, Debug, PartialEq)]
|
||||
pub enum Source {
|
||||
/// Imagen con el logotipo de PageTop.
|
||||
#[default]
|
||||
Logo(PageTopSvg),
|
||||
/// Imagen que se adapta automáticamente a su contenedor.
|
||||
///
|
||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
||||
Responsive(String),
|
||||
/// Imagen que aplica el estilo **miniatura** de Bootstrap.
|
||||
///
|
||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
||||
Thumbnail(String),
|
||||
/// 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),
|
||||
}
|
||||
|
||||
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.
|
||||
#[inline]
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Source::Logo(_) | Source::Responsive(_) => Self::IMG_FLUID,
|
||||
Source::Thumbnail(_) => Self::IMG_THUMBNAIL,
|
||||
Source::Plain(_) => "",
|
||||
}
|
||||
}
|
||||
|
||||
/* Añade la clase base asociada a la imagen según la fuente a la cadena de clases (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(&self, classes: &mut String) {
|
||||
let s = self.as_str();
|
||||
if s.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(s);
|
||||
} */
|
||||
|
||||
// Devuelve la clase asociada a la imagen según la fuente.
|
||||
#[inline]
|
||||
pub(crate) fn to_class(&self) -> String {
|
||||
let s = self.as_str();
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let mut class = String::with_capacity(s.len());
|
||||
class.push_str(s);
|
||||
class
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
//! Definiciones para crear menús [`Nav`] o alguna de sus variantes de presentación.
|
||||
//!
|
||||
//! Cada [`nav::Item`](crate::theme::nav::Item) representa un elemento individual del menú [`Nav`],
|
||||
//! con distintos comportamientos según su finalidad, como enlaces de navegación o menús
|
||||
//! desplegables [`Dropdown`](crate::theme::Dropdown).
|
||||
//!
|
||||
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
||||
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # 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::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")),
|
||||
//! ])),
|
||||
//! ))
|
||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Kind, Layout};
|
||||
|
||||
mod component;
|
||||
pub use component::Nav;
|
||||
|
||||
mod item;
|
||||
pub use item::{Item, ItemKind};
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Componente para crear un **menú** o alguna de sus variantes ([`nav::Kind`]).
|
||||
///
|
||||
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
|
||||
/// como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). También permite
|
||||
/// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
|
||||
///
|
||||
/// Ver ejemplo en el módulo [`nav`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Nav {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
items : Children,
|
||||
nav_kind : nav::Kind,
|
||||
nav_layout: nav::Layout,
|
||||
}
|
||||
|
||||
impl Component for Nav {
|
||||
fn new() -> Self {
|
||||
Nav::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, {
|
||||
let mut classes = "nav".to_string();
|
||||
self.nav_kind().push_class(&mut classes);
|
||||
self.nav_layout().push_class(&mut classes);
|
||||
classes
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
ul id=[self.id()] class=[self.classes().get()] {
|
||||
(items)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Nav {
|
||||
/// Crea un `Nav` usando pestañas para los elementos (*Tabs*).
|
||||
pub fn tabs() -> Self {
|
||||
Nav::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)
|
||||
}
|
||||
|
||||
/// Crea un `Nav` usando elementos subrayados (*Underline*).
|
||||
pub fn underline() -> Self {
|
||||
Nav::default().with_kind(nav::Kind::Underline)
|
||||
}
|
||||
|
||||
// **< Nav BUILDER >****************************************************************************
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Cambia el estilo del menú (*Tabs*, *Pills*, *Underline* o *Default*).
|
||||
#[builder_fn]
|
||||
pub fn with_kind(mut self, kind: nav::Kind) -> Self {
|
||||
self.nav_kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Selecciona la distribución y orientación del menú.
|
||||
#[builder_fn]
|
||||
pub fn with_layout(mut self, layout: nav::Layout) -> Self {
|
||||
self.nav_layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo elemento hijo al menú.
|
||||
pub fn add_item(mut self, item: nav::Item) -> Self {
|
||||
self.items.add(Child::with(item));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_items(mut self, op: TypedOp<nav::Item>) -> Self {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,284 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
// **< ItemKind >***********************************************************************************
|
||||
|
||||
/// Tipos de [`nav::Item`](crate::theme::nav::Item) disponibles en un menú
|
||||
/// [`Nav`](crate::theme::Nav).
|
||||
///
|
||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||
/// con él.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum ItemKind {
|
||||
/// Elemento vacío, no produce salida.
|
||||
#[default]
|
||||
Void,
|
||||
/// Etiqueta sin comportamiento interactivo.
|
||||
Label(L10n),
|
||||
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
|
||||
/// inicialmente deshabilitado.
|
||||
Link {
|
||||
label: L10n,
|
||||
path: FnPathByContext,
|
||||
blank: bool,
|
||||
disabled: bool,
|
||||
},
|
||||
/// Elemento que despliega un menú [`Dropdown`].
|
||||
Dropdown(Typed<Dropdown>),
|
||||
}
|
||||
|
||||
impl ItemKind {
|
||||
const ITEM: &str = "nav-item";
|
||||
const DROPDOWN: &str = "nav-item dropdown";
|
||||
|
||||
// Devuelve las clases base asociadas al tipo de elemento.
|
||||
#[inline]
|
||||
const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Void => "",
|
||||
Self::Dropdown(_) => Self::DROPDOWN,
|
||||
_ => Self::ITEM,
|
||||
}
|
||||
}
|
||||
|
||||
/* Añade las clases asociadas al tipo de elemento a la cadena de clases (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(&self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
if class.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
} */
|
||||
|
||||
// Devuelve las clases asociadas al tipo de elemento.
|
||||
#[inline]
|
||||
pub(crate) fn to_class(&self) -> String {
|
||||
self.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// 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`].
|
||||
///
|
||||
/// 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)]
|
||||
pub struct Item {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
item_kind: ItemKind,
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Item::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class());
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.item_kind() {
|
||||
ItemKind::Void => PrepareMarkup::None,
|
||||
|
||||
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
span class="nav-link disabled" aria-disabled="true" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let path = path(cx);
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = !*disabled && (current_path == Some(path));
|
||||
|
||||
let mut classes = "nav-link".to_string();
|
||||
if is_current {
|
||||
classes.push_str(" active");
|
||||
}
|
||||
if *disabled {
|
||||
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 aria_current = (href.is_some() && is_current).then_some("page");
|
||||
let aria_disabled = (*disabled).then_some("true");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
a
|
||||
class=(classes)
|
||||
href=[href]
|
||||
target=[target]
|
||||
rel=[rel]
|
||||
aria-current=[aria_current]
|
||||
aria-disabled=[aria_disabled]
|
||||
{
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ItemKind::Dropdown(menu) => {
|
||||
if let Some(dd) = menu.borrow() {
|
||||
let items = dd.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
let title = dd.title().lookup(cx).unwrap_or_else(|| {
|
||||
L10n::t("dropdown", &LOCALES_BOOTSIER)
|
||||
.lookup(cx)
|
||||
.unwrap_or_else(|| "Dropdown".to_string())
|
||||
});
|
||||
PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
a
|
||||
class="nav-link dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
{
|
||||
(title)
|
||||
}
|
||||
ul class="dropdown-menu" {
|
||||
(items)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
PrepareMarkup::None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||
pub fn label(label: L10n) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: false,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
||||
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: true,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: true,
|
||||
disabled: true,
|
||||
},
|
||||
..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`].
|
||||
pub fn dropdown(menu: Dropdown) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Dropdown(Typed::with(menu)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
// **< 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Kind >***************************************************************************************
|
||||
|
||||
/// Define la variante de presentación de un menú [`Nav`](crate::theme::Nav).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Kind {
|
||||
/// Estilo por defecto, lista de enlaces flexible y minimalista.
|
||||
#[default]
|
||||
Default,
|
||||
/// Pestañas con borde para cambiar entre secciones.
|
||||
Tabs,
|
||||
/// Botones con fondo que resaltan el elemento activo.
|
||||
Pills,
|
||||
/// Variante con subrayado del elemento activo, estética ligera.
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
const TABS: &str = "nav-tabs";
|
||||
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.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Default => "",
|
||||
Self::Tabs => Self::TABS,
|
||||
Self::Pills => Self::PILLS,
|
||||
Self::Underline => Self::UNDERLINE,
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
if class.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
|
||||
/* Devuelve la clase asociada al tipo de menú, o una cadena vacía si no aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_owned()
|
||||
} */
|
||||
}
|
||||
|
||||
// **< Layout >*************************************************************************************
|
||||
|
||||
/// Distribución y orientación de un menú [`Nav`](crate::theme::Nav).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Layout {
|
||||
/// Comportamiento por defecto, ancho definido por el contenido y sin alineación forzada.
|
||||
#[default]
|
||||
Default,
|
||||
/// Alinea los elementos al inicio de la fila.
|
||||
Start,
|
||||
/// Centra horizontalmente los elementos.
|
||||
Center,
|
||||
/// Alinea los elementos al final de la fila.
|
||||
End,
|
||||
/// Apila los elementos en columna.
|
||||
Vertical,
|
||||
/// Los elementos se expanden para rellenar la fila.
|
||||
Fill,
|
||||
/// Todos los elementos ocupan el mismo ancho rellenando la fila.
|
||||
Justified,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
const START: &str = "justify-content-start";
|
||||
const CENTER: &str = "justify-content-center";
|
||||
const END: &str = "justify-content-end";
|
||||
const VERTICAL: &str = "flex-column";
|
||||
const FILL: &str = "nav-fill";
|
||||
const JUSTIFIED: &str = "nav-justified";
|
||||
|
||||
// Devuelve la clase base asociada a la distribución y orientación del menú.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Default => "",
|
||||
Self::Start => Self::START,
|
||||
Self::Center => Self::CENTER,
|
||||
Self::End => Self::END,
|
||||
Self::Vertical => Self::VERTICAL,
|
||||
Self::Fill => Self::FILL,
|
||||
Self::Justified => Self::JUSTIFIED,
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
if class.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
|
||||
/* Devuelve la clase asociada a la distribución y orientación del menú, o una cadena vacía si no
|
||||
// aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_owned()
|
||||
} */
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
//! Definiciones para crear barras de navegación [`Navbar`].
|
||||
//!
|
||||
//! 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).
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! # Ejemplos
|
||||
//!
|
||||
//! Barra **simple**, sólo con un menú horizontal:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! 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"))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let navbar = Navbar::simple_toggle()
|
||||
//! .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"))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("PageTop"))
|
||||
//! .with_path(Some(|_| "/"));
|
||||
//!
|
||||
//! 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::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(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("Intranet"))
|
||||
//! .with_path(Some(|_| "/"));
|
||||
//!
|
||||
//! 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"))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let oc = Offcanvas::new()
|
||||
//! .with_id("main_offcanvas")
|
||||
//! .with_title(L10n::n("Main menu"))
|
||||
//! .with_placement(offcanvas::Placement::Start)
|
||||
//! .with_backdrop(offcanvas::Backdrop::Enabled);
|
||||
//!
|
||||
//! 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::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"))
|
||||
//! ))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra **fija arriba**:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("Main App"))
|
||||
//! .with_path(Some(|_| "/"));
|
||||
//!
|
||||
//! 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"))
|
||||
//! ));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Layout, Position};
|
||||
|
||||
mod brand;
|
||||
pub use brand::Brand;
|
||||
|
||||
mod component;
|
||||
pub use component::Navbar;
|
||||
|
||||
mod item;
|
||||
pub use item::Item;
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Marca de identidad para mostrar en una barra de navegación [`Navbar`].
|
||||
///
|
||||
/// 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
|
||||
/// 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)]
|
||||
pub struct Brand {
|
||||
id : AttrId,
|
||||
image : Typed<Image>,
|
||||
#[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
|
||||
title : L10n,
|
||||
slogan: L10n,
|
||||
#[default(_code = "Some(|_| \"/\")")]
|
||||
path : Option<FnPathByContext>,
|
||||
}
|
||||
|
||||
impl Component for Brand {
|
||||
fn new() -> Self {
|
||||
Brand::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let image = self.image().render(cx);
|
||||
let title = self.title().using(cx);
|
||||
if title.is_empty() && image.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
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) }
|
||||
} @else {
|
||||
span class="navbar-brand" { (image) (title) (slogan) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Brand {
|
||||
// **< Brand BUILDER >**************************************************************************
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará.
|
||||
#[builder_fn]
|
||||
pub fn with_image(mut self, image: Option<Image>) -> Self {
|
||||
self.image.alter_component(image);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el título de la identidad de marca.
|
||||
#[builder_fn]
|
||||
pub fn with_title(mut self, title: L10n) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
/// Define el eslogan de la marca.
|
||||
#[builder_fn]
|
||||
pub fn with_slogan(mut self, slogan: L10n) -> Self {
|
||||
self.slogan = slogan;
|
||||
self
|
||||
}
|
||||
|
||||
/// 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;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
const TOGGLE_COLLAPSE: &str = "collapse";
|
||||
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
||||
|
||||
/// Componente para crear una **barra de navegación**.
|
||||
///
|
||||
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
|
||||
/// botón de despliegue o dentro de un [`offcanvas`]), controladas por [`navbar::Layout`]. También
|
||||
/// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`].
|
||||
///
|
||||
/// Ver ejemplos en el módulo [`navbar`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Navbar {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
expand : BreakPoint,
|
||||
layout : navbar::Layout,
|
||||
position : navbar::Position,
|
||||
items : Children,
|
||||
}
|
||||
|
||||
impl Component for Navbar {
|
||||
fn new() -> Self {
|
||||
Navbar::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, {
|
||||
let mut classes = "navbar".to_string();
|
||||
self.expand().push_class(&mut classes, "navbar-expand", "");
|
||||
self.position().push_class(&mut classes);
|
||||
classes
|
||||
});
|
||||
}
|
||||
|
||||
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 aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
|
||||
Some("false")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
html! {
|
||||
button
|
||||
type="button"
|
||||
class="navbar-toggler"
|
||||
data-bs-toggle=(data_bs_toggle)
|
||||
data-bs-target=(id_content_target)
|
||||
aria-controls=(id_content)
|
||||
aria-expanded=[aria_expanded]
|
||||
aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
|
||||
{
|
||||
span class="navbar-toggler-icon" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
|
||||
// Asegura que la barra tiene un id estable para poder asociarlo al colapso/offcanvas.
|
||||
let id = cx.required_id::<Self>(self.id());
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
nav id=(id) class=[self.classes().get()] {
|
||||
div class="container-fluid" {
|
||||
@match self.layout() {
|
||||
// Barra más sencilla: sólo contenido.
|
||||
navbar::Layout::Simple => {
|
||||
(items)
|
||||
},
|
||||
|
||||
// Barra sencilla que se puede contraer/expandir.
|
||||
navbar::Layout::SimpleToggle => {
|
||||
@let id_content = join!(id, "-content");
|
||||
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
div id=(id_content) class="collapse navbar-collapse" {
|
||||
(items)
|
||||
}
|
||||
},
|
||||
|
||||
// Barra con marca a la izquierda, siempre visible.
|
||||
navbar::Layout::SimpleBrandLeft(brand) => {
|
||||
(brand.render(cx))
|
||||
(items)
|
||||
},
|
||||
|
||||
// Barra con marca a la izquierda y botón a la derecha.
|
||||
navbar::Layout::BrandLeft(brand) => {
|
||||
@let id_content = join!(id, "-content");
|
||||
|
||||
(brand.render(cx))
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
div id=(id_content) class="collapse navbar-collapse" {
|
||||
(items)
|
||||
}
|
||||
},
|
||||
|
||||
// Barra con botón a la izquierda y marca a la derecha.
|
||||
navbar::Layout::BrandRight(brand) => {
|
||||
@let id_content = join!(id, "-content");
|
||||
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
(brand.render(cx))
|
||||
div id=(id_content) class="collapse navbar-collapse" {
|
||||
(items)
|
||||
}
|
||||
},
|
||||
|
||||
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
|
||||
navbar::Layout::Offcanvas(offcanvas) => {
|
||||
@let id_content = offcanvas.id().unwrap_or_default();
|
||||
|
||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||
@if let Some(oc) = offcanvas.borrow() {
|
||||
(oc.render_offcanvas(cx, Some(self.items())))
|
||||
}
|
||||
},
|
||||
|
||||
// Barra con marca a la izquierda y contenido en offcanvas.
|
||||
navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
|
||||
@let id_content = offcanvas.id().unwrap_or_default();
|
||||
|
||||
(brand.render(cx))
|
||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||
@if let Some(oc) = offcanvas.borrow() {
|
||||
(oc.render_offcanvas(cx, Some(self.items())))
|
||||
}
|
||||
},
|
||||
|
||||
// Barra con contenido en offcanvas y marca a la derecha.
|
||||
navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
|
||||
@let id_content = offcanvas.id().unwrap_or_default();
|
||||
|
||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||
(brand.render(cx))
|
||||
@if let Some(oc) = offcanvas.borrow() {
|
||||
(oc.render_offcanvas(cx, Some(self.items())))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
|
||||
/// 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(
|
||||
Typed::with(brand),
|
||||
Typed::with(oc),
|
||||
))
|
||||
}
|
||||
|
||||
/// 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(
|
||||
Typed::with(brand),
|
||||
Typed::with(oc),
|
||||
))
|
||||
}
|
||||
|
||||
// **< Navbar BUILDER >*************************************************************************
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas a la barra de navegación.
|
||||
///
|
||||
/// También acepta clases predefinidas para:
|
||||
///
|
||||
/// - Modificar el color de fondo ([`classes::Background`]).
|
||||
/// - 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
|
||||
}
|
||||
|
||||
/// Define a partir de qué punto de ruptura la barra de navegación deja de colapsar.
|
||||
#[builder_fn]
|
||||
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
|
||||
self.expand = bp;
|
||||
self
|
||||
}
|
||||
|
||||
/// Define el tipo de disposición que tendrá la barra de navegación.
|
||||
#[builder_fn]
|
||||
pub fn with_layout(mut self, layout: navbar::Layout) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Define dónde se mostrará la barra de navegación dentro del documento.
|
||||
#[builder_fn]
|
||||
pub fn with_position(mut self, position: navbar::Position) -> Self {
|
||||
self.position = position;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo contenido hijo.
|
||||
#[inline]
|
||||
pub fn add_item(mut self, item: navbar::Item) -> Self {
|
||||
self.items.add(Child::with(item));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de contenidos (`children`) aplicando una operación [`TypedOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::Navbar).
|
||||
///
|
||||
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
|
||||
/// contenido** de la barra (la parte colapsable, el *offcanvas* o el bloque simple), por lo que son
|
||||
/// independientes de la marca o del botón que ya pueda definir el propio [`navbar::Layout`].
|
||||
#[derive(AutoDefault)]
|
||||
pub enum Item {
|
||||
/// Sin contenido, no produce salida.
|
||||
#[default]
|
||||
Void,
|
||||
/// Marca de identidad mostrada dentro del contenido de la barra de navegación.
|
||||
///
|
||||
/// Útil cuando el [`navbar::Layout`] no incluye marca, y se quiere incluir dentro del área
|
||||
/// colapsable/*offcanvas*. Si el *layout* ya muestra una marca, esta variante no la sustituye,
|
||||
/// sólo añade otra dentro del bloque de contenidos.
|
||||
Brand(Typed<navbar::Brand>),
|
||||
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
|
||||
Nav(Typed<Nav>),
|
||||
/// Representa un texto libre localizado.
|
||||
Text(L10n),
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Item::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Void => None,
|
||||
Self::Brand(brand) => brand.id(),
|
||||
Self::Nav(nav) => nav.id(),
|
||||
Self::Text(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
if let Self::Nav(nav) = self {
|
||||
if let Some(mut nav) = nav.borrow_mut() {
|
||||
nav.alter_classes(ClassesOp::Prepend, "navbar-nav");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self {
|
||||
Self::Void => PrepareMarkup::None,
|
||||
Self::Brand(brand) => PrepareMarkup::With(html! { (brand.render(cx)) }),
|
||||
Self::Nav(nav) => {
|
||||
if let Some(nav) = nav.borrow() {
|
||||
let items = nav.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
PrepareMarkup::With(html! {
|
||||
ul id=[nav.id()] class=[nav.classes().get()] {
|
||||
(items)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
PrepareMarkup::None
|
||||
}
|
||||
}
|
||||
Self::Text(text) => PrepareMarkup::With(html! {
|
||||
span class="navbar-text" {
|
||||
(text.using(cx))
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo [`navbar::Brand`] para añadir en el contenido de [`Navbar`].
|
||||
///
|
||||
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
|
||||
/// desplegable.
|
||||
pub fn brand(brand: navbar::Brand) -> Self {
|
||||
Self::Brand(Typed::with(brand))
|
||||
}
|
||||
|
||||
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`].
|
||||
pub fn nav(item: Nav) -> Self {
|
||||
Self::Nav(Typed::with(item))
|
||||
}
|
||||
|
||||
/// Crea un elemento de texto localizado, mostrado sin interacción.
|
||||
pub fn text(item: L10n) -> Self {
|
||||
Self::Text(item)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
// **< Layout >*************************************************************************************
|
||||
|
||||
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
|
||||
#[derive(AutoDefault)]
|
||||
pub enum Layout {
|
||||
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
||||
///
|
||||
/// La barra de navegación no se colapsa.
|
||||
#[default]
|
||||
Simple,
|
||||
|
||||
/// Barra simple, con botón de despliegue a la izquierda y sin marca de identidad.
|
||||
SimpleToggle,
|
||||
|
||||
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
|
||||
///
|
||||
/// La barra de navegación no se colapsa.
|
||||
SimpleBrandLeft(Typed<navbar::Brand>),
|
||||
|
||||
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
|
||||
BrandLeft(Typed<navbar::Brand>),
|
||||
|
||||
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
|
||||
BrandRight(Typed<navbar::Brand>),
|
||||
|
||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y sin marca de identidad.
|
||||
Offcanvas(Typed<Offcanvas>),
|
||||
|
||||
/// Contenido en [`Offcanvas`], con marca de identidad a la izquierda y botón de despliegue a la
|
||||
/// derecha.
|
||||
OffcanvasBrandLeft(Typed<navbar::Brand>, Typed<Offcanvas>),
|
||||
|
||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y marca de identidad a la
|
||||
/// derecha.
|
||||
OffcanvasBrandRight(Typed<navbar::Brand>, Typed<Offcanvas>),
|
||||
}
|
||||
|
||||
// **< Position >***********************************************************************************
|
||||
|
||||
/// Posición global de una barra de navegación [`Navbar`] en el documento.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Position {
|
||||
/// Barra normal, fluye con el documento.
|
||||
#[default]
|
||||
Static,
|
||||
/// Barra fijada en la parte superior, siempre visible.
|
||||
///
|
||||
/// Puede ser necesario reservar espacio en la parte superior del contenido que fluye debajo
|
||||
/// para evitar que quede oculto por la barra.
|
||||
FixedTop,
|
||||
/// Barra fijada en la parte inferior, siempre visible.
|
||||
///
|
||||
/// Puede ser necesario reservar espacio en la parte inferior del contenido que fluye debajo
|
||||
/// para evitar que quede oculto por la barra.
|
||||
FixedBottom,
|
||||
/// La barra de navegación se fija en la parte superior al hacer *scroll*.
|
||||
StickyTop,
|
||||
/// La barra de navegación se fija en la parte inferior al hacer *scroll*.
|
||||
StickyBottom,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
// 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 {
|
||||
Self::Static => "",
|
||||
Self::FixedTop => "fixed-top",
|
||||
Self::FixedBottom => "fixed-bottom",
|
||||
Self::StickyTop => "sticky-top",
|
||||
Self::StickyBottom => "sticky-bottom",
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
if class.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
|
||||
/* Devuelve la clase asociada a la posición de la barra de navegación, o cadena vacía si no
|
||||
// aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_string()
|
||||
} */
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`].
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let panel = Offcanvas::new()
|
||||
//! .with_id("offcanvas_example")
|
||||
//! .with_title(L10n::n("Offcanvas title"))
|
||||
//! .with_placement(offcanvas::Placement::End)
|
||||
//! .with_backdrop(offcanvas::Backdrop::Enabled)
|
||||
//! .with_body_scroll(offcanvas::BodyScroll::Enabled)
|
||||
//! .with_visibility(offcanvas::Visibility::Default)
|
||||
//! .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"))
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Backdrop, BodyScroll, Placement, Visibility};
|
||||
|
||||
mod component;
|
||||
pub use component::Offcanvas;
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
/// Componente para crear un **panel lateral deslizante** con contenidos adicionales.
|
||||
///
|
||||
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
|
||||
/// características principales:
|
||||
///
|
||||
/// - Puede mostrar una capa de fondo para centrar la atención del usuario en el panel
|
||||
/// ([`with_backdrop()`](Self::with_backdrop)); o puede bloquear el desplazamiento del documento
|
||||
/// principal ([`with_body_scroll()`](Self::with_body_scroll)).
|
||||
/// - Se puede configurar el borde de la ventana desde el que se desliza el panel
|
||||
/// ([`with_placement()`](Self::with_placement)).
|
||||
/// - Encabezado con título ([`with_title()`](Self::with_title)) y **botón de cierre** integrado.
|
||||
/// - Puede cambiar su comportamiento a partir de un punto de ruptura
|
||||
/// ([`with_breakpoint()`](Self::with_breakpoint)).
|
||||
/// - Asocia título y controles de accesibilidad a un identificador único y expone atributos
|
||||
/// adecuados para lectores de pantalla y navegación por teclado.
|
||||
///
|
||||
/// Ver ejemplo en el módulo [`offcanvas`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Offcanvas {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
title : L10n,
|
||||
breakpoint: BreakPoint,
|
||||
backdrop : offcanvas::Backdrop,
|
||||
scrolling : offcanvas::BodyScroll,
|
||||
placement : offcanvas::Placement,
|
||||
visibility: offcanvas::Visibility,
|
||||
children : Children,
|
||||
}
|
||||
|
||||
impl Component for Offcanvas {
|
||||
fn new() -> Self {
|
||||
Offcanvas::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, {
|
||||
let mut classes = "offcanvas".to_string();
|
||||
self.breakpoint().push_class(&mut classes, "offcanvas", "");
|
||||
self.placement().push_class(&mut classes);
|
||||
self.visibility().push_class(&mut classes);
|
||||
classes
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
PrepareMarkup::With(self.render_offcanvas(cx, None))
|
||||
}
|
||||
}
|
||||
|
||||
impl Offcanvas {
|
||||
// **< Offcanvas BUILDER >**********************************************************************
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Establece el título del encabezado.
|
||||
#[builder_fn]
|
||||
pub fn with_title(mut self, title: L10n) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el punto de ruptura a partir del cual cambia el comportamiento del panel.
|
||||
///
|
||||
/// - **Por debajo** de ese tamaño de pantalla, el componente actúa como panel deslizante
|
||||
/// ([`Offcanvas`]).
|
||||
/// - **Por encima**, el contenido del panel se muestra tal cual, integrado en la página.
|
||||
///
|
||||
/// Por ejemplo, con `BreakPoint::LG`, será *offcanvas* en móviles y tabletas, y visible
|
||||
/// directamente en pantallas grandes. Por defecto usa `BreakPoint::None` para que sea
|
||||
/// *offcanvas* siempre.
|
||||
#[builder_fn]
|
||||
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
|
||||
self.breakpoint = bp;
|
||||
self
|
||||
}
|
||||
|
||||
/// Ajusta la capa de fondo del panel para definir su comportamiento al hacer clic fuera del
|
||||
/// panel.
|
||||
#[builder_fn]
|
||||
pub fn with_backdrop(mut self, backdrop: offcanvas::Backdrop) -> Self {
|
||||
self.backdrop = backdrop;
|
||||
self
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Indica desde qué borde de la ventana entra y se ancla el panel.
|
||||
#[builder_fn]
|
||||
pub fn with_placement(mut self, placement: offcanvas::Placement) -> Self {
|
||||
self.placement = placement;
|
||||
self
|
||||
}
|
||||
|
||||
/// Fija el estado inicial del panel (oculto o visible al cargar).
|
||||
#[builder_fn]
|
||||
pub fn with_visibility(mut self, visibility: offcanvas::Visibility) -> Self {
|
||||
self.visibility = visibility;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente hijo al panel.
|
||||
#[inline]
|
||||
pub fn add_child(mut self, child: impl Component) -> Self {
|
||||
self.children.add(Child::with(child));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_children(mut self, op: ChildOp) -> Self {
|
||||
self.children.alter_child(op);
|
||||
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 {
|
||||
let body = self.children().render(cx);
|
||||
let body_extra = extra.map(|c| c.render(cx)).unwrap_or_else(|| html! {});
|
||||
if body.is_empty() && body_extra.is_empty() {
|
||||
return html! {};
|
||||
}
|
||||
|
||||
let id = cx.required_id::<Self>(self.id());
|
||||
let id_label = join!(id, "-label");
|
||||
let id_target = join!("#", id);
|
||||
|
||||
let body_scroll = match self.body_scroll() {
|
||||
offcanvas::BodyScroll::Disabled => None,
|
||||
offcanvas::BodyScroll::Enabled => Some("true"),
|
||||
};
|
||||
|
||||
let backdrop = match self.backdrop() {
|
||||
offcanvas::Backdrop::Disabled => Some("false"),
|
||||
offcanvas::Backdrop::Enabled => None,
|
||||
offcanvas::Backdrop::Static => Some("static"),
|
||||
};
|
||||
|
||||
let title = self.title().using(cx);
|
||||
|
||||
html! {
|
||||
div
|
||||
id=(id)
|
||||
class=[self.classes().get()]
|
||||
tabindex="-1"
|
||||
data-bs-scroll=[body_scroll]
|
||||
data-bs-backdrop=[backdrop]
|
||||
aria-labelledby=(id_label)
|
||||
{
|
||||
div class="offcanvas-header" {
|
||||
@if !title.is_empty() {
|
||||
h5 class="offcanvas-title" id=(id_label) { (title) }
|
||||
}
|
||||
button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="offcanvas"
|
||||
data-bs-target=(id_target)
|
||||
aria-label=[L10n::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)]
|
||||
{}
|
||||
}
|
||||
div class="offcanvas-body" {
|
||||
(body)
|
||||
(body_extra)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Backdrop >***********************************************************************************
|
||||
|
||||
/// Comportamiento de la capa de fondo (*backdrop*) de un panel
|
||||
/// [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Backdrop {
|
||||
/// Sin capa de fondo, la página principal permanece visible e interactiva.
|
||||
Disabled,
|
||||
/// Opción por defecto, se oscurece el fondo; un clic fuera del panel suele cerrarlo.
|
||||
#[default]
|
||||
Enabled,
|
||||
/// Muestra la capa de fondo pero no se cierra al hacer clic fuera del panel. Útil si se
|
||||
/// requiere completar una acción antes de salir.
|
||||
Static,
|
||||
}
|
||||
|
||||
// **< BodyScroll >*********************************************************************************
|
||||
|
||||
/// Controla si la página principal puede desplazarse al abrir un panel
|
||||
/// [`Offcanvas`](crate::theme::Offcanvas).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum BodyScroll {
|
||||
/// Opción por defecto, la página principal se bloquea centrando la interacción en el panel.
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Permite el desplazamiento de la página principal.
|
||||
Enabled,
|
||||
}
|
||||
|
||||
// **< Placement >**********************************************************************************
|
||||
|
||||
/// Posición de aparición de un panel [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
|
||||
///
|
||||
/// Define desde qué borde de la ventana entra y se ancla el panel.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Placement {
|
||||
/// Opción por defecto, desde el borde inicial según dirección de lectura (respetando LTR/RTL).
|
||||
#[default]
|
||||
Start,
|
||||
/// Desde el borde final según dirección de lectura (respetando LTR/RTL).
|
||||
End,
|
||||
/// Desde la parte superior.
|
||||
Top,
|
||||
/// Desde la parte inferior.
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
// Devuelve la clase base asociada a la posición de aparición del panel.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Placement::Start => "offcanvas-start",
|
||||
Placement::End => "offcanvas-end",
|
||||
Placement::Top => "offcanvas-top",
|
||||
Placement::Bottom => "offcanvas-bottom",
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(self.as_str());
|
||||
}
|
||||
|
||||
/* Devuelve la clase asociada a la posición de aparición del panel (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_owned()
|
||||
} */
|
||||
}
|
||||
|
||||
// **< Visibility >*********************************************************************************
|
||||
|
||||
/// Estado inicial de un panel [`Offcanvas`](crate::theme::Offcanvas).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Visibility {
|
||||
/// El panel permanece oculto desde el principio.
|
||||
#[default]
|
||||
Default,
|
||||
/// El panel se muestra abierto al cargar.
|
||||
Show,
|
||||
}
|
||||
|
||||
impl Visibility {
|
||||
// Devuelve la clase base asociada al estado inicial del panel.
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Visibility::Default => "",
|
||||
Visibility::Show => "show",
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
if class.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
|
||||
/* Devuelve la clase asociada al estado inicial, o una cadena vacía si no aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_owned()
|
||||
} */
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,108 +0,0 @@
|
|||
// Enable CSS Grid
|
||||
$enable-grid-classes: false;
|
||||
$enable-cssgrid: true;
|
||||
|
||||
// Opacity
|
||||
.bg-opacity-0 {
|
||||
--bs-bg-opacity: 0;
|
||||
}
|
||||
|
||||
.border-opacity-0 {
|
||||
--bs-border-opacity: 0;
|
||||
}
|
||||
|
||||
.text-opacity-0 {
|
||||
--bs-text-opacity: 0;
|
||||
}
|
||||
.text-opacity-10 {
|
||||
--bs-text-opacity: 0.1;
|
||||
}
|
||||
|
||||
// Extending utilities
|
||||
$utilities: map-merge(
|
||||
$utilities,
|
||||
(
|
||||
// Individual border widths
|
||||
"border-top": (
|
||||
property: border-top-width,
|
||||
class: border-top,
|
||||
values: $border-widths
|
||||
),
|
||||
"border-end": (
|
||||
property: border-right-width,
|
||||
class: border-end,
|
||||
values: $border-widths
|
||||
),
|
||||
"border-bottom": (
|
||||
property: border-bottom-width,
|
||||
class: border-bottom,
|
||||
values: $border-widths
|
||||
),
|
||||
"border-start": (
|
||||
property: border-left-width,
|
||||
class: border-start,
|
||||
values: $border-widths
|
||||
),
|
||||
// Individual rounded values
|
||||
"rounded-top-start": (
|
||||
property: border-top-left-radius,
|
||||
class: rounded-top-start,
|
||||
values: (
|
||||
null: var(--#{$prefix}border-radius),
|
||||
0: 0,
|
||||
1: var(--#{$prefix}border-radius-sm),
|
||||
2: var(--#{$prefix}border-radius),
|
||||
3: var(--#{$prefix}border-radius-lg),
|
||||
4: var(--#{$prefix}border-radius-xl),
|
||||
5: var(--#{$prefix}border-radius-xxl),
|
||||
circle: 50%,
|
||||
pill: var(--#{$prefix}border-radius-pill)
|
||||
)
|
||||
),
|
||||
"rounded-top-end": (
|
||||
property: border-top-right-radius,
|
||||
class: rounded-top-end,
|
||||
values: (
|
||||
null: var(--#{$prefix}border-radius),
|
||||
0: 0,
|
||||
1: var(--#{$prefix}border-radius-sm),
|
||||
2: var(--#{$prefix}border-radius),
|
||||
3: var(--#{$prefix}border-radius-lg),
|
||||
4: var(--#{$prefix}border-radius-xl),
|
||||
5: var(--#{$prefix}border-radius-xxl),
|
||||
circle: 50%,
|
||||
pill: var(--#{$prefix}border-radius-pill)
|
||||
)
|
||||
),
|
||||
"rounded-bottom-start": (
|
||||
property: border-bottom-left-radius,
|
||||
class: rounded-bottom-start,
|
||||
values: (
|
||||
null: var(--#{$prefix}border-radius),
|
||||
0: 0,
|
||||
1: var(--#{$prefix}border-radius-sm),
|
||||
2: var(--#{$prefix}border-radius),
|
||||
3: var(--#{$prefix}border-radius-lg),
|
||||
4: var(--#{$prefix}border-radius-xl),
|
||||
5: var(--#{$prefix}border-radius-xxl),
|
||||
circle: 50%,
|
||||
pill: var(--#{$prefix}border-radius-pill)
|
||||
)
|
||||
),
|
||||
"rounded-bottom-end": (
|
||||
property: border-bottom-right-radius,
|
||||
class: rounded-bottom-end,
|
||||
values: (
|
||||
null: var(--#{$prefix}border-radius),
|
||||
0: 0,
|
||||
1: var(--#{$prefix}border-radius-sm),
|
||||
2: var(--#{$prefix}border-radius),
|
||||
3: var(--#{$prefix}border-radius-lg),
|
||||
4: var(--#{$prefix}border-radius-xl),
|
||||
5: var(--#{$prefix}border-radius-xxl),
|
||||
circle: 50%,
|
||||
pill: var(--#{$prefix}border-radius-pill)
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
@import "bootstrap-5.3.8/mixins/banner";
|
||||
@include bsBanner("");
|
||||
|
||||
|
||||
// scss-docs-start import-stack
|
||||
// Configuration
|
||||
@import "bootstrap-5.3.8/functions";
|
||||
@import "bootstrap-5.3.8/variables";
|
||||
@import "bootstrap-5.3.8/variables-dark";
|
||||
@import "bootstrap-5.3.8/maps";
|
||||
@import "bootstrap-5.3.8/mixins";
|
||||
@import "bootstrap-5.3.8/utilities";
|
||||
|
||||
// Layout & components
|
||||
@import "bootstrap-5.3.8/root";
|
||||
@import "bootstrap-5.3.8/reboot";
|
||||
@import "bootstrap-5.3.8/type";
|
||||
@import "bootstrap-5.3.8/images";
|
||||
@import "bootstrap-5.3.8/containers";
|
||||
@import "bootstrap-5.3.8/grid";
|
||||
@import "bootstrap-5.3.8/tables";
|
||||
@import "bootstrap-5.3.8/forms";
|
||||
@import "bootstrap-5.3.8/buttons";
|
||||
@import "bootstrap-5.3.8/transitions";
|
||||
@import "bootstrap-5.3.8/dropdown";
|
||||
@import "bootstrap-5.3.8/button-group";
|
||||
@import "bootstrap-5.3.8/nav";
|
||||
@import "bootstrap-5.3.8/navbar";
|
||||
@import "bootstrap-5.3.8/card";
|
||||
@import "bootstrap-5.3.8/accordion";
|
||||
@import "bootstrap-5.3.8/breadcrumb";
|
||||
@import "bootstrap-5.3.8/pagination";
|
||||
@import "bootstrap-5.3.8/badge";
|
||||
@import "bootstrap-5.3.8/alert";
|
||||
@import "bootstrap-5.3.8/progress";
|
||||
@import "bootstrap-5.3.8/list-group";
|
||||
@import "bootstrap-5.3.8/close";
|
||||
@import "bootstrap-5.3.8/toasts";
|
||||
@import "bootstrap-5.3.8/modal";
|
||||
@import "bootstrap-5.3.8/tooltip";
|
||||
@import "bootstrap-5.3.8/popover";
|
||||
@import "bootstrap-5.3.8/carousel";
|
||||
@import "bootstrap-5.3.8/spinners";
|
||||
@import "bootstrap-5.3.8/offcanvas";
|
||||
@import "bootstrap-5.3.8/placeholders";
|
||||
|
||||
// Helpers
|
||||
@import "bootstrap-5.3.8/helpers";
|
||||
|
||||
// Custom definitions
|
||||
@import "customs";
|
||||
|
||||
// Utilities
|
||||
@import "bootstrap-5.3.8/utilities/api";
|
||||
// scss-docs-end import-stack
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
@mixin box-shadow($shadow...) {
|
||||
@if $enable-shadows {
|
||||
$result: ();
|
||||
$has-single-value: false;
|
||||
$single-value: null;
|
||||
|
||||
@each $value in $shadow {
|
||||
@if $value != null {
|
||||
@if $value == none or $value == initial or $value == inherit or $value == unset {
|
||||
$has-single-value: true;
|
||||
$single-value: $value;
|
||||
} @else {
|
||||
$result: append($result, $value, "comma");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@if $has-single-value {
|
||||
box-shadow: $single-value;
|
||||
} @else if (length($result) > 0) {
|
||||
box-shadow: $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
@import "../../functions";
|
||||
@import "../../variables";
|
||||
@import "../../mixins";
|
||||
|
||||
// Store original value
|
||||
$original-enable-shadows: $enable-shadows;
|
||||
|
||||
// Enable shadows for all tests
|
||||
$enable-shadows: true !global;
|
||||
|
||||
@include describe("box-shadow mixin") {
|
||||
@include it("handles single none value") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test {
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("handles multiple none values by consolidating them") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test {
|
||||
@include box-shadow(none, none, none);
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("handles other single-value keywords (initial, inherit, unset)") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test-initial {
|
||||
@include box-shadow(initial);
|
||||
}
|
||||
.test-inherit {
|
||||
@include box-shadow(inherit);
|
||||
}
|
||||
.test-unset {
|
||||
@include box-shadow(unset);
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test-initial {
|
||||
box-shadow: initial;
|
||||
}
|
||||
.test-inherit {
|
||||
box-shadow: inherit;
|
||||
}
|
||||
.test-unset {
|
||||
box-shadow: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("handles multiple single-value keywords by using the last one") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test {
|
||||
@include box-shadow(initial, inherit, unset);
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test {
|
||||
box-shadow: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("handles regular box-shadow values") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test {
|
||||
@include box-shadow(0 0 10px rgba(0, 0, 0, .5));
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, .5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("handles multiple regular box-shadow values") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test {
|
||||
@include box-shadow(0 0 10px rgba(0, 0, 0, .5), 0 0 20px rgba(0, 0, 0, .3));
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, .5), 0 0 20px rgba(0, 0, 0, .3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("handles null values by ignoring them") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test {
|
||||
@include box-shadow(null, 0 0 10px rgba(0, 0, 0, .5), null);
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, .5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("handles mixed values with keywords and regular shadows") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test {
|
||||
@include box-shadow(none, 0 0 10px rgba(0, 0, 0, .5));
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("handles empty input") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test {
|
||||
@include box-shadow();
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test { // stylelint-disable-line block-no-empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("respects $enable-shadows variable") {
|
||||
$enable-shadows: false !global;
|
||||
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.test {
|
||||
@include box-shadow(0 0 10px rgba(0, 0, 0, .5));
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.test { // stylelint-disable-line block-no-empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$enable-shadows: true !global;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original value
|
||||
$enable-shadows: $original-enable-shadows !global;
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
@import "../../functions";
|
||||
@import "../../variables";
|
||||
@import "../../variables-dark";
|
||||
@import "../../maps";
|
||||
@import "../../mixins";
|
||||
|
||||
@include describe("color-contrast function") {
|
||||
@include it("should return a color when contrast ratio equals minimum requirement (WCAG 2.1 compliance)") {
|
||||
// Test case: Background color that produces contrast ratio close to 4.5:1
|
||||
// This tests the WCAG 2.1 requirement that contrast should be "at least 4.5:1" (>= 4.5)
|
||||
// rather than "strictly greater than 4.5:1" (> 4.5)
|
||||
|
||||
// #777777 produces 4.4776:1 contrast ratio with white text
|
||||
// Since this is below the 4.5:1 threshold, it should return the highest available contrast color
|
||||
$test-background: #777;
|
||||
$result: color-contrast($test-background);
|
||||
|
||||
@include assert-equal($result, $black, "Should return black (highest available contrast) for background with 4.4776:1 contrast ratio (below threshold)");
|
||||
}
|
||||
|
||||
@include it("should return a color when contrast ratio is above minimum requirement") {
|
||||
// Test case: Background color that produces contrast ratio above 4.5:1
|
||||
// #767676 produces 4.5415:1 contrast ratio with white text
|
||||
$test-background: #767676;
|
||||
$result: color-contrast($test-background);
|
||||
|
||||
@include assert-equal($result, $white, "Should return white for background with 4.5415:1 contrast ratio (above threshold)");
|
||||
}
|
||||
|
||||
@include it("should return a color when contrast ratio is below minimum requirement") {
|
||||
// Test case: Background color that produces contrast ratio below 4.5:1
|
||||
// #787878 produces 4.4155:1 contrast ratio with white text
|
||||
$test-background: #787878;
|
||||
$result: color-contrast($test-background);
|
||||
|
||||
// Should return the color with the highest available contrast ratio
|
||||
@include assert-equal($result, $black, "Should return black (highest available contrast) for background with 4.4155:1 contrast ratio (below threshold)");
|
||||
}
|
||||
|
||||
@include it("should handle edge case with very light background") {
|
||||
// Test case: Very light background that should return dark text
|
||||
$test-background: #f8f9fa; // Very light gray
|
||||
$result: color-contrast($test-background);
|
||||
|
||||
@include assert-equal($result, $color-contrast-dark, "Should return dark text for very light background");
|
||||
}
|
||||
|
||||
@include it("should handle edge case with very dark background") {
|
||||
// Test case: Very dark background that should return light text
|
||||
$test-background: #212529; // Very dark gray
|
||||
$result: color-contrast($test-background);
|
||||
|
||||
@include assert-equal($result, $color-contrast-light, "Should return light text for very dark background");
|
||||
}
|
||||
|
||||
@include it("should work with custom minimum contrast ratio") {
|
||||
// Test case: Using a custom minimum contrast ratio
|
||||
$test-background: #666;
|
||||
$result: color-contrast($test-background, $color-contrast-dark, $color-contrast-light, 3);
|
||||
|
||||
@include assert-equal($result, $white, "Should return white when using custom minimum contrast ratio of 3.0");
|
||||
}
|
||||
|
||||
@include it("should test contrast ratio calculation accuracy") {
|
||||
// Test case: Verify that contrast-ratio function works correctly
|
||||
$background: #767676;
|
||||
$foreground: $white;
|
||||
$ratio: contrast-ratio($background, $foreground);
|
||||
// Bootstrap's implementation calculates this as ~4.5415, not exactly 4.5, due to its luminance math.
|
||||
// We use 4.54 as the threshold for this test to match the actual implementation.
|
||||
@include assert-true($ratio >= 4.54 and $ratio <= 4.55, "Contrast ratio should be approximately 4.54:1 (Bootstrap's math)");
|
||||
}
|
||||
|
||||
@include it("should test luminance calculation") {
|
||||
// Test case: Verify luminance function works correctly
|
||||
$white-luminance: luminance($white);
|
||||
$black-luminance: luminance($black);
|
||||
|
||||
@include assert-equal($white-luminance, 1, "White should have luminance of 1");
|
||||
@include assert-equal($black-luminance, 0, "Black should have luminance of 0");
|
||||
}
|
||||
|
||||
@include it("should handle rgba colors correctly") {
|
||||
// Test case: Test with rgba colors
|
||||
$test-background: rgba(118, 118, 118, 1); // Same as #767676
|
||||
$result: color-contrast($test-background);
|
||||
|
||||
@include assert-equal($result, $white, "Should handle rgba colors correctly");
|
||||
}
|
||||
|
||||
@include it("should test the WCAG 2.1 boundary condition with color below threshold") {
|
||||
// Test case: Background color that produces contrast ratio below 4.5:1
|
||||
// #787878 produces 4.4155:1 contrast ratio with white
|
||||
$test-background: #787878; // Produces 4.4155:1 contrast ratio
|
||||
$contrast-ratio: contrast-ratio($test-background, $white);
|
||||
|
||||
// Verify the contrast ratio is below 4.5:1
|
||||
@include assert-true($contrast-ratio < 4.5, "Contrast ratio should be below 4.5:1 threshold");
|
||||
|
||||
// The color-contrast function should return the color with highest available contrast
|
||||
$result: color-contrast($test-background);
|
||||
@include assert-equal($result, $black, "color-contrast should return black (highest available contrast) for below-threshold ratio");
|
||||
}
|
||||
|
||||
@include it("should test the WCAG 2.1 boundary condition with color at threshold") {
|
||||
// Test case: Background color that produces contrast ratio close to 4.5:1
|
||||
// #777777 produces 4.4776:1 contrast ratio with white
|
||||
$test-background: #777; // Produces 4.4776:1 contrast ratio
|
||||
$contrast-ratio: contrast-ratio($test-background, $white);
|
||||
|
||||
// Verify the contrast ratio is below 4.5:1 threshold
|
||||
@include assert-true($contrast-ratio < 4.5, "Contrast ratio is below threshold, function should handle gracefully");
|
||||
}
|
||||
|
||||
@include it("should demonstrate the difference between > and >= operators") {
|
||||
// Test case: Demonstrates the difference between > and >= operators
|
||||
// Uses #767676 with a custom minimum contrast ratio that matches its actual ratio (4.5415)
|
||||
// With > 4.5415: should return black (fallback to highest available)
|
||||
// With >= 4.5415: should return white (meets threshold)
|
||||
|
||||
$test-background: #767676; // Produces 4.5415:1 contrast ratio
|
||||
$actual-ratio: contrast-ratio($test-background, $white);
|
||||
|
||||
// Test with a custom minimum that matches the actual ratio
|
||||
$result: color-contrast($test-background, $color-contrast-dark, $color-contrast-light, $actual-ratio);
|
||||
|
||||
// Should return white when using >= implementation
|
||||
@include assert-equal($result, $white, "color-contrast should return white when using exact ratio as threshold (>= implementation)");
|
||||
}
|
||||
|
||||
@include it("should test additional working colors above threshold") {
|
||||
// Test case: Background color that produces contrast ratio well above 4.5:1
|
||||
// #757575 produces 4.6047:1 contrast ratio with white text
|
||||
$test-background: #757575; // Produces 4.6047:1 contrast ratio
|
||||
$result: color-contrast($test-background);
|
||||
|
||||
@include assert-equal($result, $white, "Should return white for background with 4.6047:1 contrast ratio (well above threshold)");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# CHANGELOG
|
||||
|
||||
Este archivo documenta los cambios más relevantes realizados en cada versión. El formato está basado
|
||||
en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y las versiones se numeran siguiendo
|
||||
las reglas del [Versionado Semántico](https://semver.org/lang/es/).
|
||||
|
||||
Resume la evolución del proyecto para usuarios y colaboradores, destacando nuevas funcionalidades,
|
||||
correcciones, mejoras durante el desarrollo o cambios en la documentación. Cambios menores o
|
||||
internos pueden omitirse si no afectan al uso del proyecto.
|
||||
|
||||
## 0.3.1 (2025-09-20)
|
||||
|
||||
### Dependencias
|
||||
|
||||
- Actualiza dependencias para 0.4.0
|
||||
|
||||
### Documentado
|
||||
|
||||
- Normaliza referencias al nombre PageTop
|
||||
|
||||
## 0.3.0 (2025-08-16)
|
||||
|
||||
### Cambiado
|
||||
|
||||
- Mejora función `from_dir` por compatibilidad
|
||||
- Mejora la integración de archivos estáticos
|
||||
|
||||
### Documentado
|
||||
|
||||
- Cambia el formato para la documentación
|
||||
|
||||
## 0.2.0 (2025-08-09)
|
||||
|
||||
### Añadido
|
||||
|
||||
- Añade librería propia para gestionar recursos estáticos
|
||||
|
||||
### Otros cambios
|
||||
|
||||
- 🩹 Corrige enlace del botón de licencia en la documentación
|
||||
- 🚩 Afina Cargo.toml para buscar la mejor categoría
|
||||
|
||||
## 0.1.1 (2025-08-05)
|
||||
|
||||
- Depura la edición de CHANGELOGs y publicación de nuevas versiones
|
||||
|
||||
## 0.1.0 (2025-08-05)
|
||||
|
||||
- Versión inicial
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
[package]
|
||||
name = "pagetop-build"
|
||||
version = "0.3.1"
|
||||
version = "0.0.11"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el
|
||||
binario de un proyecto PageTop.
|
||||
description = """\
|
||||
Simplifies the process of embedding resources in PageTop app binaries.\
|
||||
"""
|
||||
categories = ["development-tools::build-utils"]
|
||||
categories = ["development-tools::build-utils", "web-programming"]
|
||||
keywords = ["pagetop", "build", "assets", "resources", "static"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
grass = "0.13"
|
||||
pagetop-statics.workspace = true
|
||||
grass = "0.13.4"
|
||||
static-files.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,201 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Build</h1>
|
||||
|
||||
<p>Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el binario de un proyecto <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-build)
|
||||
[](https://crates.io/crates/pagetop-build)
|
||||
[](https://crates.io/crates/pagetop-build)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build#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.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
Añadir en el archivo `Cargo.toml` del proyecto:
|
||||
|
||||
```toml
|
||||
[build-dependencies]
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_dir("./static", None)
|
||||
.with_name("guides")
|
||||
.build()
|
||||
}
|
||||
```
|
||||
|
||||
Si es necesario, se puede añadir un filtro para seleccionar archivos específicos de la carpeta, por
|
||||
ejemplo:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop_build::StaticFilesBundle;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
fn only_pdf_files(path: &Path) -> bool {
|
||||
// Selecciona únicamente los archivos con extensión `.pdf`.
|
||||
path.extension().map_or(false, |ext| ext == "pdf")
|
||||
}
|
||||
|
||||
StaticFilesBundle::from_dir("./static", Some(only_pdf_files))
|
||||
.with_name("guides")
|
||||
.build()
|
||||
}
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_scss("./styles/main.scss", "styles.min.css")
|
||||
.with_name("main_styles")
|
||||
.build()
|
||||
}
|
||||
```
|
||||
|
||||
Este código compila el archivo `main.scss` de la carpeta `static` del proyecto, y prepara un recurso
|
||||
llamado `main_styles` que contiene el archivo `styles.min.css` obtenido.
|
||||
|
||||
|
||||
# 📦 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)
|
||||
donde se incluye el código necesario para compilar el proyecto. Por ejemplo, para
|
||||
`with_name("guides")` se genera un archivo llamado `guides.rs`.
|
||||
|
||||
No hay ningún problema en generar más de un conjunto de recursos para cada proyecto siempre que se
|
||||
usen nombres diferentes.
|
||||
|
||||
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos
|
||||
en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.html)
|
||||
para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo:
|
||||
|
||||
```rust,ignore
|
||||
use pagetop::prelude::*;
|
||||
|
||||
pub struct MyExtension;
|
||||
|
||||
impl Extension for MyExtension {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# 🚧 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.
|
||||
|
|
@ -1,212 +1,150 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop Build</h1>
|
||||
|
||||
<p>Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el binario de un proyecto <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-build)
|
||||
[](https://crates.io/crates/pagetop-build)
|
||||
[](https://crates.io/crates/pagetop-build)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build#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.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
Añadir en el archivo `Cargo.toml` del proyecto:
|
||||
|
||||
```toml
|
||||
[build-dependencies]
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_dir("./static", None)
|
||||
.with_name("guides")
|
||||
.build()
|
||||
}
|
||||
```
|
||||
|
||||
Si es necesario, se puede añadir un filtro para seleccionar archivos específicos de la carpeta, por
|
||||
ejemplo:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop_build::StaticFilesBundle;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
fn only_pdf_files(path: &Path) -> bool {
|
||||
// Selecciona únicamente los archivos con extensión `.pdf`.
|
||||
path.extension().map_or(false, |ext| ext == "pdf")
|
||||
}
|
||||
|
||||
StaticFilesBundle::from_dir("./static", Some(only_pdf_files))
|
||||
.with_name("guides")
|
||||
.build()
|
||||
}
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_scss("./styles/main.scss", "styles.min.css")
|
||||
.with_name("main_styles")
|
||||
.build()
|
||||
}
|
||||
```
|
||||
|
||||
Este código compila el archivo `main.scss` de la carpeta `static` del proyecto, y prepara un recurso
|
||||
llamado `main_styles` que contiene el archivo `styles.min.css` obtenido.
|
||||
|
||||
|
||||
# 📦 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)
|
||||
donde se incluye el código necesario para compilar el proyecto. Por ejemplo, para
|
||||
`with_name("guides")` se genera un archivo llamado `guides.rs`.
|
||||
|
||||
No hay ningún problema en generar más de un conjunto de recursos para cada proyecto siempre que se
|
||||
usen nombres diferentes.
|
||||
|
||||
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos
|
||||
en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.html)
|
||||
para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo:
|
||||
|
||||
```rust,ignore
|
||||
use pagetop::prelude::*;
|
||||
|
||||
pub struct MyExtension;
|
||||
|
||||
impl Extension for MyExtension {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
||||
#![doc(
|
||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||
)]
|
||||
//! Provide an easy way to embed static files or compiled SCSS files into your binary at compile
|
||||
//! time.
|
||||
//!
|
||||
//! ## Adding to your project
|
||||
//!
|
||||
//! Add the following to your `Cargo.toml`:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [build-dependencies]
|
||||
//! pagetop-build = { ... }
|
||||
//! ```
|
||||
//!
|
||||
//! Next, create a `build.rs` file to configure how your static resources or SCSS files will be
|
||||
//! bundled in your PageTop application, package, or theme.
|
||||
//!
|
||||
//! ## Usage examples
|
||||
//!
|
||||
//! ### 1. Embedding static files from a directory
|
||||
//!
|
||||
//! Include all files from a directory:
|
||||
//!
|
||||
//! ```rust#ignore
|
||||
//! use pagetop_build::StaticFilesBundle;
|
||||
//!
|
||||
//! fn main() -> std::io::Result<()> {
|
||||
//! StaticFilesBundle::from_dir("./static", None)
|
||||
//! .with_name("guides")
|
||||
//! .build()
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Apply a filter to include only specific files:
|
||||
//!
|
||||
//! ```rust#ignore
|
||||
//! use pagetop_build::StaticFilesBundle;
|
||||
//! use std::path::Path;
|
||||
//!
|
||||
//! fn main() -> std::io::Result<()> {
|
||||
//! fn only_css_files(path: &Path) -> bool {
|
||||
//! // Include only files with `.css` extension.
|
||||
//! path.extension().map_or(false, |ext| ext == "css")
|
||||
//! }
|
||||
//!
|
||||
//! StaticFilesBundle::from_dir("./static", Some(only_css_files))
|
||||
//! .with_name("guides")
|
||||
//! .build()
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### 2. Compiling SCSS files to CSS
|
||||
//!
|
||||
//! Compile a SCSS file into CSS and embed it:
|
||||
//!
|
||||
//! ```rust#ignore
|
||||
//! use pagetop_build::StaticFilesBundle;
|
||||
//!
|
||||
//! fn main() -> std::io::Result<()> {
|
||||
//! StaticFilesBundle::from_scss("./styles/main.scss", "main.css")
|
||||
//! .with_name("main_styles")
|
||||
//! .build()
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This compiles the `main.scss` file, including all imported SCSS files, into `main.css`. All
|
||||
//! imports are resolved automatically, and the result is accessible within the binary file.
|
||||
//!
|
||||
//! ## Generated module
|
||||
//!
|
||||
//! [`StaticFilesBundle`] generates a file in the standard directory
|
||||
//! [OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html) where all
|
||||
//! intermediate and output artifacts are placed during compilation. For example, if you use
|
||||
//! `with_name("guides")`, it generates a file named `guides.rs`:
|
||||
//!
|
||||
//! You don't need to access this file, just include it in your project using the builder name as an
|
||||
//! identifier:
|
||||
//!
|
||||
//! ```rust#ignore
|
||||
//! use pagetop::prelude::*;
|
||||
//!
|
||||
//! include_files!(guides);
|
||||
//! ```
|
||||
//!
|
||||
//! Or, access the entire bundle as a global static `HashMap`:
|
||||
//!
|
||||
//! ```rust#ignore
|
||||
//! use pagetop::prelude::*;
|
||||
//!
|
||||
//! include_files!(guides => BUNDLE_GUIDES);
|
||||
//! ```
|
||||
//!
|
||||
//! You can build more than one resources file to compile with your project.
|
||||
|
||||
use grass::{from_path, Options, OutputStyle};
|
||||
use pagetop_statics::{resource_dir, ResourceDir};
|
||||
use static_files::{resource_dir, ResourceDir};
|
||||
|
||||
use std::fs::{create_dir_all, remove_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
/// Prepara un conjunto de recursos para ser incluidos en el binario del proyecto.
|
||||
/// Generates the resources to embed at compile time using
|
||||
/// [static_files](https://docs.rs/static-files/latest/static_files/).
|
||||
pub struct StaticFilesBundle {
|
||||
resource_dir: ResourceDir,
|
||||
}
|
||||
|
||||
impl StaticFilesBundle {
|
||||
/// Prepara el conjunto de recursos con los archivos de un directorio. Opcionalmente se puede
|
||||
/// aplicar un filtro para seleccionar un subconjunto de los archivos.
|
||||
/// Creates a bundle from a directory of static files, with an optional filter.
|
||||
///
|
||||
/// # Argumentos
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `dir` - Directorio que contiene los archivos.
|
||||
/// * `filter` - Una función opcional para aceptar o no un archivo según su ruta.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop_build::StaticFilesBundle;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// fn only_images(path: &Path) -> bool {
|
||||
/// matches!(
|
||||
/// path.extension().and_then(|ext| ext.to_str()),
|
||||
/// Some("jpg" | "png" | "gif")
|
||||
/// )
|
||||
/// }
|
||||
///
|
||||
/// StaticFilesBundle::from_dir("./static", Some(only_images))
|
||||
/// .with_name("images")
|
||||
/// .build()
|
||||
/// }
|
||||
/// ```
|
||||
pub fn from_dir<P>(dir: P, filter: Option<fn(&Path) -> bool>) -> Self
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let dir_path = dir.as_ref();
|
||||
let dir_str = dir_path.to_str().unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Resource directory path is not valid UTF-8: {}",
|
||||
dir_path.display()
|
||||
);
|
||||
});
|
||||
/// * `dir` - The directory containing the static files.
|
||||
/// * `filter` - An optional function to filter files or directories to include.
|
||||
pub fn from_dir(dir: &'static str, filter: Option<fn(p: &Path) -> bool>) -> Self {
|
||||
let mut resource_dir = resource_dir(dir);
|
||||
|
||||
let mut resource_dir = resource_dir(dir_str);
|
||||
|
||||
// Aplica el filtro si está definido.
|
||||
// Apply the filter if provided.
|
||||
if let Some(f) = filter {
|
||||
resource_dir.with_filter(f);
|
||||
}
|
||||
|
||||
// Identifica el directorio temporal de recursos.
|
||||
StaticFilesBundle { resource_dir }
|
||||
}
|
||||
|
||||
/// Prepara un recurso CSS minimizado a partir de la compilación de un archivo SCSS (que puede a
|
||||
/// su vez importar otros archivos SCSS).
|
||||
/// Creates a bundle starting from a SCSS file.
|
||||
///
|
||||
/// # Argumentos
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Archivo SCSS a compilar.
|
||||
/// * `target_name` - Nombre para el archivo CSS.
|
||||
/// * `path` - The SCSS file to compile.
|
||||
/// * `target_name` - The name for the CSS file in the bundle.
|
||||
///
|
||||
/// # Ejemplo
|
||||
/// This function will panic:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop_build::StaticFilesBundle;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// StaticFilesBundle::from_scss("./bootstrap/scss/main.scss", "bootstrap.min.css")
|
||||
/// .with_name("bootstrap_css")
|
||||
/// .build()
|
||||
/// }
|
||||
/// ```
|
||||
/// * If the environment variable `OUT_DIR` is not set.
|
||||
/// * If it is unable to create a temporary directory in the `OUT_DIR`.
|
||||
/// * If the SCSS file cannot be compiled due to syntax errors in the SCSS file or missing
|
||||
/// dependencies or import paths required for compilation.
|
||||
/// * If it is unable to create the output CSS file in the temporary directory due to an invalid
|
||||
/// `target_name` or insufficient permissions to create files in the temporary directory.
|
||||
/// * If the function fails to write the compiled CSS content to the file.
|
||||
pub fn from_scss<P>(path: P, target_name: &str) -> Self
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
// Crea un directorio temporal para el archivo CSS.
|
||||
// Create a temporary directory for the CSS file.
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
let temp_dir = Path::new(&out_dir).join("from_scss_files");
|
||||
|
||||
// Limpia el directorio temporal de ejecuciones previas, si existe.
|
||||
// Clean up the temporary directory from previous runs, if it exists.
|
||||
if temp_dir.exists() {
|
||||
remove_dir_all(&temp_dir).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
|
|
@ -222,7 +160,7 @@ impl StaticFilesBundle {
|
|||
);
|
||||
});
|
||||
|
||||
// Compila SCSS a CSS.
|
||||
// Compile SCSS to CSS.
|
||||
let css_content = from_path(
|
||||
path.as_ref(),
|
||||
&Options::default().style(OutputStyle::Compressed),
|
||||
|
|
@ -234,22 +172,31 @@ impl StaticFilesBundle {
|
|||
)
|
||||
});
|
||||
|
||||
// Guarda el archivo CSS compilado en el directorio temporal.
|
||||
// Write the compiled CSS to the temporary directory.
|
||||
let css_path = temp_dir.join(target_name);
|
||||
File::create(&css_path)
|
||||
.unwrap_or_else(|_| panic!("Failed to create CSS file `{}`", css_path.display()))
|
||||
.expect(&format!(
|
||||
"Failed to create CSS file `{}`",
|
||||
css_path.display()
|
||||
))
|
||||
.write_all(css_content.as_bytes())
|
||||
.unwrap_or_else(|_| panic!("Failed to write CSS content to `{}`", css_path.display()));
|
||||
.expect(&format!(
|
||||
"Failed to write CSS content to `{}`",
|
||||
css_path.display()
|
||||
));
|
||||
|
||||
// Identifica el directorio temporal de recursos.
|
||||
// Initialize ResourceDir with the temporary directory.
|
||||
StaticFilesBundle {
|
||||
resource_dir: resource_dir(temp_dir.to_str().unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Asigna un nombre al conjunto de recursos.
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
let name = name.as_ref();
|
||||
/// Configures the name for the bundle of static files.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if the standard `OUT_DIR` environment variable is not set.
|
||||
pub fn with_name(mut self, name: &'static str) -> Self {
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
let filename = Path::new(&out_dir).join(format!("{name}.rs"));
|
||||
self.resource_dir.with_generated_filename(filename);
|
||||
|
|
@ -258,7 +205,12 @@ impl StaticFilesBundle {
|
|||
self
|
||||
}
|
||||
|
||||
/// Contruye finalmente el conjunto de recursos para incluir en el binario de la aplicación.
|
||||
/// Builds the bundle.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if there is an issue with I/O operations, such as failing
|
||||
/// to read or write to a file.
|
||||
pub fn build(self) -> std::io::Result<()> {
|
||||
self.resource_dir.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
# CHANGELOG
|
||||
|
||||
Este archivo documenta los cambios más relevantes realizados en cada versión. El formato está basado
|
||||
en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y las versiones se numeran siguiendo
|
||||
las reglas del [Versionado Semántico](https://semver.org/lang/es/).
|
||||
|
||||
Resume la evolución del proyecto para usuarios y colaboradores, destacando nuevas funcionalidades,
|
||||
correcciones, mejoras durante el desarrollo o cambios en la documentación. Cambios menores o
|
||||
internos pueden omitirse si no afectan al uso del proyecto.
|
||||
|
||||
## 0.2.0 (2025-09-20)
|
||||
|
||||
### Cambiado
|
||||
|
||||
- Retoques en el código
|
||||
- Majora la validación de `builder_fn`
|
||||
|
||||
### Dependencias
|
||||
|
||||
- Actualiza dependencias para 0.4.0
|
||||
|
||||
### Documentado
|
||||
|
||||
- Normaliza referencias al nombre PageTop
|
||||
|
||||
### Otros cambios
|
||||
|
||||
- 🚨 Ajustes menores sugeridos por clippy
|
||||
|
||||
## 0.1.1 (2025-08-16)
|
||||
|
||||
### Documentado
|
||||
|
||||
- Cambia el formato para la documentación (#4)
|
||||
- Corrige enlaces de licencia en la documentación
|
||||
|
||||
### Otros cambios
|
||||
|
||||
- Afina Cargo.toml para buscar la mejor categoría
|
||||
|
||||
## 0.1.0 (2025-08-06)
|
||||
|
||||
- Versión inicial
|
||||
|
|
@ -1,24 +1,25 @@
|
|||
[package]
|
||||
name = "pagetop-macros"
|
||||
version = "0.2.0"
|
||||
version = "0.0.13"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Una colección de macros que mejoran la experiencia de desarrollo con PageTop.
|
||||
description = """\
|
||||
A collection of macros that boost PageTop development.\
|
||||
"""
|
||||
categories = ["development-tools::procedural-macro-helpers"]
|
||||
categories = ["development-tools::procedural-macro-helpers", "web-programming"]
|
||||
keywords = ["pagetop", "macros", "proc-macros", "codegen"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
proc-macro2-diagnostics = { version = "0.10", default-features = false }
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
proc-macro2 = "1.0.89"
|
||||
proc-macro-crate = "3.2.0"
|
||||
proc-macro-error = "1.0.4"
|
||||
quote = "1.0.37"
|
||||
syn = { version = "2.0.87", features = ["full"] }
|
||||
|
|
|
|||
|
|
@ -1,201 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Macros</h1>
|
||||
|
||||
<p>Una colección de macros que mejoran la experiencia de desarrollo con <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-macros)
|
||||
[](https://crates.io/crates/pagetop-macros)
|
||||
[](https://crates.io/crates/pagetop-macros)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros#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.
|
||||
|
||||
## Créditos
|
||||
|
||||
Esta librería 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
|
||||
[SmartDefault](https://crates.io/crates/smart_default) (0.7.1) de
|
||||
[Jane Doe](https://crates.io/users/jane-doe), llamada `AutoDefault`. Estas macros eliminan la
|
||||
necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de
|
||||
cada proyecto PageTop.
|
||||
|
||||
|
||||
# 🚧 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.
|
||||
|
|
@ -1,109 +1,119 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop Macros</h1>
|
||||
|
||||
<p>Una colección de macros que mejoran la experiencia de desarrollo con <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-macros)
|
||||
[](https://crates.io/crates/pagetop-macros)
|
||||
[](https://crates.io/crates/pagetop-macros)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros#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.
|
||||
|
||||
## Créditos
|
||||
|
||||
Esta librería 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
|
||||
[SmartDefault](https://crates.io/crates/smart_default) (0.7.1) de
|
||||
[Jane Doe](https://crates.io/users/jane-doe), llamada `AutoDefault`. Estas macros eliminan la
|
||||
necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de
|
||||
cada proyecto PageTop.
|
||||
*/
|
||||
|
||||
#![doc(
|
||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||
)]
|
||||
|
||||
mod maud;
|
||||
mod smart_default;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::{parse_macro_input, spanned::Spanned, DeriveInput};
|
||||
use proc_macro_error::proc_macro_error;
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::{parse_macro_input, parse_str, DeriveInput, ItemFn};
|
||||
|
||||
/// Macro attribute to generate builder methods from `set_` methods.
|
||||
///
|
||||
/// This macro takes a method with the `set_` prefix and generates a corresponding method with the
|
||||
/// `with_` prefix to use in the builder pattern.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if a parameter identifier is not found in the argument list.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// #[fn_builder]
|
||||
/// pub fn set_example(&mut self) -> &mut Self {
|
||||
/// // implementation
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Will generate:
|
||||
///
|
||||
/// ```
|
||||
/// pub fn with_example(mut self) -> Self {
|
||||
/// self.set_example();
|
||||
/// self
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn fn_builder(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let fn_set = parse_macro_input!(item as ItemFn);
|
||||
let fn_set_name = fn_set.sig.ident.to_string();
|
||||
|
||||
if !fn_set_name.starts_with("set_") {
|
||||
let expanded = quote_spanned! {
|
||||
fn_set.sig.ident.span() =>
|
||||
compile_error!("expected a \"pub fn set_...() -> &mut Self\" method");
|
||||
};
|
||||
return expanded.into();
|
||||
}
|
||||
|
||||
let fn_with_name = fn_set_name.replace("set_", "with_");
|
||||
let fn_with_generics = if fn_set.sig.generics.params.is_empty() {
|
||||
fn_with_name.clone()
|
||||
} else {
|
||||
let g = &fn_set.sig.generics;
|
||||
format!("{fn_with_name}{}", quote! { #g }.to_string())
|
||||
};
|
||||
|
||||
let where_clause = fn_set
|
||||
.sig
|
||||
.generics
|
||||
.where_clause
|
||||
.as_ref()
|
||||
.map_or(String::new(), |where_clause| {
|
||||
format!("{} ", quote! { #where_clause }.to_string())
|
||||
});
|
||||
|
||||
let args: Vec<String> = fn_set
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|arg| arg.to_token_stream().to_string())
|
||||
.collect();
|
||||
|
||||
let params: Vec<String> = args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
arg.split_whitespace()
|
||||
.next()
|
||||
.unwrap()
|
||||
.trim_end_matches(':')
|
||||
.to_string()
|
||||
})
|
||||
.collect();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let fn_with = parse_str::<ItemFn>(format!(r#"
|
||||
pub fn {fn_with_generics}(mut self, {}) -> Self {where_clause} {{
|
||||
self.{fn_set_name}({});
|
||||
self
|
||||
}}
|
||||
"#, args.join(", "), params.join(", ")
|
||||
).as_str()).unwrap();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let fn_set_doc = format!(r##"
|
||||
<p id="method.{fn_with_name}" style="margin-bottom: 12px;">Use
|
||||
<code class="code-header">pub fn <span class="fn" href="#method.{fn_with_name}">{fn_with_name}</span>(self, …) -> Self</code>
|
||||
for the <a href="#method.new">builder pattern</a>.
|
||||
</p>
|
||||
"##);
|
||||
|
||||
let expanded = quote! {
|
||||
#[doc(hidden)]
|
||||
#fn_with
|
||||
#[inline]
|
||||
#[doc = #fn_set_doc]
|
||||
#fn_set
|
||||
};
|
||||
expanded.into()
|
||||
}
|
||||
|
||||
/// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)).
|
||||
#[proc_macro]
|
||||
#[proc_macro_error]
|
||||
pub fn html(input: TokenStream) -> TokenStream {
|
||||
maud::expand(input.into()).into()
|
||||
}
|
||||
|
||||
/// Deriva [`Default`] con atributos personalizados (basada en
|
||||
/// [SmartDefault](https://docs.rs/smart-default)).
|
||||
///
|
||||
/// Al derivar una estructura con *AutoDefault* se genera automáticamente la implementación de
|
||||
/// [`Default`]. Aunque, a diferencia de un simple `#[derive(Default)]`, el atributo
|
||||
/// `#[derive(AutoDefault)]` permite usar anotaciones en los campos como `#[default = "..."]`,
|
||||
/// funcionando incluso en estructuras con campos que no implementan [`Default`] o en *enums*.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_macros::AutoDefault;
|
||||
/// # fn main() {
|
||||
/// #[derive(AutoDefault)]
|
||||
/// # #[derive(PartialEq)]
|
||||
/// # #[allow(dead_code)]
|
||||
/// enum Foo {
|
||||
/// Bar,
|
||||
/// #[default]
|
||||
/// Baz {
|
||||
/// #[default = 12]
|
||||
/// a: i32,
|
||||
/// b: i32,
|
||||
/// #[default(Some(Default::default()))]
|
||||
/// c: Option<i32>,
|
||||
/// #[default(_code = "vec![1, 2, 3]")]
|
||||
/// d: Vec<u32>,
|
||||
/// #[default = "four"]
|
||||
/// e: String,
|
||||
/// },
|
||||
/// Qux(i32),
|
||||
/// }
|
||||
///
|
||||
/// assert!(Foo::default() == Foo::Baz {
|
||||
/// a: 12,
|
||||
/// b: 0,
|
||||
/// c: Some(0),
|
||||
/// d: vec![1, 2, 3],
|
||||
/// e: "four".to_owned(),
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// * `Baz` tiene el atributo `#[default]`. Esto significa que el valor por defecto de `Foo` es
|
||||
/// `Foo::Baz`. Solo una variante puede tener el atributo `#[default]`, y dicho atributo no debe
|
||||
/// tener ningún valor asociado.
|
||||
/// * `a` tiene el atributo `#[default = 12]`. Esto significa que su valor por defecto es `12`.
|
||||
/// * `b` no tiene ningún atributo `#[default = ...]`. Su valor por defecto será, por tanto, el
|
||||
/// valor por defecto de `i32`, es decir, `0`.
|
||||
/// * `c` es un `Option<i32>`, y su valor por defecto es `Some(Default::default())`. Rust no puede
|
||||
/// (actualmente) analizar `#[default = Some(Default::default())]`, pero podemos escribir
|
||||
/// `#[default(Some(Default::default))]`.
|
||||
/// * `d` contiene el token `!`, que (actualmente) no puede ser analizado ni siquiera usando
|
||||
/// `#[default(...)]`, así que debemos codificarlo como una cadena y marcarlo con `_code =`.
|
||||
/// * `e` es un `String`, por lo que el literal de cadena `"four"` se convierte automáticamente en
|
||||
/// él. Esta conversión automática **solo** ocurre con literales de cadena (o de bytes), y solo si
|
||||
/// no se usa `_code`.
|
||||
#[proc_macro_derive(AutoDefault, attributes(default))]
|
||||
pub fn derive_auto_default(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
|
@ -113,285 +123,13 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
/// Macro (*attribute*) que asocia un método *builder* `with_` con un método `alter_`.
|
||||
/// Marks async main function as the `PageTop` entry-point.
|
||||
///
|
||||
/// La macro añade automáticamente un método `alter_` que permite modificar la instancia actual
|
||||
/// usando `&mut self`; y redefine el método *builder* `with_`, que consume `mut self`, para delegar
|
||||
/// la lógica al nuevo método `alter_`, reutilizando así la misma implementación.
|
||||
///
|
||||
/// Esta macro emitirá un error en tiempo de compilación si la función anotada no cumple con la
|
||||
/// firma esperada para el método *builder*: `pub fn with_...(mut self, ...) -> Self`.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// Si defines un método `with_` como este:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_macros::builder_fn;
|
||||
/// # struct Example {value: Option<String>};
|
||||
/// # impl Example {
|
||||
/// #[builder_fn]
|
||||
/// pub fn with_example(mut self, value: impl Into<String>) -> Self {
|
||||
/// self.value = Some(value.into());
|
||||
/// self
|
||||
/// }
|
||||
/// # }
|
||||
/// # Examples
|
||||
/// ```
|
||||
///
|
||||
/// la macro rescribirá el método `with_` y generará un nuevo método `alter_`:
|
||||
///
|
||||
/// ```rust
|
||||
/// # struct Example {value: Option<String>};
|
||||
/// # impl Example {
|
||||
/// #[inline]
|
||||
/// pub fn with_example(mut self, value: impl Into<String>) -> Self {
|
||||
/// self.alter_example(value);
|
||||
/// self
|
||||
/// }
|
||||
///
|
||||
/// pub fn alter_example(&mut self, value: impl Into<String>) -> &mut Self {
|
||||
/// self.value = Some(value.into());
|
||||
/// self
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// De esta forma, cada método *builder* `with_...()` generará automáticamente su correspondiente
|
||||
/// método `alter_...()` para dejar modificar instancias existentes.
|
||||
#[proc_macro_attribute]
|
||||
pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||
use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type};
|
||||
|
||||
let ts: proc_macro2::TokenStream = item.clone().into();
|
||||
|
||||
enum Kind {
|
||||
Impl(ImplItemFn),
|
||||
Trait(TraitItemFn),
|
||||
}
|
||||
|
||||
// Detecta si estamos en `impl` o `trait`.
|
||||
let kind = if let Ok(it) = parse2::<ImplItemFn>(ts.clone()) {
|
||||
Kind::Impl(it)
|
||||
} else if let Ok(tt) = parse2::<TraitItemFn>(ts.clone()) {
|
||||
Kind::Trait(tt)
|
||||
} else {
|
||||
return quote! {
|
||||
compile_error!("#[builder_fn] only supports methods in `impl` blocks or `trait` items");
|
||||
}
|
||||
.into();
|
||||
};
|
||||
|
||||
// Extrae piezas comunes (sig, attrs, vis, bloque?, es_trait?).
|
||||
let (sig, attrs, vis, body_opt, is_trait) = match &kind {
|
||||
Kind::Impl(m) => (&m.sig, &m.attrs, Some(&m.vis), Some(&m.block), false),
|
||||
Kind::Trait(t) => (&t.sig, &t.attrs, None, t.default.as_ref(), true),
|
||||
};
|
||||
|
||||
let with_name = sig.ident.clone();
|
||||
let with_name_str = sig.ident.to_string();
|
||||
|
||||
// Valida el nombre del método.
|
||||
if !with_name_str.starts_with("with_") {
|
||||
return quote_spanned! {
|
||||
sig.ident.span() => compile_error!("expected a named `with_...()` method");
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
// Sólo se exige `pub` en `impl` (en `trait` no aplica).
|
||||
let vis_pub = match (is_trait, vis) {
|
||||
(false, Some(v)) => quote! { #v },
|
||||
_ => quote! {},
|
||||
};
|
||||
|
||||
// Validaciones comunes.
|
||||
if sig.asyncness.is_some() {
|
||||
return quote_spanned! {
|
||||
sig.asyncness.span() => compile_error!("`with_...()` cannot be `async`");
|
||||
}
|
||||
.into();
|
||||
}
|
||||
if sig.constness.is_some() {
|
||||
return quote_spanned! {
|
||||
sig.constness.span() => compile_error!("`with_...()` cannot be `const`");
|
||||
}
|
||||
.into();
|
||||
}
|
||||
if sig.abi.is_some() {
|
||||
return quote_spanned! {
|
||||
sig.abi.span() => compile_error!("`with_...()` cannot be `extern`");
|
||||
}
|
||||
.into();
|
||||
}
|
||||
if sig.unsafety.is_some() {
|
||||
return quote_spanned! {
|
||||
sig.unsafety.span() => compile_error!("`with_...()` cannot be `unsafe`");
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
// En `impl` se exige exactamente `mut self`; y en `trait` se exige `self` (sin &).
|
||||
let receiver_ok = match sig.inputs.first() {
|
||||
Some(FnArg::Receiver(r)) => {
|
||||
// Rechaza `self: SomeType`.
|
||||
if r.colon_token.is_some() {
|
||||
false
|
||||
} else if is_trait {
|
||||
// Exactamente `self` (sin &, sin mut).
|
||||
r.reference.is_none() && r.mutability.is_none()
|
||||
} else {
|
||||
// Exactamente `mut self`.
|
||||
r.reference.is_none() && r.mutability.is_some()
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !receiver_ok {
|
||||
let msg = if is_trait {
|
||||
"expected `self` (not `mut self`, `&self` or `&mut self`) in trait method"
|
||||
} else {
|
||||
"expected first argument to be exactly `mut self`"
|
||||
};
|
||||
let err = sig
|
||||
.inputs
|
||||
.first()
|
||||
.map(|a| a.span())
|
||||
.unwrap_or(sig.ident.span());
|
||||
return quote_spanned! {
|
||||
err => compile_error!(#msg);
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
// Valida que el método devuelve exactamente `Self`.
|
||||
match &sig.output {
|
||||
ReturnType::Type(_, ty) => match ty.as_ref() {
|
||||
Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self") => {}
|
||||
_ => {
|
||||
return quote_spanned! {
|
||||
ty.span() => compile_error!("expected return type to be exactly `Self`");
|
||||
}
|
||||
.into();
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return quote_spanned! {
|
||||
sig.output.span() => compile_error!("expected return type to be exactly `Self`");
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
// Genera el nombre del método alter_...().
|
||||
let stem = with_name_str.strip_prefix("with_").expect("validated");
|
||||
let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span());
|
||||
|
||||
// Extrae genéricos y cláusulas where.
|
||||
let generics = &sig.generics;
|
||||
let where_clause = &sig.generics.where_clause;
|
||||
|
||||
// Extrae identificadores de los argumentos para la llamada (sin `mut` ni patrones complejos).
|
||||
let args: Vec<_> = sig.inputs.iter().skip(1).collect();
|
||||
let call_idents: Vec<Ident> = {
|
||||
let mut v = Vec::new();
|
||||
for arg in sig.inputs.iter().skip(1) {
|
||||
match arg {
|
||||
FnArg::Typed(pat) => {
|
||||
if let Pat::Ident(pat_ident) = pat.pat.as_ref() {
|
||||
v.push(pat_ident.ident.clone());
|
||||
} else {
|
||||
return quote_spanned! {
|
||||
pat.pat.span() => compile_error!(
|
||||
"each parameter must be a simple identifier, e.g. `value: T`"
|
||||
);
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return quote_spanned! {
|
||||
arg.span() => compile_error!("unexpected receiver in parameter list");
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
};
|
||||
|
||||
// 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();
|
||||
|
||||
// Documentación del método alter_...().
|
||||
let doc = format!("Equivale a [`Self::{with_name_str}()`], pero fuera del patrón *builder*.");
|
||||
|
||||
// Genera el código final.
|
||||
let expanded = match body_opt {
|
||||
None => {
|
||||
quote! {
|
||||
#(#attrs)*
|
||||
fn #with_name #generics (self, #(#args),*) -> Self #where_clause;
|
||||
|
||||
#(#non_doc_or_inline_attrs)*
|
||||
#[doc = #doc]
|
||||
fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause;
|
||||
}
|
||||
}
|
||||
Some(body) => {
|
||||
// Si no se indicó ninguna forma de `inline`, fuerza `#[inline]` para `with_...()`.
|
||||
let force_inline = if attrs.iter().any(|a| a.path().is_ident("inline")) {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { #[inline] }
|
||||
};
|
||||
let with_fn = if is_trait {
|
||||
quote! {
|
||||
#force_inline
|
||||
#vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause {
|
||||
let mut s = self;
|
||||
s.#alter_ident(#(#call_idents),*);
|
||||
s
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#force_inline
|
||||
#vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause {
|
||||
self.#alter_ident(#(#call_idents),*);
|
||||
self
|
||||
}
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#(#attrs)*
|
||||
#with_fn
|
||||
|
||||
#(#non_doc_or_inline_attrs)*
|
||||
#[doc = #doc]
|
||||
#vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause {
|
||||
#body
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
expanded.into()
|
||||
}
|
||||
|
||||
/// Define una función `main` asíncrona como punto de entrada de PageTop.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[pagetop::main]
|
||||
/// async fn main() {
|
||||
/// async { println!("Hello world!"); }.await
|
||||
/// async { println!("Hello world"); }.await
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
|
|
@ -405,11 +143,10 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
output
|
||||
}
|
||||
|
||||
/// Define funciones de prueba asíncronas para usar con PageTop.
|
||||
/// Marks async test functions to use the `PageTop` entry-point.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// #[pagetop::test]
|
||||
/// async fn test() {
|
||||
/// assert_eq!(async { "Hello world" }.await, "Hello world");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// #![doc(html_root_url = "https://docs.rs/maud_macros/0.27.0")]
|
||||
// #![doc(html_root_url = "https://docs.rs/maud_macros/0.25.0")]
|
||||
// TokenStream values are reference counted, and the mental overhead of tracking
|
||||
// lifetimes outweighs the marginal gains from explicit borrowing
|
||||
// #![allow(clippy::needless_pass_by_value)]
|
||||
|
|
@ -6,45 +6,34 @@
|
|||
mod ast;
|
||||
mod escape;
|
||||
mod generate;
|
||||
mod parse;
|
||||
|
||||
use ast::DiagnosticParse;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use proc_macro2_diagnostics::Diagnostic;
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use proc_macro_crate::{crate_name, FoundCrate};
|
||||
use quote::quote;
|
||||
use syn::parse::{ParseStream, Parser};
|
||||
|
||||
pub fn expand(input: TokenStream) -> TokenStream {
|
||||
let output_ident = TokenTree::Ident(Ident::new("__maud_output", Span::mixed_site()));
|
||||
// Heuristic: the size of the resulting markup tends to correlate with the
|
||||
// code size of the template itself
|
||||
let size_hint = input.to_string().len();
|
||||
|
||||
let mut diagnostics = Vec::new();
|
||||
let markups = match Parser::parse2(
|
||||
|input: ParseStream| ast::Markups::diagnostic_parse(input, &mut diagnostics),
|
||||
input,
|
||||
) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
let err = err.to_compile_error();
|
||||
let diag_tokens = diagnostics.into_iter().map(Diagnostic::emit_as_expr_tokens);
|
||||
|
||||
return quote! {{
|
||||
#err
|
||||
#(#diag_tokens)*
|
||||
}};
|
||||
}
|
||||
};
|
||||
|
||||
let diag_tokens = diagnostics.into_iter().map(Diagnostic::emit_as_expr_tokens);
|
||||
|
||||
let output_ident = Ident::new("__maud_output", Span::mixed_site());
|
||||
let markups = parse::parse(input);
|
||||
let stmts = generate::generate(markups, output_ident.clone());
|
||||
|
||||
quote! {{
|
||||
let found_crate = crate_name("pagetop").expect("pagetop is present in `Cargo.toml`");
|
||||
let pre_escaped = match found_crate {
|
||||
FoundCrate::Itself => quote!(
|
||||
crate::html::PreEscaped(#output_ident)
|
||||
),
|
||||
_ => quote!(
|
||||
pagetop::html::PreEscaped(#output_ident)
|
||||
),
|
||||
};
|
||||
|
||||
quote!({
|
||||
extern crate alloc;
|
||||
let mut #output_ident = alloc::string::String::with_capacity(#size_hint);
|
||||
#stmts
|
||||
#(#diag_tokens)*
|
||||
pagetop::html::PreEscaped(#output_ident)
|
||||
}}
|
||||
#pre_escaped
|
||||
})
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,10 @@
|
|||
// !!!!!!!! PLEASE KEEP THIS IN SYNC WITH `maud/src/escape.rs` !!!!!!!!!
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::string::String;
|
||||
|
||||
pub fn escape_to_string(input: &str, output: &mut String) {
|
||||
for b in input.bytes() {
|
||||
match b {
|
||||
|
|
@ -16,7 +20,10 @@ pub fn escape_to_string(input: &str, output: &mut String) {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
extern crate alloc;
|
||||
|
||||
use super::escape_to_string;
|
||||
use alloc::string::String;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{parse_quote, token::Brace, Expr, Local};
|
||||
use proc_macro2::{Delimiter, Group, Ident, Literal, Span, TokenStream, TokenTree};
|
||||
use proc_macro_error::SpanRange;
|
||||
use quote::quote;
|
||||
|
||||
use crate::maud::{ast::*, escape};
|
||||
|
||||
pub fn generate(markups: Markups<Element>, output_ident: Ident) -> TokenStream {
|
||||
use proc_macro_crate::{crate_name, FoundCrate};
|
||||
|
||||
pub fn generate(markups: Vec<Markup>, output_ident: TokenTree) -> TokenStream {
|
||||
let mut build = Builder::new(output_ident.clone());
|
||||
Generator::new(output_ident).markups(markups, &mut build);
|
||||
build.finish()
|
||||
}
|
||||
|
||||
struct Generator {
|
||||
output_ident: Ident,
|
||||
output_ident: TokenTree,
|
||||
}
|
||||
|
||||
impl Generator {
|
||||
fn new(output_ident: Ident) -> Generator {
|
||||
fn new(output_ident: TokenTree) -> Generator {
|
||||
Generator { output_ident }
|
||||
}
|
||||
|
||||
|
|
@ -23,94 +25,129 @@ impl Generator {
|
|||
Builder::new(self.output_ident.clone())
|
||||
}
|
||||
|
||||
fn markups<E: Into<Element>>(&self, markups: Markups<E>, build: &mut Builder) {
|
||||
for markup in markups.markups {
|
||||
fn markups(&self, markups: Vec<Markup>, build: &mut Builder) {
|
||||
for markup in markups {
|
||||
self.markup(markup, build);
|
||||
}
|
||||
}
|
||||
|
||||
fn markup<E: Into<Element>>(&self, markup: Markup<E>, build: &mut Builder) {
|
||||
fn markup(&self, markup: Markup, build: &mut Builder) {
|
||||
match markup {
|
||||
Markup::Block(block) => {
|
||||
if block.markups.markups.iter().any(|markup| {
|
||||
matches!(
|
||||
*markup,
|
||||
Markup::ControlFlow(ControlFlow {
|
||||
kind: ControlFlowKind::Let(_),
|
||||
..
|
||||
})
|
||||
)
|
||||
}) {
|
||||
self.block(block, build);
|
||||
Markup::ParseError { .. } => {}
|
||||
Markup::Block(Block {
|
||||
markups,
|
||||
outer_span,
|
||||
}) => {
|
||||
if markups
|
||||
.iter()
|
||||
.any(|markup| matches!(*markup, Markup::Let { .. }))
|
||||
{
|
||||
self.block(
|
||||
Block {
|
||||
markups,
|
||||
outer_span,
|
||||
},
|
||||
build,
|
||||
);
|
||||
} else {
|
||||
self.markups(block.markups, build);
|
||||
self.markups(markups, build);
|
||||
}
|
||||
}
|
||||
Markup::Lit(lit) => build.push_escaped(&lit.to_string()),
|
||||
Markup::Literal { content, .. } => build.push_escaped(&content),
|
||||
Markup::Symbol { symbol } => self.name(symbol, build),
|
||||
Markup::Splice { expr, .. } => self.splice(expr, build),
|
||||
Markup::Element(element) => self.element(element.into(), build),
|
||||
Markup::ControlFlow(control_flow) => self.control_flow(control_flow, build),
|
||||
Markup::Semi(_) => {}
|
||||
Markup::Element { name, attrs, body } => self.element(name, attrs, body, build),
|
||||
Markup::Let { tokens, .. } => build.push_tokens(tokens),
|
||||
Markup::Special { segments } => {
|
||||
for Special { head, body, .. } in segments {
|
||||
build.push_tokens(head);
|
||||
self.block(body, build);
|
||||
}
|
||||
}
|
||||
|
||||
fn block<E: Into<Element>>(&self, block: Block<E>, build: &mut Builder) {
|
||||
let markups = {
|
||||
Markup::Match {
|
||||
head,
|
||||
arms,
|
||||
arms_span,
|
||||
..
|
||||
} => {
|
||||
let body = {
|
||||
let mut build = self.builder();
|
||||
self.markups(block.markups, &mut build);
|
||||
for MatchArm { head, body } in arms {
|
||||
build.push_tokens(head);
|
||||
self.block(body, &mut build);
|
||||
}
|
||||
build.finish()
|
||||
};
|
||||
|
||||
build.push_tokens(quote!({ #markups }));
|
||||
let mut body = TokenTree::Group(Group::new(Delimiter::Brace, body));
|
||||
body.set_span(arms_span.collapse());
|
||||
build.push_tokens(quote!(#head #body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn splice(&self, expr: Expr, build: &mut Builder) {
|
||||
let output_ident = &self.output_ident;
|
||||
build.push_tokens(
|
||||
quote!(pagetop::html::html_private::render_to!(&(#expr), &mut #output_ident);),
|
||||
);
|
||||
fn block(
|
||||
&self,
|
||||
Block {
|
||||
markups,
|
||||
outer_span,
|
||||
}: Block,
|
||||
build: &mut Builder,
|
||||
) {
|
||||
let block = {
|
||||
let mut build = self.builder();
|
||||
self.markups(markups, &mut build);
|
||||
build.finish()
|
||||
};
|
||||
let mut block = TokenTree::Group(Group::new(Delimiter::Brace, block));
|
||||
block.set_span(outer_span.collapse());
|
||||
build.push_tokens(TokenStream::from(block));
|
||||
}
|
||||
|
||||
fn element(&self, element: Element, build: &mut Builder) {
|
||||
let element_name = element.name.clone().unwrap_or_else(|| parse_quote!(div));
|
||||
fn splice(&self, expr: TokenStream, build: &mut Builder) {
|
||||
let output_ident = self.output_ident.clone();
|
||||
|
||||
let found_crate = crate_name("pagetop").expect("pagetop is present in `Cargo.toml`");
|
||||
build.push_tokens(match found_crate {
|
||||
FoundCrate::Itself => quote!(
|
||||
crate::html::html_private::render_to!(&#expr, &mut #output_ident);
|
||||
),
|
||||
_ => quote!(
|
||||
pagetop::html::html_private::render_to!(&#expr, &mut #output_ident);
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
fn element(&self, name: TokenStream, attrs: Vec<Attr>, body: ElementBody, build: &mut Builder) {
|
||||
build.push_str("<");
|
||||
self.name(element_name.clone(), build);
|
||||
self.attrs(element.attrs, build);
|
||||
self.name(name.clone(), build);
|
||||
self.attrs(attrs, build);
|
||||
build.push_str(">");
|
||||
if let ElementBody::Block(block) = element.body {
|
||||
if let ElementBody::Block { block } = body {
|
||||
self.markups(block.markups, build);
|
||||
build.push_str("</");
|
||||
self.name(element_name, build);
|
||||
self.name(name, build);
|
||||
build.push_str(">");
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self, name: HtmlName, build: &mut Builder) {
|
||||
build.push_escaped(&name.to_string());
|
||||
fn name(&self, name: TokenStream, build: &mut Builder) {
|
||||
build.push_escaped(&name_to_string(name));
|
||||
}
|
||||
|
||||
fn name_or_markup(&self, name: HtmlNameOrMarkup, build: &mut Builder) {
|
||||
match name {
|
||||
HtmlNameOrMarkup::HtmlName(name) => self.name(name, build),
|
||||
HtmlNameOrMarkup::Markup(markup) => self.markup(markup, build),
|
||||
}
|
||||
}
|
||||
|
||||
fn attr(&self, name: HtmlName, value: AttributeType, build: &mut Builder) {
|
||||
match value {
|
||||
AttributeType::Normal { value, .. } => {
|
||||
fn attrs(&self, attrs: Vec<Attr>, build: &mut Builder) {
|
||||
for NamedAttr { name, attr_type } in desugar_attrs(attrs) {
|
||||
match attr_type {
|
||||
AttrType::Normal { value } => {
|
||||
build.push_str(" ");
|
||||
self.name(name, build);
|
||||
build.push_str("=\"");
|
||||
self.markup(value, build);
|
||||
build.push_str("\"");
|
||||
}
|
||||
AttributeType::Optional {
|
||||
AttrType::Optional {
|
||||
toggler: Toggler { cond, .. },
|
||||
..
|
||||
} => {
|
||||
let inner_value: Expr = parse_quote!(inner_value);
|
||||
|
||||
let inner_value = quote!(inner_value);
|
||||
let body = {
|
||||
let mut build = self.builder();
|
||||
build.push_str(" ");
|
||||
|
|
@ -122,11 +159,13 @@ impl Generator {
|
|||
};
|
||||
build.push_tokens(quote!(if let Some(#inner_value) = (#cond) { #body }));
|
||||
}
|
||||
AttributeType::Empty(None) => {
|
||||
AttrType::Empty { toggler: None } => {
|
||||
build.push_str(" ");
|
||||
self.name(name, build);
|
||||
}
|
||||
AttributeType::Empty(Some(Toggler { cond, .. })) => {
|
||||
AttrType::Empty {
|
||||
toggler: Some(Toggler { cond, .. }),
|
||||
} => {
|
||||
let body = {
|
||||
let mut build = self.builder();
|
||||
build.push_str(" ");
|
||||
|
|
@ -137,221 +176,106 @@ impl Generator {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn attrs(&self, attrs: Vec<Attribute>, build: &mut Builder) {
|
||||
let (classes, id, named_attrs) = split_attrs(attrs);
|
||||
|
||||
if !classes.is_empty() {
|
||||
let mut toggle_class_exprs = vec![];
|
||||
|
||||
build.push_str(" ");
|
||||
self.name(parse_quote!(class), build);
|
||||
build.push_str("=\"");
|
||||
for (i, (name, toggler)) in classes.into_iter().enumerate() {
|
||||
if let Some(toggler) = toggler {
|
||||
toggle_class_exprs.push((i > 0, name, toggler));
|
||||
} else {
|
||||
if i > 0 {
|
||||
build.push_str(" ");
|
||||
}
|
||||
self.name_or_markup(name, build);
|
||||
}
|
||||
}
|
||||
|
||||
for (not_first, name, toggler) in toggle_class_exprs {
|
||||
let body = {
|
||||
let mut build = self.builder();
|
||||
if not_first {
|
||||
build.push_str(" ");
|
||||
}
|
||||
self.name_or_markup(name, &mut build);
|
||||
build.finish()
|
||||
};
|
||||
build.push_tokens(quote!(if (#toggler) { #body }));
|
||||
}
|
||||
|
||||
build.push_str("\"");
|
||||
}
|
||||
|
||||
if let Some(id) = id {
|
||||
build.push_str(" ");
|
||||
self.name(parse_quote!(id), build);
|
||||
build.push_str("=\"");
|
||||
self.name_or_markup(id, build);
|
||||
build.push_str("\"");
|
||||
}
|
||||
|
||||
for (name, attr_type) in named_attrs {
|
||||
self.attr(name, attr_type, build);
|
||||
}
|
||||
}
|
||||
|
||||
fn control_flow<E: Into<Element>>(&self, control_flow: ControlFlow<E>, build: &mut Builder) {
|
||||
match control_flow.kind {
|
||||
ControlFlowKind::If(if_) => self.control_flow_if(if_, build),
|
||||
ControlFlowKind::Let(let_) => self.control_flow_let(let_, build),
|
||||
ControlFlowKind::For(for_) => self.control_flow_for(for_, build),
|
||||
ControlFlowKind::While(while_) => self.control_flow_while(while_, build),
|
||||
ControlFlowKind::Match(match_) => self.control_flow_match(match_, build),
|
||||
}
|
||||
}
|
||||
|
||||
fn control_flow_if<E: Into<Element>>(
|
||||
&self,
|
||||
IfExpr {
|
||||
if_token,
|
||||
cond,
|
||||
then_branch,
|
||||
else_branch,
|
||||
}: IfExpr<E>,
|
||||
build: &mut Builder,
|
||||
) {
|
||||
build.push_tokens(quote!(#if_token #cond));
|
||||
self.block(then_branch, build);
|
||||
|
||||
if let Some((_, else_token, if_or_block)) = else_branch {
|
||||
build.push_tokens(quote!(#else_token));
|
||||
self.control_flow_if_or_block(*if_or_block, build);
|
||||
}
|
||||
}
|
||||
|
||||
fn control_flow_if_or_block<E: Into<Element>>(
|
||||
&self,
|
||||
if_or_block: IfOrBlock<E>,
|
||||
build: &mut Builder,
|
||||
) {
|
||||
match if_or_block {
|
||||
IfOrBlock::If(if_) => self.control_flow_if(if_, build),
|
||||
IfOrBlock::Block(block) => self.block(block, build),
|
||||
}
|
||||
}
|
||||
|
||||
fn control_flow_let(&self, let_: Local, build: &mut Builder) {
|
||||
build.push_tokens(let_.to_token_stream());
|
||||
}
|
||||
|
||||
fn control_flow_for<E: Into<Element>>(
|
||||
&self,
|
||||
ForExpr {
|
||||
for_token,
|
||||
pat,
|
||||
in_token,
|
||||
expr,
|
||||
body,
|
||||
}: ForExpr<E>,
|
||||
build: &mut Builder,
|
||||
) {
|
||||
build.push_tokens(quote!(#for_token #pat #in_token (#expr)));
|
||||
self.block(body, build);
|
||||
}
|
||||
|
||||
fn control_flow_while<E: Into<Element>>(
|
||||
&self,
|
||||
WhileExpr {
|
||||
while_token,
|
||||
cond,
|
||||
body,
|
||||
}: WhileExpr<E>,
|
||||
build: &mut Builder,
|
||||
) {
|
||||
build.push_tokens(quote!(#while_token #cond));
|
||||
self.block(body, build);
|
||||
}
|
||||
|
||||
fn control_flow_match<E: Into<Element>>(
|
||||
&self,
|
||||
MatchExpr {
|
||||
match_token,
|
||||
expr,
|
||||
brace_token,
|
||||
arms,
|
||||
}: MatchExpr<E>,
|
||||
build: &mut Builder,
|
||||
) {
|
||||
let arms = {
|
||||
let mut build = self.builder();
|
||||
for MatchArm {
|
||||
pat,
|
||||
guard,
|
||||
fat_arrow_token,
|
||||
body,
|
||||
comma_token,
|
||||
} in arms
|
||||
{
|
||||
build.push_tokens(quote!(#pat));
|
||||
if let Some((if_token, cond)) = guard {
|
||||
build.push_tokens(quote!(#if_token #cond));
|
||||
}
|
||||
build.push_tokens(quote!(#fat_arrow_token));
|
||||
self.block(
|
||||
Block {
|
||||
brace_token: Brace(Span::call_site()),
|
||||
markups: Markups {
|
||||
markups: vec![body],
|
||||
},
|
||||
},
|
||||
&mut build,
|
||||
);
|
||||
build.push_tokens(quote!(#comma_token));
|
||||
}
|
||||
build.finish()
|
||||
};
|
||||
|
||||
let mut arm_block = TokenStream::new();
|
||||
|
||||
brace_token.surround(&mut arm_block, |tokens| {
|
||||
arms.to_tokens(tokens);
|
||||
});
|
||||
|
||||
build.push_tokens(quote!(#match_token #expr #arm_block));
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn split_attrs(
|
||||
attrs: Vec<Attribute>,
|
||||
) -> (
|
||||
Vec<(HtmlNameOrMarkup, Option<Expr>)>,
|
||||
Option<HtmlNameOrMarkup>,
|
||||
Vec<(HtmlName, AttributeType)>,
|
||||
) {
|
||||
let mut classes = vec![];
|
||||
let mut id = None;
|
||||
fn desugar_attrs(attrs: Vec<Attr>) -> Vec<NamedAttr> {
|
||||
let mut classes_static = vec![];
|
||||
let mut classes_toggled = vec![];
|
||||
let mut ids = vec![];
|
||||
let mut named_attrs = vec![];
|
||||
|
||||
for attr in attrs {
|
||||
match attr {
|
||||
Attribute::Class { name, toggler, .. } => {
|
||||
classes.push((name, toggler.map(|toggler| toggler.cond)))
|
||||
Attr::Class {
|
||||
name,
|
||||
toggler: Some(toggler),
|
||||
..
|
||||
} => classes_toggled.push((name, toggler)),
|
||||
Attr::Class {
|
||||
name,
|
||||
toggler: None,
|
||||
..
|
||||
} => classes_static.push(name),
|
||||
Attr::Id { name, .. } => ids.push(name),
|
||||
Attr::Named { named_attr } => named_attrs.push(named_attr),
|
||||
}
|
||||
Attribute::Id { name, .. } => id = Some(name),
|
||||
Attribute::Named { name, attr_type } => named_attrs.push((name, attr_type)),
|
||||
}
|
||||
let classes = desugar_classes_or_ids("class", classes_static, classes_toggled);
|
||||
let ids = desugar_classes_or_ids("id", ids, vec![]);
|
||||
classes.into_iter().chain(ids).chain(named_attrs).collect()
|
||||
}
|
||||
|
||||
(classes, id, named_attrs)
|
||||
fn desugar_classes_or_ids(
|
||||
attr_name: &'static str,
|
||||
values_static: Vec<Markup>,
|
||||
values_toggled: Vec<(Markup, Toggler)>,
|
||||
) -> Option<NamedAttr> {
|
||||
if values_static.is_empty() && values_toggled.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut markups = Vec::new();
|
||||
let mut leading_space = false;
|
||||
for name in values_static {
|
||||
markups.extend(prepend_leading_space(name, &mut leading_space));
|
||||
}
|
||||
for (name, Toggler { cond, cond_span }) in values_toggled {
|
||||
let body = Block {
|
||||
markups: prepend_leading_space(name, &mut leading_space),
|
||||
// TODO: is this correct?
|
||||
outer_span: cond_span,
|
||||
};
|
||||
markups.push(Markup::Special {
|
||||
segments: vec![Special {
|
||||
at_span: SpanRange::call_site(),
|
||||
head: quote!(if (#cond)),
|
||||
body,
|
||||
}],
|
||||
});
|
||||
}
|
||||
Some(NamedAttr {
|
||||
name: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))),
|
||||
attr_type: AttrType::Normal {
|
||||
value: Markup::Block(Block {
|
||||
markups,
|
||||
outer_span: SpanRange::call_site(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn prepend_leading_space(name: Markup, leading_space: &mut bool) -> Vec<Markup> {
|
||||
let mut markups = Vec::new();
|
||||
if *leading_space {
|
||||
markups.push(Markup::Literal {
|
||||
content: " ".to_owned(),
|
||||
span: name.span(),
|
||||
});
|
||||
}
|
||||
*leading_space = true;
|
||||
markups.push(name);
|
||||
markups
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
struct Builder {
|
||||
output_ident: Ident,
|
||||
tokens: TokenStream,
|
||||
output_ident: TokenTree,
|
||||
tokens: Vec<TokenTree>,
|
||||
tail: String,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
fn new(output_ident: Ident) -> Builder {
|
||||
fn new(output_ident: TokenTree) -> Builder {
|
||||
Builder {
|
||||
output_ident,
|
||||
tokens: TokenStream::new(),
|
||||
tokens: Vec::new(),
|
||||
tail: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_str(&mut self, string: &'static str) {
|
||||
fn push_str(&mut self, string: &str) {
|
||||
self.tail.push_str(string);
|
||||
}
|
||||
|
||||
|
|
@ -370,8 +294,8 @@ impl Builder {
|
|||
}
|
||||
let push_str_expr = {
|
||||
let output_ident = self.output_ident.clone();
|
||||
let tail = &self.tail;
|
||||
quote!(#output_ident.push_str(#tail);)
|
||||
let string = TokenTree::Literal(Literal::string(&self.tail));
|
||||
quote!(#output_ident.push_str(#string);)
|
||||
};
|
||||
self.tail.clear();
|
||||
self.tokens.extend(push_str_expr);
|
||||
|
|
@ -379,6 +303,6 @@ impl Builder {
|
|||
|
||||
fn finish(mut self) -> TokenStream {
|
||||
self.cut();
|
||||
self.tokens
|
||||
self.tokens.into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
752
helpers/pagetop-macros/src/maud/parse.rs
Normal file
752
helpers/pagetop-macros/src/maud/parse.rs
Normal file
|
|
@ -0,0 +1,752 @@
|
|||
use proc_macro2::{Delimiter, Ident, Literal, Spacing, Span, TokenStream, TokenTree};
|
||||
use proc_macro_error::{abort, abort_call_site, emit_error, SpanRange};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use syn::Lit;
|
||||
|
||||
use crate::maud::ast;
|
||||
|
||||
pub fn parse(input: TokenStream) -> Vec<ast::Markup> {
|
||||
Parser::new(input).markups()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Parser {
|
||||
/// If we're inside an attribute, then this contains the attribute name.
|
||||
current_attr: Option<String>,
|
||||
input: <TokenStream as IntoIterator>::IntoIter,
|
||||
}
|
||||
|
||||
impl Iterator for Parser {
|
||||
type Item = TokenTree;
|
||||
|
||||
fn next(&mut self) -> Option<TokenTree> {
|
||||
self.input.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn new(input: TokenStream) -> Parser {
|
||||
Parser {
|
||||
current_attr: None,
|
||||
input: input.into_iter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_input(&self, input: TokenStream) -> Parser {
|
||||
Parser {
|
||||
current_attr: self.current_attr.clone(),
|
||||
input: input.into_iter(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the next token in the stream without consuming it.
|
||||
fn peek(&mut self) -> Option<TokenTree> {
|
||||
self.clone().next()
|
||||
}
|
||||
|
||||
/// Returns the next two tokens in the stream without consuming them.
|
||||
fn peek2(&mut self) -> Option<(TokenTree, Option<TokenTree>)> {
|
||||
let mut clone = self.clone();
|
||||
clone.next().map(|first| (first, clone.next()))
|
||||
}
|
||||
|
||||
/// Advances the cursor by one step.
|
||||
fn advance(&mut self) {
|
||||
self.next();
|
||||
}
|
||||
|
||||
/// Advances the cursor by two steps.
|
||||
fn advance2(&mut self) {
|
||||
self.next();
|
||||
self.next();
|
||||
}
|
||||
|
||||
/// Parses multiple blocks of markup.
|
||||
fn markups(&mut self) -> Vec<ast::Markup> {
|
||||
let mut result = Vec::new();
|
||||
loop {
|
||||
match self.peek2() {
|
||||
None => break,
|
||||
Some((TokenTree::Punct(ref punct), _)) if punct.as_char() == ';' => self.advance(),
|
||||
Some((TokenTree::Punct(ref punct), Some(TokenTree::Ident(ref ident))))
|
||||
if punct.as_char() == '@' && *ident == "let" =>
|
||||
{
|
||||
self.advance2();
|
||||
let keyword = TokenTree::Ident(ident.clone());
|
||||
result.push(self.let_expr(punct.span(), keyword));
|
||||
}
|
||||
_ => result.push(self.markup()),
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Parses a single block of markup.
|
||||
fn markup(&mut self) -> ast::Markup {
|
||||
let token = match self.peek() {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
abort_call_site!("unexpected end of input");
|
||||
}
|
||||
};
|
||||
let markup = match token {
|
||||
// Literal
|
||||
TokenTree::Literal(literal) => {
|
||||
self.advance();
|
||||
self.literal(literal)
|
||||
}
|
||||
// Special form
|
||||
TokenTree::Punct(ref punct) if punct.as_char() == '@' => {
|
||||
self.advance();
|
||||
let at_span = punct.span();
|
||||
match self.next() {
|
||||
Some(TokenTree::Ident(ident)) => {
|
||||
let keyword = TokenTree::Ident(ident.clone());
|
||||
match ident.to_string().as_str() {
|
||||
"if" => {
|
||||
let mut segments = Vec::new();
|
||||
self.if_expr(at_span, vec![keyword], &mut segments);
|
||||
ast::Markup::Special { segments }
|
||||
}
|
||||
"while" => self.while_expr(at_span, keyword),
|
||||
"for" => self.for_expr(at_span, keyword),
|
||||
"match" => self.match_expr(at_span, keyword),
|
||||
"let" => {
|
||||
let span = SpanRange {
|
||||
first: at_span,
|
||||
last: ident.span(),
|
||||
};
|
||||
abort!(span, "`@let` only works inside a block");
|
||||
}
|
||||
other => {
|
||||
let span = SpanRange {
|
||||
first: at_span,
|
||||
last: ident.span(),
|
||||
};
|
||||
abort!(span, "unknown keyword `@{}`", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
abort!(at_span, "expected keyword after `@`");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Element
|
||||
TokenTree::Ident(ident) => {
|
||||
let ident_string = ident.to_string();
|
||||
match ident_string.as_str() {
|
||||
"if" | "while" | "for" | "match" | "let" => {
|
||||
abort!(
|
||||
ident,
|
||||
"found keyword `{}`", ident_string;
|
||||
help = "should this be a `@{}`?", ident_string
|
||||
);
|
||||
}
|
||||
"true" | "false" => {
|
||||
if let Some(attr_name) = &self.current_attr {
|
||||
emit_error!(
|
||||
ident,
|
||||
r#"attribute value must be a string"#;
|
||||
help = "to declare an empty attribute, omit the equals sign: `{}`",
|
||||
attr_name;
|
||||
help = "to toggle the attribute, use square brackets: `{}[some_boolean_flag]`",
|
||||
attr_name;
|
||||
);
|
||||
return ast::Markup::ParseError {
|
||||
span: SpanRange::single_span(ident.span()),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// `.try_namespaced_name()` should never fail as we've
|
||||
// already seen an `Ident`
|
||||
let name = self.try_namespaced_name().expect("identifier");
|
||||
self.element(name)
|
||||
}
|
||||
// Div element shorthand
|
||||
TokenTree::Punct(ref punct) if punct.as_char() == '.' || punct.as_char() == '#' => {
|
||||
let name = TokenTree::Ident(Ident::new("div", punct.span()));
|
||||
self.element(name.into())
|
||||
}
|
||||
// Splice
|
||||
TokenTree::Group(ref group) if group.delimiter() == Delimiter::Parenthesis => {
|
||||
self.advance();
|
||||
ast::Markup::Splice {
|
||||
expr: group.stream(),
|
||||
outer_span: SpanRange::single_span(group.span()),
|
||||
}
|
||||
}
|
||||
// Block
|
||||
TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => {
|
||||
self.advance();
|
||||
ast::Markup::Block(self.block(group.stream(), SpanRange::single_span(group.span())))
|
||||
}
|
||||
// ???
|
||||
token => {
|
||||
abort!(token, "invalid syntax");
|
||||
}
|
||||
};
|
||||
markup
|
||||
}
|
||||
|
||||
/// Parses a literal string.
|
||||
fn literal(&mut self, literal: Literal) -> ast::Markup {
|
||||
match Lit::new(literal.clone()) {
|
||||
Lit::Str(lit_str) => {
|
||||
return ast::Markup::Literal {
|
||||
content: lit_str.value(),
|
||||
span: SpanRange::single_span(literal.span()),
|
||||
}
|
||||
}
|
||||
// Boolean literals are idents, so `Lit::Bool` is handled in
|
||||
// `markup`, not here.
|
||||
Lit::Int(..) | Lit::Float(..) => {
|
||||
emit_error!(literal, r#"literal must be double-quoted: `"{}"`"#, literal);
|
||||
}
|
||||
Lit::Char(lit_char) => {
|
||||
emit_error!(
|
||||
literal,
|
||||
r#"literal must be double-quoted: `"{}"`"#,
|
||||
lit_char.value(),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
emit_error!(literal, "expected string");
|
||||
}
|
||||
}
|
||||
ast::Markup::ParseError {
|
||||
span: SpanRange::single_span(literal.span()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an `@if` expression.
|
||||
///
|
||||
/// The leading `@if` should already be consumed.
|
||||
fn if_expr(&mut self, at_span: Span, prefix: Vec<TokenTree>, segments: &mut Vec<ast::Special>) {
|
||||
let mut head = prefix;
|
||||
let body = loop {
|
||||
match self.next() {
|
||||
Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => {
|
||||
break self.block(block.stream(), SpanRange::single_span(block.span()));
|
||||
}
|
||||
Some(token) => head.push(token),
|
||||
None => {
|
||||
let mut span = ast::span_tokens(head);
|
||||
span.first = at_span;
|
||||
abort!(span, "expected body for this `@if`");
|
||||
}
|
||||
}
|
||||
};
|
||||
segments.push(ast::Special {
|
||||
at_span: SpanRange::single_span(at_span),
|
||||
head: head.into_iter().collect(),
|
||||
body,
|
||||
});
|
||||
self.else_if_expr(segments)
|
||||
}
|
||||
|
||||
/// Parses an optional `@else if` or `@else`.
|
||||
///
|
||||
/// The leading `@else if` or `@else` should *not* already be consumed.
|
||||
fn else_if_expr(&mut self, segments: &mut Vec<ast::Special>) {
|
||||
match self.peek2() {
|
||||
Some((TokenTree::Punct(ref punct), Some(TokenTree::Ident(ref else_keyword))))
|
||||
if punct.as_char() == '@' && *else_keyword == "else" =>
|
||||
{
|
||||
self.advance2();
|
||||
let at_span = punct.span();
|
||||
let else_keyword = TokenTree::Ident(else_keyword.clone());
|
||||
match self.peek() {
|
||||
// `@else if`
|
||||
Some(TokenTree::Ident(ref if_keyword)) if *if_keyword == "if" => {
|
||||
self.advance();
|
||||
let if_keyword = TokenTree::Ident(if_keyword.clone());
|
||||
self.if_expr(at_span, vec![else_keyword, if_keyword], segments)
|
||||
}
|
||||
// Just an `@else`
|
||||
_ => match self.next() {
|
||||
Some(TokenTree::Group(ref group))
|
||||
if group.delimiter() == Delimiter::Brace =>
|
||||
{
|
||||
let body =
|
||||
self.block(group.stream(), SpanRange::single_span(group.span()));
|
||||
segments.push(ast::Special {
|
||||
at_span: SpanRange::single_span(at_span),
|
||||
head: vec![else_keyword].into_iter().collect(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
let span = SpanRange {
|
||||
first: at_span,
|
||||
last: else_keyword.span(),
|
||||
};
|
||||
abort!(span, "expected body for this `@else`");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// We didn't find an `@else`; stop
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an `@while` expression.
|
||||
///
|
||||
/// The leading `@while` should already be consumed.
|
||||
fn while_expr(&mut self, at_span: Span, keyword: TokenTree) -> ast::Markup {
|
||||
let keyword_span = keyword.span();
|
||||
let mut head = vec![keyword];
|
||||
let body = loop {
|
||||
match self.next() {
|
||||
Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => {
|
||||
break self.block(block.stream(), SpanRange::single_span(block.span()));
|
||||
}
|
||||
Some(token) => head.push(token),
|
||||
None => {
|
||||
let span = SpanRange {
|
||||
first: at_span,
|
||||
last: keyword_span,
|
||||
};
|
||||
abort!(span, "expected body for this `@while`");
|
||||
}
|
||||
}
|
||||
};
|
||||
ast::Markup::Special {
|
||||
segments: vec![ast::Special {
|
||||
at_span: SpanRange::single_span(at_span),
|
||||
head: head.into_iter().collect(),
|
||||
body,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a `@for` expression.
|
||||
///
|
||||
/// The leading `@for` should already be consumed.
|
||||
fn for_expr(&mut self, at_span: Span, keyword: TokenTree) -> ast::Markup {
|
||||
let keyword_span = keyword.span();
|
||||
let mut head = vec![keyword];
|
||||
loop {
|
||||
match self.next() {
|
||||
Some(TokenTree::Ident(ref in_keyword)) if *in_keyword == "in" => {
|
||||
head.push(TokenTree::Ident(in_keyword.clone()));
|
||||
break;
|
||||
}
|
||||
Some(token) => head.push(token),
|
||||
None => {
|
||||
let span = SpanRange {
|
||||
first: at_span,
|
||||
last: keyword_span,
|
||||
};
|
||||
abort!(span, "missing `in` in `@for` loop");
|
||||
}
|
||||
}
|
||||
}
|
||||
let body = loop {
|
||||
match self.next() {
|
||||
Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => {
|
||||
break self.block(block.stream(), SpanRange::single_span(block.span()));
|
||||
}
|
||||
Some(token) => head.push(token),
|
||||
None => {
|
||||
let span = SpanRange {
|
||||
first: at_span,
|
||||
last: keyword_span,
|
||||
};
|
||||
abort!(span, "expected body for this `@for`");
|
||||
}
|
||||
}
|
||||
};
|
||||
ast::Markup::Special {
|
||||
segments: vec![ast::Special {
|
||||
at_span: SpanRange::single_span(at_span),
|
||||
head: head.into_iter().collect(),
|
||||
body,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a `@match` expression.
|
||||
///
|
||||
/// The leading `@match` should already be consumed.
|
||||
fn match_expr(&mut self, at_span: Span, keyword: TokenTree) -> ast::Markup {
|
||||
let keyword_span = keyword.span();
|
||||
let mut head = vec![keyword];
|
||||
let (arms, arms_span) = loop {
|
||||
match self.next() {
|
||||
Some(TokenTree::Group(ref body)) if body.delimiter() == Delimiter::Brace => {
|
||||
let span = SpanRange::single_span(body.span());
|
||||
break (self.with_input(body.stream()).match_arms(), span);
|
||||
}
|
||||
Some(token) => head.push(token),
|
||||
None => {
|
||||
let span = SpanRange {
|
||||
first: at_span,
|
||||
last: keyword_span,
|
||||
};
|
||||
abort!(span, "expected body for this `@match`");
|
||||
}
|
||||
}
|
||||
};
|
||||
ast::Markup::Match {
|
||||
at_span: SpanRange::single_span(at_span),
|
||||
head: head.into_iter().collect(),
|
||||
arms,
|
||||
arms_span,
|
||||
}
|
||||
}
|
||||
|
||||
fn match_arms(&mut self) -> Vec<ast::MatchArm> {
|
||||
let mut arms = Vec::new();
|
||||
while let Some(arm) = self.match_arm() {
|
||||
arms.push(arm);
|
||||
}
|
||||
arms
|
||||
}
|
||||
|
||||
fn match_arm(&mut self) -> Option<ast::MatchArm> {
|
||||
let mut head = Vec::new();
|
||||
loop {
|
||||
match self.peek2() {
|
||||
Some((TokenTree::Punct(ref eq), Some(TokenTree::Punct(ref gt))))
|
||||
if eq.as_char() == '='
|
||||
&& gt.as_char() == '>'
|
||||
&& eq.spacing() == Spacing::Joint =>
|
||||
{
|
||||
self.advance2();
|
||||
head.push(TokenTree::Punct(eq.clone()));
|
||||
head.push(TokenTree::Punct(gt.clone()));
|
||||
break;
|
||||
}
|
||||
Some((token, _)) => {
|
||||
self.advance();
|
||||
head.push(token);
|
||||
}
|
||||
None => {
|
||||
if head.is_empty() {
|
||||
return None;
|
||||
} else {
|
||||
let head_span = ast::span_tokens(head);
|
||||
abort!(head_span, "unexpected end of @match pattern");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let body = match self.next() {
|
||||
// $pat => { $stmts }
|
||||
Some(TokenTree::Group(ref body)) if body.delimiter() == Delimiter::Brace => {
|
||||
let body = self.block(body.stream(), SpanRange::single_span(body.span()));
|
||||
// Trailing commas are optional if the match arm is a braced block
|
||||
if let Some(TokenTree::Punct(ref punct)) = self.peek() {
|
||||
if punct.as_char() == ',' {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
body
|
||||
}
|
||||
// $pat => $expr
|
||||
Some(first_token) => {
|
||||
let mut span = SpanRange::single_span(first_token.span());
|
||||
let mut body = vec![first_token];
|
||||
loop {
|
||||
match self.next() {
|
||||
Some(TokenTree::Punct(ref punct)) if punct.as_char() == ',' => break,
|
||||
Some(token) => {
|
||||
span.last = token.span();
|
||||
body.push(token);
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
self.block(body.into_iter().collect(), span)
|
||||
}
|
||||
None => {
|
||||
let span = ast::span_tokens(head);
|
||||
abort!(span, "unexpected end of @match arm");
|
||||
}
|
||||
};
|
||||
Some(ast::MatchArm {
|
||||
head: head.into_iter().collect(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a `@let` expression.
|
||||
///
|
||||
/// The leading `@let` should already be consumed.
|
||||
fn let_expr(&mut self, at_span: Span, keyword: TokenTree) -> ast::Markup {
|
||||
let mut tokens = vec![keyword];
|
||||
loop {
|
||||
match self.next() {
|
||||
Some(token) => match token {
|
||||
TokenTree::Punct(ref punct) if punct.as_char() == '=' => {
|
||||
tokens.push(token.clone());
|
||||
break;
|
||||
}
|
||||
_ => tokens.push(token),
|
||||
},
|
||||
None => {
|
||||
let mut span = ast::span_tokens(tokens);
|
||||
span.first = at_span;
|
||||
abort!(span, "unexpected end of `@let` expression");
|
||||
}
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match self.next() {
|
||||
Some(token) => match token {
|
||||
TokenTree::Punct(ref punct) if punct.as_char() == ';' => {
|
||||
tokens.push(token.clone());
|
||||
break;
|
||||
}
|
||||
_ => tokens.push(token),
|
||||
},
|
||||
None => {
|
||||
let mut span = ast::span_tokens(tokens);
|
||||
span.first = at_span;
|
||||
abort!(
|
||||
span,
|
||||
"unexpected end of `@let` expression";
|
||||
help = "are you missing a semicolon?"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::Markup::Let {
|
||||
at_span: SpanRange::single_span(at_span),
|
||||
tokens: tokens.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an element node.
|
||||
///
|
||||
/// The element name should already be consumed.
|
||||
fn element(&mut self, name: TokenStream) -> ast::Markup {
|
||||
if self.current_attr.is_some() {
|
||||
let span = ast::span_tokens(name);
|
||||
abort!(span, "unexpected element");
|
||||
}
|
||||
let attrs = self.attrs();
|
||||
let body = match self.peek() {
|
||||
Some(TokenTree::Punct(ref punct))
|
||||
if punct.as_char() == ';' || punct.as_char() == '/' =>
|
||||
{
|
||||
// Void element
|
||||
self.advance();
|
||||
if punct.as_char() == '/' {
|
||||
emit_error!(
|
||||
punct,
|
||||
"void elements must use `;`, not `/`";
|
||||
help = "change this to `;`";
|
||||
help = "see https://github.com/lambda-fairy/maud/pull/315 for details";
|
||||
);
|
||||
}
|
||||
ast::ElementBody::Void {
|
||||
semi_span: SpanRange::single_span(punct.span()),
|
||||
}
|
||||
}
|
||||
Some(_) => match self.markup() {
|
||||
ast::Markup::Block(block) => ast::ElementBody::Block { block },
|
||||
markup => {
|
||||
let markup_span = markup.span();
|
||||
abort!(
|
||||
markup_span,
|
||||
"element body must be wrapped in braces";
|
||||
help = "see https://github.com/lambda-fairy/maud/pull/137 for details"
|
||||
);
|
||||
}
|
||||
},
|
||||
None => abort_call_site!("expected `;`, found end of macro"),
|
||||
};
|
||||
ast::Markup::Element { name, attrs, body }
|
||||
}
|
||||
|
||||
/// Parses the attributes of an element.
|
||||
fn attrs(&mut self) -> Vec<ast::Attr> {
|
||||
let mut attrs = Vec::new();
|
||||
loop {
|
||||
if let Some(name) = self.try_namespaced_name() {
|
||||
// Attribute
|
||||
match self.peek() {
|
||||
// Non-empty attribute
|
||||
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => {
|
||||
self.advance();
|
||||
// Parse a value under an attribute context
|
||||
assert!(self.current_attr.is_none());
|
||||
self.current_attr = Some(ast::name_to_string(name.clone()));
|
||||
let attr_type = match self.attr_toggler() {
|
||||
Some(toggler) => ast::AttrType::Optional { toggler },
|
||||
None => {
|
||||
let value = self.markup();
|
||||
ast::AttrType::Normal { value }
|
||||
}
|
||||
};
|
||||
self.current_attr = None;
|
||||
attrs.push(ast::Attr::Named {
|
||||
named_attr: ast::NamedAttr { name, attr_type },
|
||||
});
|
||||
}
|
||||
// Empty attribute (legacy syntax)
|
||||
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '?' => {
|
||||
self.advance();
|
||||
let toggler = self.attr_toggler();
|
||||
attrs.push(ast::Attr::Named {
|
||||
named_attr: ast::NamedAttr {
|
||||
name: name.clone(),
|
||||
attr_type: ast::AttrType::Empty { toggler },
|
||||
},
|
||||
});
|
||||
}
|
||||
// Empty attribute (new syntax)
|
||||
_ => {
|
||||
let toggler = self.attr_toggler();
|
||||
attrs.push(ast::Attr::Named {
|
||||
named_attr: ast::NamedAttr {
|
||||
name: name.clone(),
|
||||
attr_type: ast::AttrType::Empty { toggler },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match self.peek() {
|
||||
// Class shorthand
|
||||
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '.' => {
|
||||
self.advance();
|
||||
let name = self.class_or_id_name();
|
||||
let toggler = self.attr_toggler();
|
||||
attrs.push(ast::Attr::Class {
|
||||
dot_span: SpanRange::single_span(punct.span()),
|
||||
name,
|
||||
toggler,
|
||||
});
|
||||
}
|
||||
// ID shorthand
|
||||
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '#' => {
|
||||
self.advance();
|
||||
let name = self.class_or_id_name();
|
||||
attrs.push(ast::Attr::Id {
|
||||
hash_span: SpanRange::single_span(punct.span()),
|
||||
name,
|
||||
});
|
||||
}
|
||||
// If it's not a valid attribute, backtrack and bail out
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut attr_map: HashMap<String, Vec<SpanRange>> = HashMap::new();
|
||||
let mut has_class = false;
|
||||
for attr in &attrs {
|
||||
let name = match attr {
|
||||
ast::Attr::Class { .. } => {
|
||||
if has_class {
|
||||
// Only check the first class to avoid spurious duplicates
|
||||
continue;
|
||||
}
|
||||
has_class = true;
|
||||
"class".to_string()
|
||||
}
|
||||
ast::Attr::Id { .. } => "id".to_string(),
|
||||
ast::Attr::Named { named_attr } => named_attr
|
||||
.name
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|token| token.to_string())
|
||||
.collect(),
|
||||
};
|
||||
let entry = attr_map.entry(name).or_default();
|
||||
entry.push(attr.span());
|
||||
}
|
||||
|
||||
for (name, spans) in attr_map {
|
||||
if spans.len() > 1 {
|
||||
let mut spans = spans.into_iter();
|
||||
let first_span = spans.next().expect("spans should be non-empty");
|
||||
abort!(first_span, "duplicate attribute `{}`", name);
|
||||
}
|
||||
}
|
||||
|
||||
attrs
|
||||
}
|
||||
|
||||
/// Parses the name of a class or ID.
|
||||
fn class_or_id_name(&mut self) -> ast::Markup {
|
||||
if let Some(symbol) = self.try_name() {
|
||||
ast::Markup::Symbol { symbol }
|
||||
} else {
|
||||
self.markup()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the `[cond]` syntax after an empty attribute or class shorthand.
|
||||
fn attr_toggler(&mut self) -> Option<ast::Toggler> {
|
||||
match self.peek() {
|
||||
Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Bracket => {
|
||||
self.advance();
|
||||
Some(ast::Toggler {
|
||||
cond: group.stream(),
|
||||
cond_span: SpanRange::single_span(group.span()),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an identifier, without dealing with namespaces.
|
||||
fn try_name(&mut self) -> Option<TokenStream> {
|
||||
let mut result = Vec::new();
|
||||
if let Some(token @ TokenTree::Ident(_)) = self.peek() {
|
||||
self.advance();
|
||||
result.push(token);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
let mut expect_ident = false;
|
||||
loop {
|
||||
expect_ident = match self.peek() {
|
||||
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '-' => {
|
||||
self.advance();
|
||||
result.push(TokenTree::Punct(punct.clone()));
|
||||
true
|
||||
}
|
||||
Some(TokenTree::Ident(ref ident)) if expect_ident => {
|
||||
self.advance();
|
||||
result.push(TokenTree::Ident(ident.clone()));
|
||||
false
|
||||
}
|
||||
_ => break,
|
||||
};
|
||||
}
|
||||
Some(result.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Parses a HTML element or attribute name, along with a namespace
|
||||
/// if necessary.
|
||||
fn try_namespaced_name(&mut self) -> Option<TokenStream> {
|
||||
let mut result = vec![self.try_name()?];
|
||||
if let Some(TokenTree::Punct(ref punct)) = self.peek() {
|
||||
if punct.as_char() == ':' {
|
||||
self.advance();
|
||||
result.push(TokenStream::from(TokenTree::Punct(punct.clone())));
|
||||
result.push(self.try_name()?);
|
||||
}
|
||||
}
|
||||
Some(result.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Parses the given token stream as a Maud expression.
|
||||
fn block(&mut self, body: TokenStream, outer_span: SpanRange) -> ast::Block {
|
||||
let markups = self.with_input(body).markups();
|
||||
ast::Block {
|
||||
markups,
|
||||
outer_span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ pub fn impl_my_derive(input: &DeriveInput) -> Result<TokenStream, Error> {
|
|||
quote! {
|
||||
#name #body_assignment
|
||||
},
|
||||
format!("Returns a `{name}` default."),
|
||||
format!("Returns a `{}` default.", name),
|
||||
)
|
||||
}
|
||||
syn::Data::Enum(ref body) => {
|
||||
|
|
@ -44,7 +44,7 @@ pub fn impl_my_derive(input: &DeriveInput) -> Result<TokenStream, Error> {
|
|||
quote! {
|
||||
#name :: #default_variant_name #body_assignment
|
||||
},
|
||||
format!("Returns a `{name}::{default_variant_name}` default."),
|
||||
format!("Returns a `{}::{}` default.", name, default_variant_name),
|
||||
)
|
||||
}
|
||||
syn::Data::Union(_) => {
|
||||
|
|
@ -109,7 +109,7 @@ fn default_body_tt(body: &syn::Fields) -> Result<(TokenStream, String), Error> {
|
|||
.iter()
|
||||
.map(|field| {
|
||||
let (default_value, default_doc) = field_default_expr_and_doc(field)?;
|
||||
write!(&mut doc, "{default_doc}, ").unwrap();
|
||||
write!(&mut doc, "{}, ", default_doc).unwrap();
|
||||
Ok(default_value)
|
||||
})
|
||||
.collect::<Result<Vec<TokenStream>, Error>>()?;
|
||||
|
|
@ -145,7 +145,7 @@ fn field_default_expr_and_doc(field: &syn::Field) -> Result<(TokenStream, String
|
|||
ConversionStrategy::Into => quote!((#field_value).into()),
|
||||
};
|
||||
|
||||
let field_doc = format!("{field_value}");
|
||||
let field_doc = format!("{}", field_value);
|
||||
Ok((field_value, field_doc))
|
||||
} else {
|
||||
Ok((
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
# CHANGELOG
|
||||
|
||||
Este archivo documenta los cambios más relevantes realizados en cada versión. El formato está basado
|
||||
en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y las versiones se numeran siguiendo
|
||||
las reglas del [Versionado Semántico](https://semver.org/lang/es/).
|
||||
|
||||
Resume la evolución del proyecto para usuarios y colaboradores, destacando nuevas funcionalidades,
|
||||
correcciones, mejoras durante el desarrollo o cambios en la documentación. Cambios menores o
|
||||
internos pueden omitirse si no afectan al uso del proyecto.
|
||||
|
||||
## 0.1.2 (2025-09-20)
|
||||
|
||||
### Dependencias
|
||||
|
||||
- Actualiza dependencias para 0.4.0
|
||||
|
||||
### Documentado
|
||||
|
||||
- Normaliza referencias al nombre PageTop
|
||||
|
||||
## 0.1.1 (2025-08-16)
|
||||
|
||||
### Documentado
|
||||
|
||||
- Cambia el formato para la documentación
|
||||
|
||||
## 0.1.0 (2025-08-09)
|
||||
|
||||
### Añadido
|
||||
|
||||
- Versión inicial
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
[package]
|
||||
name = "pagetop-statics"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Librería para automatizar la recopilación de recursos estáticos en PageTop.
|
||||
"""
|
||||
categories = ["development-tools::build-utils"]
|
||||
keywords = ["pagetop", "build", "static", "resources", "file"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["change-detection"]
|
||||
sort = []
|
||||
|
||||
[dependencies]
|
||||
change-detection = { version = "1.2", optional = true }
|
||||
mime_guess = "2.0"
|
||||
path-slash = "0.2"
|
||||
|
||||
actix-web.workspace = true
|
||||
derive_more = "0.99.17"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||
|
||||
[build-dependencies]
|
||||
change-detection = { version = "1.2", optional = true }
|
||||
mime_guess = "2.0"
|
||||
path-slash = "0.2"
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Statics</h1>
|
||||
|
||||
<p>Librería para automatizar la recopilación de recursos estáticos en <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-statics)
|
||||
[](https://crates.io/crates/pagetop-statics)
|
||||
[](https://crates.io/crates/pagetop-statics)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics#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
|
||||
|
||||
Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
|
||||
compilación.
|
||||
|
||||
## 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
|
||||
[actix-web-static-files](https://crates.io/crates/actix_web_static_files) (versión
|
||||
[4.0.1](https://github.com/kilork/actix-web-static-files/tree/v4.0.1)), desarrollados ambos por
|
||||
[Alexander Korolev](https://crates.io/users/kilork).
|
||||
|
||||
Estas implementaciones se integran en PageTop para evitar que cada proyecto tenga que declarar
|
||||
`static-files` manualmente como dependencia en su `Cargo.toml`.
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
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.
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
#![allow(dead_code)]
|
||||
#![doc(html_no_source)]
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
mod resource {
|
||||
include!("src/resource.rs");
|
||||
}
|
||||
use resource::generate_resources_mapping;
|
||||
mod resource_dir {
|
||||
include!("src/resource_dir.rs");
|
||||
}
|
||||
use resource_dir::resource_dir;
|
||||
mod sets {
|
||||
include!("src/sets.rs");
|
||||
}
|
||||
use sets::{generate_resources_sets, SplitByCount};
|
||||
|
||||
use std::{env, path::Path};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
resource_dir("./tests").build_test()?;
|
||||
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
generate_resources_mapping(
|
||||
"./tests",
|
||||
None,
|
||||
Path::new(&out_dir).join("generated_mapping.rs"),
|
||||
"pagetop_statics",
|
||||
)?;
|
||||
|
||||
generate_resources_sets(
|
||||
"./tests",
|
||||
None,
|
||||
Path::new(&out_dir).join("generated_sets.rs"),
|
||||
"sets",
|
||||
"generate",
|
||||
&mut SplitByCount::new(2),
|
||||
"pagetop_statics",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop Statics</h1>
|
||||
|
||||
<p>Librería para automatizar la recopilación de recursos estáticos en <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-statics)
|
||||
[](https://crates.io/crates/pagetop-statics)
|
||||
[](https://crates.io/crates/pagetop-statics)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics#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
|
||||
|
||||
Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
|
||||
compilación.
|
||||
|
||||
## 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
|
||||
[actix-web-static-files](https://crates.io/crates/actix_web_static_files) (versión
|
||||
[4.0.1](https://github.com/kilork/actix-web-static-files/tree/v4.0.1)), desarrollados ambos por
|
||||
[Alexander Korolev](https://crates.io/users/kilork).
|
||||
|
||||
Estas implementaciones se integran en PageTop para evitar que cada proyecto tenga que declarar
|
||||
`static-files` manualmente como dependencia en su `Cargo.toml`.
|
||||
*/
|
||||
|
||||
#![doc(test(no_crate_inject))]
|
||||
#![doc(
|
||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||
)]
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
/// Resource definition and single module based generation.
|
||||
pub mod resource;
|
||||
pub use resource::Resource as StaticResource;
|
||||
|
||||
mod resource_dir;
|
||||
pub use resource_dir::{resource_dir, ResourceDir};
|
||||
|
||||
mod resource_files;
|
||||
pub use resource_files::{ResourceFiles, UriSegmentError};
|
||||
|
||||
/// Support for module based generations. Use it for large data sets (more than 128 Mb).
|
||||
pub mod sets;
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
use path_slash::PathExt;
|
||||
use std::{
|
||||
fs::{self, File, Metadata},
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
/// Static files resource.
|
||||
pub struct Resource {
|
||||
pub data: &'static [u8],
|
||||
pub modified: u64,
|
||||
pub mime_type: &'static str,
|
||||
}
|
||||
|
||||
/// Used internally in generated functions.
|
||||
#[inline]
|
||||
pub fn new_resource(data: &'static [u8], modified: u64, mime_type: &'static str) -> Resource {
|
||||
Resource {
|
||||
data,
|
||||
modified,
|
||||
mime_type,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const DEFAULT_VARIABLE_NAME: &str = "r";
|
||||
|
||||
/// Generate resources for `project_dir` using `filter`.
|
||||
/// Result saved in `generated_filename` and function named as `fn_name`.
|
||||
///
|
||||
/// in `build.rs`:
|
||||
/// ```rust
|
||||
/// use std::{env, path::Path};
|
||||
/// use pagetop_statics::resource::generate_resources;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let out_dir = env::var("OUT_DIR").unwrap();
|
||||
/// let generated_filename = Path::new(&out_dir).join("generated.rs");
|
||||
/// generate_resources("./tests", None, generated_filename, "generate", "pagetop_statics").unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// in `main.rs`:
|
||||
/// ```rust
|
||||
/// include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
///
|
||||
/// fn main() {
|
||||
/// let generated_file = generate();
|
||||
///
|
||||
/// assert_eq!(generated_file.len(), 4);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn generate_resources<P: AsRef<Path>, G: AsRef<Path>>(
|
||||
project_dir: P,
|
||||
filter: Option<fn(p: &Path) -> bool>,
|
||||
generated_filename: G,
|
||||
fn_name: &str,
|
||||
crate_name: &str,
|
||||
) -> io::Result<()> {
|
||||
let resources = collect_resources(&project_dir, filter)?;
|
||||
|
||||
let mut f = File::create(&generated_filename)?;
|
||||
|
||||
generate_function_header(&mut f, fn_name, crate_name)?;
|
||||
generate_uses(&mut f, crate_name)?;
|
||||
|
||||
generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?;
|
||||
generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?;
|
||||
generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?;
|
||||
|
||||
generate_function_end(&mut f)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate resource mapping for `project_dir` using `filter`.
|
||||
/// Result saved in `generated_filename` as anonymous block which returns HashMap<&'static str, Resource>.
|
||||
///
|
||||
/// in `build.rs`:
|
||||
/// ```rust
|
||||
///
|
||||
/// use std::{env, path::Path};
|
||||
/// use pagetop_statics::resource::generate_resources_mapping;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let out_dir = env::var("OUT_DIR").unwrap();
|
||||
/// let generated_filename = Path::new(&out_dir).join("generated_mapping.rs");
|
||||
/// generate_resources_mapping("./tests", None, generated_filename, "pagetop_statics").unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// in `main.rs`:
|
||||
/// ```rust
|
||||
/// use std::collections::HashMap;
|
||||
///
|
||||
/// use pagetop_statics::StaticResource;
|
||||
///
|
||||
/// fn generate_mapping() -> HashMap<&'static str, StaticResource> {
|
||||
/// include!(concat!(env!("OUT_DIR"), "/generated_mapping.rs"))
|
||||
/// }
|
||||
///
|
||||
/// fn main() {
|
||||
/// let generated_file = generate_mapping();
|
||||
///
|
||||
/// assert_eq!(generated_file.len(), 4);
|
||||
///
|
||||
/// }
|
||||
/// ```
|
||||
pub fn generate_resources_mapping<P: AsRef<Path>, G: AsRef<Path>>(
|
||||
project_dir: P,
|
||||
filter: Option<fn(p: &Path) -> bool>,
|
||||
generated_filename: G,
|
||||
crate_name: &str,
|
||||
) -> io::Result<()> {
|
||||
let resources = collect_resources(&project_dir, filter)?;
|
||||
|
||||
let mut f = File::create(&generated_filename)?;
|
||||
writeln!(f, "{{")?;
|
||||
|
||||
generate_uses(&mut f, crate_name)?;
|
||||
|
||||
generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?;
|
||||
|
||||
generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?;
|
||||
|
||||
generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?;
|
||||
|
||||
writeln!(f, "}}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sort"))]
|
||||
pub(crate) fn collect_resources<P: AsRef<Path>>(
|
||||
path: P,
|
||||
filter: Option<fn(p: &Path) -> bool>,
|
||||
) -> io::Result<Vec<(PathBuf, Metadata)>> {
|
||||
collect_resources_nested(path, filter)
|
||||
}
|
||||
|
||||
#[cfg(feature = "sort")]
|
||||
pub(crate) fn collect_resources<P: AsRef<Path>>(
|
||||
path: P,
|
||||
filter: Option<fn(p: &Path) -> bool>,
|
||||
) -> io::Result<Vec<(PathBuf, Metadata)>> {
|
||||
let mut resources = collect_resources_nested(path, filter)?;
|
||||
resources.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok(resources)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn collect_resources_nested<P: AsRef<Path>>(
|
||||
path: P,
|
||||
filter: Option<fn(p: &Path) -> bool>,
|
||||
) -> io::Result<Vec<(PathBuf, Metadata)>> {
|
||||
let mut result = vec![];
|
||||
|
||||
for entry in fs::read_dir(&path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if let Some(ref filter) = filter {
|
||||
if !filter(path.as_ref()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
let nested = collect_resources(path, filter)?;
|
||||
result.extend(nested);
|
||||
} else {
|
||||
result.push((path, entry.metadata()?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_resource_inserts<P: AsRef<Path>, W: Write>(
|
||||
f: &mut W,
|
||||
project_dir: &P,
|
||||
variable_name: &str,
|
||||
resources: Vec<(PathBuf, Metadata)>,
|
||||
) -> io::Result<()> {
|
||||
for resource in &resources {
|
||||
generate_resource_insert(f, project_dir, variable_name, resource)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_debug_formatting)]
|
||||
pub(crate) fn generate_resource_insert<P: AsRef<Path>, W: Write>(
|
||||
f: &mut W,
|
||||
project_dir: &P,
|
||||
variable_name: &str,
|
||||
resource: &(PathBuf, Metadata),
|
||||
) -> io::Result<()> {
|
||||
let (path, metadata) = resource;
|
||||
let abs_path = path.canonicalize()?;
|
||||
let key_path = path.strip_prefix(project_dir).unwrap().to_slash().unwrap();
|
||||
|
||||
let modified = if let Ok(Ok(modified)) = metadata
|
||||
.modified()
|
||||
.map(|x| x.duration_since(SystemTime::UNIX_EPOCH))
|
||||
{
|
||||
modified.as_secs()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mime_type = mime_guess::MimeGuess::from_path(path).first_or_octet_stream();
|
||||
writeln!(
|
||||
f,
|
||||
"{}.insert({:?},n(i!({:?}),{:?},{:?}));",
|
||||
variable_name, &key_path, &abs_path, modified, &mime_type,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_function_header<F: Write>(
|
||||
f: &mut F,
|
||||
fn_name: &str,
|
||||
crate_name: &str,
|
||||
) -> io::Result<()> {
|
||||
writeln!(
|
||||
f,
|
||||
"#[allow(clippy::unreadable_literal)] pub fn {fn_name}() -> ::std::collections::HashMap<&'static str, ::{crate_name}::StaticResource> {{",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_function_end<F: Write>(f: &mut F) -> io::Result<()> {
|
||||
writeln!(f, "}}")
|
||||
}
|
||||
|
||||
pub(crate) fn generate_uses<F: Write>(f: &mut F, crate_name: &str) -> io::Result<()> {
|
||||
writeln!(
|
||||
f,
|
||||
"use ::{crate_name}::resource::new_resource as n;
|
||||
use ::std::include_bytes as i;",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_variable_header<F: Write>(f: &mut F, variable_name: &str) -> io::Result<()> {
|
||||
writeln!(
|
||||
f,
|
||||
"let mut {variable_name} = ::std::collections::HashMap::new();",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_variable_return<F: Write>(f: &mut F, variable_name: &str) -> io::Result<()> {
|
||||
writeln!(f, "{variable_name}")
|
||||
}
|
||||
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