♻️ Refactoriza la API de Children e InRegion

- Patrón prototipo en `InRegion`: cada petición recibe clones profundos.
- `ComponentClone` habilita clonar `dyn Component` de forma segura.
- `ChildTyped<C>` renombrado a `Slot<C>`, elimina `ChildTypedOp`.
- `Mutex` en lugar de `Arc<RwLock>` en `Child` y `Slot`.
- `is_renderable` y `setup_before_prepare` reciben `&Context`.
- Nuevos tests para `Children`, `ChildOp` y `Slot`.
This commit is contained in:
Manuel Cillero 2026-03-26 20:36:32 +01:00
parent 04e3d5b3c2
commit 54f990b11c
33 changed files with 740 additions and 314 deletions

View file

@ -5,9 +5,9 @@ use pagetop::prelude::*;
/// Tipo de función para manipular componentes y su contexto de renderizado.
///
/// Se usa en [`action::component::BeforeRender`] y [`action::component::AfterRender`] para alterar
/// el comportamiento predefinido de los componentes.
/// el comportamiento predefinido de los componentes antes y después de renderizarlos.
///
/// Recibe referencias mutables (`&mut`) del componente `component` y del contexto `cx`.
/// Recibe referencias mutables del componente `component` y del contexto `cx`.
pub type FnActionWithComponent<C> = fn(component: &mut C, cx: &mut Context);
/// Tipo de función para alterar el [`Markup`] generado por un componente.
@ -18,10 +18,12 @@ pub type FnActionWithComponent<C> = fn(component: &mut C, cx: &mut Context);
/// sustituciones, concatenaciones y cualquier otra primitiva de trabajo con cadenas.
///
/// La función recibe una referencia inmutable al componente `component` (el renderizado ya ha
/// concluido, solo se necesita leer su estado), una referencia mutable al contexto `cx`, y toma
/// posesión del `markup` producido hasta ese momento. Devuelve el nuevo [`Markup`] transformado,
/// que se encadena como entrada para la siguiente acción registrada, si la hay.
pub type FnActionTransformMarkup<C> = fn(component: &C, cx: &mut Context, markup: Markup) -> Markup;
/// concluido, solo se necesita leer su estado), y al contexto `cx` (solo para consulta), y toma
/// posesión del `markup` producido hasta ese momento.
///
/// Devuelve el nuevo [`Markup`] transformado, que se encadena como entrada para la siguiente acción
/// registrada, si la hay.
pub type FnActionTransformMarkup<C> = fn(component: &C, cx: &Context, markup: Markup) -> Markup;
mod before_render_component;
pub use before_render_component::*;

View file

@ -54,7 +54,7 @@ impl<C: Component> TransformMarkup<C> {
/// Despacha las acciones encadenando el [`Markup`] entre cada una.
#[inline]
pub(crate) fn dispatch(component: &C, cx: &mut Context, markup: Markup) -> Markup {
pub(crate) fn dispatch(component: &C, cx: &Context, markup: Markup) -> Markup {
let mut output = markup;
// Primero despacha las acciones para el tipo de componente.

View file

@ -4,7 +4,7 @@ use crate::prelude::*;
///
/// Los bloques se utilizan como contenedores de otros componentes o contenidos, con un título
/// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*).
#[derive(AutoDefault, Debug, Getters)]
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Block {
#[getters(skip)]
id: AttrId,
@ -25,11 +25,11 @@ impl Component for Block {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
fn setup(&mut self, _cx: &Context) {
self.alter_classes(ClassesOp::Prepend, "block");
}
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let block_body = self.children().render(cx);
if block_body.is_empty() {

View file

@ -1,6 +1,7 @@
use crate::prelude::*;
use std::fmt;
use std::sync::Arc;
/// Componente básico que renderiza dinámicamente código HTML según el contexto.
///
@ -31,7 +32,8 @@ use std::fmt;
/// }
/// });
/// ```
pub struct Html(Box<dyn Fn(&mut Context) -> Markup + Send + Sync>);
#[derive(Clone)]
pub struct Html(Arc<dyn Fn(&mut Context) -> Markup + Send + Sync>);
impl fmt::Debug for Html {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -52,7 +54,7 @@ impl Component for Html {
Self::default()
}
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(self.html(cx))
}
}
@ -62,26 +64,26 @@ impl Html {
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
///
/// El método [`Self::prepare_component()`] delega el renderizado a la función que aquí se
/// proporciona, que recibe una referencia mutable al [`Context`].
/// El método [`Self::prepare()`] delega el renderizado a la función que aquí se proporciona,
/// con una llamada que requiere una referencia mutable al [`Context`].
pub fn with<F>(f: F) -> Self
where
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
{
Html(Box::new(f))
Html(Arc::new(f))
}
/// Sustituye la función que genera el `Markup`.
///
/// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando
/// [`Self::prepare_component()`] invoque esta instancia. La nueva función también recibe una
/// referencia al [`Context`].
/// [`Self::prepare()`] invoque esta instancia. La nueva función también recibe una referencia
/// mutable al [`Context`].
#[builder_fn]
pub fn with_fn<F>(mut self, f: F) -> Self
where
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
{
self.0 = Box::new(f);
self.0 = Arc::new(f);
self
}
@ -91,8 +93,8 @@ impl Html {
///
/// Normalmente no se invoca manualmente, ya que el proceso de renderizado de los componentes lo
/// invoca automáticamente durante la construcción de la página. Puede usarse, no obstante, para
/// sobrescribir [`prepare_component()`](crate::core::component::Component::prepare_component)
/// y alterar el comportamiento del componente.
/// sobrescribir [`prepare()`](crate::core::component::Component::prepare) y alterar el
/// comportamiento del componente.
pub fn html(&self, cx: &mut Context) -> Markup {
(self.0)(cx)
}

View file

@ -76,7 +76,7 @@ pub enum IntroOpening {
/// })),
/// );
/// ```
#[derive(Debug, Getters)]
#[derive(Clone, Debug, Getters)]
pub struct Intro {
/// Devuelve el título de entrada.
title: L10n,
@ -109,13 +109,10 @@ impl Component for Intro {
Self::default()
}
fn setup_before_prepare(&mut self, cx: &mut Context) {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
cx.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION),
));
}
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
if *self.opening() == IntroOpening::PageTop {
cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
util::indoc!(r#"

View file

@ -8,7 +8,7 @@ const LINK: &str = "<a href=\"https://pagetop.cillero.es\" rel=\"noopener norefe
/// Por defecto, usando [`default()`](Self::default) sólo se muestra un reconocimiento a PageTop.
/// Sin embargo, se puede usar [`new()`](Self::new) para crear una instancia con un texto de
/// copyright predeterminado.
#[derive(AutoDefault, Debug, Getters)]
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct PoweredBy {
/// Devuelve el texto de copyright actual, si existe.
copyright: Option<String>,
@ -25,7 +25,7 @@ impl Component for PoweredBy {
PoweredBy { copyright: Some(c) }
}
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! {
div id=[self.id()] class="poweredby" {
@if let Some(c) = self.copyright() {