Añade soporte para responder páginas HTML

- Amplia la estructura "Page" para trabajar el renderizado con regiones
  de componentes para componer la página.
- Añade acciones "BeforeRenderBody" y "AfterRenderBody" para alterar el
  contenido de la página antes y después del renderizado.
- Actualiza "Context" para admitir parámetros dinámicos y mejorar la
  gestión de temas.
- Implementa el manejo de errores HTTP respondiendo páginas.
- Mejora la documentación y reorganiza el código en varios módulos.
This commit is contained in:
Manuel Cillero 2025-07-27 21:24:49 +02:00
parent f23c8d5c4c
commit a1bb6cd12d
17 changed files with 669 additions and 143 deletions

View file

@ -61,37 +61,42 @@ impl Error for ErrorParam {}
/// hojas de estilo ([`StyleSheet`]) y *scripts* ([`JavaScript`]), así como parámetros de contexto
/// definidos en tiempo de ejecución.
///
/// # Ejemplo
/// # Ejemplos
///
/// Crea un nuevo contexto asociado a una solicitud HTTP:
///
/// ```rust
/// use pagetop::prelude::*;
///
/// fn configure_context(cx: &mut Context) {
/// // Establece el idioma del documento a español.
/// cx.alter_langid(LangMatch::langid_or_default("es-ES"))
/// // Selecciona un tema (por su nombre corto).
/// .alter_theme("aliner")
/// // Añade un parámetro dinámico al contexto.
/// .alter_param("usuario_id", 42)
/// // Asigna un favicon.
/// .alter_assets(AssetsOp::SetFavicon(Some(
/// Favicon::new().with_icon("/icons/favicon.ico")
/// )))
/// // Añade una hoja de estilo externa.
/// .alter_assets(AssetsOp::AddStyleSheet(
/// StyleSheet::from("/css/style.css")
/// ))
/// // Añade un script JavaScript.
/// .alter_assets(AssetsOp::AddJavaScript(
/// JavaScript::defer("/js/main.js")
/// ));
/// fn new_context(request: HttpRequest) -> Context {
/// Context::new(request)
/// // Establece el idioma del documento a español.
/// .with_langid(LangMatch::langid_or_default("es-ES"))
/// // Selecciona un tema (por su nombre corto).
/// .with_theme("aliner")
/// // Asigna un favicon.
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
/// // Añade una hoja de estilo externa.
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/style.css")))
/// // Añade un script JavaScript.
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js")))
/// // Añade un parámetro dinámico al contexto.
/// .with_param("usuario_id", 42)
/// }
/// ```
///
/// Y hace operaciones con un contexto dado:
///
/// ```rust
/// use pagetop::prelude::*;
///
/// fn use_context(cx: &mut Context) {
/// // Recupera el tema seleccionado.
/// let active_theme = cx.theme();
/// assert_eq!(active_theme.short_name(), "aliner");
///
/// // Recupera el parámetro a su tipo original.
/// let id: i32 = cx.param("usuario_id").unwrap();
/// let id: i32 = cx.get_param("usuario_id").unwrap();
/// assert_eq!(id, 42);
///
/// // Genera un identificador para un componente de tipo `Menu`.
@ -102,32 +107,33 @@ impl Error for ErrorParam {}
/// ```
#[rustfmt::skip]
pub struct Context {
request : HttpRequest, // Solicitud HTTP de origen.
langid : &'static LanguageIdentifier, // Identificador del idioma.
theme : ThemeRef, // Referencia al tema para renderizar.
layout : &'static str, // Composición del documento para renderizar.
params : HashMap<String, String>, // Parámetros definidos en tiempo de ejecución.
favicon : Option<Favicon>, // Favicon, si se ha definido.
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
javascripts: Assets<JavaScript>, // Scripts JavaScript.
id_counter : usize, // Contador para generar identificadores únicos.
request : HttpRequest, // Solicitud HTTP de origen.
langid : &'static LanguageIdentifier, // Identificador de idioma.
theme : ThemeRef, // Referencia al tema para renderizar.
layout : &'static str, // Composición del documento para renderizar.
favicon : Option<Favicon>, // Favicon, si se ha definido.
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
javascripts: Assets<JavaScript>, // Scripts JavaScript.
params : HashMap<&'static str, String>, // Parámetros definidos en tiempo de ejecución.
id_counter : usize, // Contador para generar identificadores únicos.
}
impl Context {
// Crea un nuevo contexto asociado a una solicitud HTTP.
//
// El contexto inicializa el idioma por defecto, sin favicon ni recursos cargados.
/// Crea un nuevo contexto asociado a una solicitud HTTP.
///
/// El contexto inicializa el idioma, tema y composición por defecto, sin favicon ni recursos
/// cargados.
#[rustfmt::skip]
pub(crate) fn new(request: HttpRequest) -> Self {
pub fn new(request: HttpRequest) -> Self {
Context {
request,
langid : &DEFAULT_LANGID,
theme : *DEFAULT_THEME,
layout : "default",
params : HashMap::<String, String>::new(),
favicon : None,
stylesheets: Assets::<StyleSheet>::new(),
javascripts: Assets::<JavaScript>::new(),
params : HashMap::<&str, String>::new(),
id_counter : 0,
}
}
@ -141,37 +147,24 @@ impl Context {
self
}
/// Establece el tema que se usará para renderizar el documento.
/// Modifica el tema que se usará para renderizar el documento.
///
/// Localiza el tema por su [`short_name`](crate::core::AnyInfo::short_name), y si no aplica
/// ninguno entonces usará el tema por defecto.
#[builder_fn]
pub fn with_theme(mut self, theme_name: impl AsRef<str>) -> Self {
pub fn with_theme(mut self, theme_name: &'static str) -> Self {
self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME);
self
}
/// Define el tipo de composición usado para renderizar el documento.
/// Modifica la composición para renderizar el documento.
#[builder_fn]
pub fn with_layout(mut self, layout_name: &'static str) -> Self {
self.layout = layout_name;
self
}
/// Añade o modifica un parámetro del contexto almacenando el valor como [`String`].
#[builder_fn]
pub fn with_param<T: ToString>(mut self, key: impl AsRef<str>, value: T) -> Self {
self.params
.insert(key.as_ref().to_string(), value.to_string());
self
}
/// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó.
pub fn remove_param(&mut self, key: impl AsRef<str>) -> bool {
self.params.remove(key.as_ref()).is_some()
}
/// Modifica información o recursos del contexto usando [`AssetsOp`].
/// Define los recursos del contexto usando [`AssetsOp`].
#[builder_fn]
pub fn with_assets(mut self, op: AssetsOp) -> Self {
match op {
@ -209,7 +202,7 @@ impl Context {
&self.request
}
/// Devuelve el identificador del idioma asociado al documento.
/// Devuelve el identificador de idioma asociado al documento.
pub fn langid(&self) -> &LanguageIdentifier {
self.langid
}
@ -219,23 +212,11 @@ impl Context {
self.theme
}
/// Devuelve el tipo de composición usado para renderizar el documento. El valor predeterminado
/// es `"default"`.
/// Devuelve la composición para renderizar el documento. Por defecto es `"default"`.
pub fn layout(&self) -> &str {
self.layout
}
/// Recupera un parámetro del contexto convertido al tipo especificado.
///
/// Devuelve un error si el parámetro no existe ([`ErrorParam::NotFound`]) o la conversión falla
/// ([`ErrorParam::ParseError`]).
pub fn param<T: FromStr>(&self, key: impl AsRef<str>) -> Result<T, ErrorParam> {
self.params
.get(key.as_ref())
.ok_or(ErrorParam::NotFound)
.and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone())))
}
// Context RENDER ******************************************************************************
/// Renderiza los recursos del contexto.
@ -249,6 +230,31 @@ impl Context {
}
}
// Context PARAMS ******************************************************************************
/// Añade o modifica un parámetro del contexto almacenando el valor como [`String`].
#[builder_fn]
pub fn with_param<T: ToString>(mut self, key: &'static str, value: T) -> Self {
self.params.insert(key, value.to_string());
self
}
/// Recupera un parámetro del contexto convertido al tipo especificado.
///
/// Devuelve un error si el parámetro no existe ([`ErrorParam::NotFound`]) o la conversión falla
/// ([`ErrorParam::ParseError`]).
pub fn get_param<T: FromStr>(&self, key: &'static str) -> Result<T, ErrorParam> {
self.params
.get(key)
.ok_or(ErrorParam::NotFound)
.and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone())))
}
/// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó.
pub fn remove_param(&mut self, key: &'static str) -> bool {
self.params.remove(key).is_some()
}
// Context EXTRAS ******************************************************************************
/// Genera un identificador único si no se proporciona uno explícito.