Compare commits

...

2 commits

Author SHA1 Message Date
04e3d5b3c2 Completa la API de temas con setup_component!
Elimina `action::theme` fusionando sus responsabilidades en
`action::component`. Renombra `AlterMarkup` a `TransformMarkup` y
`FnActionAlterMarkup` a `FnActionTransformMarkup`. Simplifica
`ActionKey` y mueve los tipos de función al módulo de componente.
2026-03-23 15:52:06 +01:00
0c648fb95a Añade acción AlterMarkup para filtrar render
Permite a las extensiones transformar el `Markup` final de un componente
mediante edición de texto. Se despacha como último paso del ciclo de
renderizado.
2026-03-22 12:15:31 +01:00
18 changed files with 252 additions and 221 deletions

View file

@ -1,17 +1,5 @@
//! Acciones predefinidas para alterar el funcionamiento interno de PageTop. //! Acciones predefinidas para alterar el funcionamiento interno de PageTop.
use crate::prelude::*;
/// Tipo de función para manipular componentes y su contexto de renderizado.
///
/// Se usa en acciones definidas en [`component`] y [`theme`] para alterar el comportamiento de los
/// componentes.
///
/// Recibe referencias mutables (`&mut`) del componente `component` y del contexto `cx`.
pub type FnActionWithComponent<C> = fn(component: &mut C, cx: &mut Context);
pub mod component; pub mod component;
pub mod theme;
pub mod page; pub mod page;

View file

@ -1,7 +1,33 @@
//! Acciones que operan sobre componentes. //! Acciones que operan sobre componentes.
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.
///
/// Recibe referencias mutables (`&mut`) 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.
///
/// Se usa en [`action::component::TransformMarkup`] para permitir a las extensiones alterar el HTML
/// final producido por el renderizado de un componente. La edición trabaja a nivel de texto: el
/// [`Markup`] recibido expone su contenido como [`String`], lo que permite aplicar búsquedas,
/// 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;
mod before_render_component; mod before_render_component;
pub use before_render_component::*; pub use before_render_component::*;
mod after_render_component; mod after_render_component;
pub use after_render_component::*; pub use after_render_component::*;
mod transform_markup_component;
pub use transform_markup_component::*;

View file

@ -1,6 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use crate::base::action::FnActionWithComponent; use super::FnActionWithComponent;
/// Ejecuta [`FnActionWithComponent`] después de renderizar un componente. /// Ejecuta [`FnActionWithComponent`] después de renderizar un componente.
pub struct AfterRender<C: Component> { pub struct AfterRender<C: Component> {
@ -57,23 +57,14 @@ impl<C: Component> AfterRender<C> {
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) { pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
// Primero despacha las acciones para el tipo de componente. // Primero despacha las acciones para el tipo de componente.
dispatch_actions( dispatch_actions(
&ActionKey::new( &ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), None),
UniqueId::of::<Self>(),
None,
Some(UniqueId::of::<C>()),
None,
),
|action: &Self| (action.f)(component, cx), |action: &Self| (action.f)(component, cx),
); );
// Y luego despacha las acciones para el tipo de componente con un identificador dado. // Y luego despacha las acciones para el tipo de componente con un identificador dado.
if let Some(id) = component.id() { if let Some(id) = component.id() {
dispatch_actions( dispatch_actions(
&ActionKey::new( &ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), Some(id)),
UniqueId::of::<Self>(),
None,
Some(UniqueId::of::<C>()),
Some(id),
),
|action: &Self| (action.f)(component, cx), |action: &Self| (action.f)(component, cx),
); );
} }

View file

@ -1,6 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use crate::base::action::FnActionWithComponent; use super::FnActionWithComponent;
/// Ejecuta [`FnActionWithComponent`] antes de renderizar el componente. /// Ejecuta [`FnActionWithComponent`] antes de renderizar el componente.
pub struct BeforeRender<C: Component> { pub struct BeforeRender<C: Component> {
@ -57,23 +57,14 @@ impl<C: Component> BeforeRender<C> {
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) { pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
// Primero despacha las acciones para el tipo de componente. // Primero despacha las acciones para el tipo de componente.
dispatch_actions( dispatch_actions(
&ActionKey::new( &ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), None),
UniqueId::of::<Self>(),
None,
Some(UniqueId::of::<C>()),
None,
),
|action: &Self| (action.f)(component, cx), |action: &Self| (action.f)(component, cx),
); );
// Y luego despacha las aciones para el tipo de componente con un identificador dado. // Y luego despacha las aciones para el tipo de componente con un identificador dado.
if let Some(id) = component.id() { if let Some(id) = component.id() {
dispatch_actions( dispatch_actions(
&ActionKey::new( &ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), Some(id)),
UniqueId::of::<Self>(),
None,
Some(UniqueId::of::<C>()),
Some(id),
),
|action: &Self| (action.f)(component, cx), |action: &Self| (action.f)(component, cx),
); );
} }

View file

@ -0,0 +1,82 @@
use crate::prelude::*;
use super::FnActionTransformMarkup;
/// Ejecuta [`FnActionTransformMarkup`] para alterar el renderizado de componentes.
pub struct TransformMarkup<C: Component> {
f: FnActionTransformMarkup<C>,
referer_type_id: Option<UniqueId>,
referer_id: AttrId,
weight: Weight,
}
/// Filtro para despachar [`FnActionTransformMarkup`] sobre el renderizado de un componente `C`.
impl<C: Component> ActionDispatcher for TransformMarkup<C> {
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
fn referer_type_id(&self) -> Option<UniqueId> {
self.referer_type_id
}
/// Devuelve el identificador del componente.
fn referer_id(&self) -> Option<String> {
self.referer_id.get()
}
/// Devuelve el peso para definir el orden de ejecución.
fn weight(&self) -> Weight {
self.weight
}
}
impl<C: Component> TransformMarkup<C> {
/// Permite [registrar](Extension::actions) una nueva acción [`FnActionTransformMarkup`].
pub fn new(f: FnActionTransformMarkup<C>) -> Self {
TransformMarkup {
f,
referer_type_id: Some(UniqueId::of::<C>()),
referer_id: AttrId::default(),
weight: 0,
}
}
/// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el
/// componente `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
self.referer_id.alter_id(id);
self
}
/// Opcional. Acciones con pesos más bajos se aplican antes. Se pueden usar valores negativos.
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
/// Despacha las acciones encadenando el [`Markup`] entre cada una.
#[inline]
pub(crate) fn dispatch(component: &C, cx: &mut Context, markup: Markup) -> Markup {
let mut output = markup;
// Primero despacha las acciones para el tipo de componente.
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), None),
|action: &Self| {
let taken = std::mem::replace(&mut output, html! {});
output = (action.f)(component, cx, taken);
},
);
// Y luego despacha las acciones para el tipo de componente con un identificador dado.
if let Some(id) = component.id() {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), Some(id)),
|action: &Self| {
let taken = std::mem::replace(&mut output, html! {});
output = (action.f)(component, cx, taken);
},
);
}
output
}
}

View file

@ -1,6 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use crate::base::action::page::FnActionWithPage; use super::FnActionWithPage;
/// Ejecuta [`FnActionWithPage`](crate::base::action::page::FnActionWithPage) después de renderizar /// Ejecuta [`FnActionWithPage`](crate::base::action::page::FnActionWithPage) después de renderizar
/// el cuerpo de la página. /// el cuerpo de la página.
@ -39,7 +39,7 @@ impl AfterRenderBody {
#[allow(clippy::inline_always)] #[allow(clippy::inline_always)]
pub(crate) fn dispatch(page: &mut Page) { pub(crate) fn dispatch(page: &mut Page) {
dispatch_actions( dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None, None), &ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| (action.f)(page), |action: &Self| (action.f)(page),
); );
} }

View file

@ -1,6 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use crate::base::action::page::FnActionWithPage; use super::FnActionWithPage;
/// Ejecuta [`FnActionWithPage`](crate::base::action::page::FnActionWithPage) antes de renderizar /// Ejecuta [`FnActionWithPage`](crate::base::action::page::FnActionWithPage) antes de renderizar
/// el cuerpo de la página. /// el cuerpo de la página.
@ -39,7 +39,7 @@ impl BeforeRenderBody {
#[allow(clippy::inline_always)] #[allow(clippy::inline_always)]
pub(crate) fn dispatch(page: &mut Page) { pub(crate) fn dispatch(page: &mut Page) {
dispatch_actions( dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None, None), &ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| (action.f)(page), |action: &Self| (action.f)(page),
); );
} }

View file

@ -1,7 +0,0 @@
//! Acciones lanzadas desde los temas.
mod before_render_component;
pub use before_render_component::*;
mod after_render_component;
pub use after_render_component::*;

View file

@ -1,50 +0,0 @@
use crate::prelude::*;
use crate::base::action::FnActionWithComponent;
/// Ejecuta [`FnActionWithComponent`] después de que un tema renderice el componente.
pub struct AfterRender<C: Component> {
f: FnActionWithComponent<C>,
theme_type_id: Option<UniqueId>,
referer_type_id: Option<UniqueId>,
}
/// Filtro para despachar [`FnActionWithComponent`] después de que un tema renderice el componente
/// `C`.
impl<C: Component> ActionDispatcher for AfterRender<C> {
/// Devuelve el identificador de tipo ([`UniqueId`]) del tema.
fn theme_type_id(&self) -> Option<UniqueId> {
self.theme_type_id
}
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
fn referer_type_id(&self) -> Option<UniqueId> {
self.referer_type_id
}
}
impl<C: Component> AfterRender<C> {
/// Permite [registrar](Extension::actions) una nueva acción [`FnActionWithComponent`] para un
/// tema dado.
pub fn new(theme: ThemeRef, f: FnActionWithComponent<C>) -> Self {
AfterRender {
f,
theme_type_id: Some(theme.type_id()),
referer_type_id: Some(UniqueId::of::<C>()),
}
}
/// Despacha las acciones.
#[inline]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
dispatch_actions(
&ActionKey::new(
UniqueId::of::<Self>(),
Some(cx.theme().type_id()),
Some(UniqueId::of::<C>()),
None,
),
|action: &Self| (action.f)(component, cx),
);
}
}

View file

@ -1,50 +0,0 @@
use crate::prelude::*;
use crate::base::action::FnActionWithComponent;
/// Ejecuta [`FnActionWithComponent`] antes de que un tema renderice el componente.
pub struct BeforeRender<C: Component> {
f: FnActionWithComponent<C>,
theme_type_id: Option<UniqueId>,
referer_type_id: Option<UniqueId>,
}
/// Filtro para despachar [`FnActionWithComponent`] antes de que un tema renderice el componente
/// `C`.
impl<C: Component> ActionDispatcher for BeforeRender<C> {
/// Devuelve el identificador de tipo ([`UniqueId`]) del tema.
fn theme_type_id(&self) -> Option<UniqueId> {
self.theme_type_id
}
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
fn referer_type_id(&self) -> Option<UniqueId> {
self.referer_type_id
}
}
impl<C: Component> BeforeRender<C> {
/// Permite [registrar](Extension::actions) una nueva acción [`FnActionWithComponent`] para un
/// tema dado.
pub fn new(theme: ThemeRef, f: FnActionWithComponent<C>) -> Self {
BeforeRender {
f,
theme_type_id: Some(theme.type_id()),
referer_type_id: Some(UniqueId::of::<C>()),
}
}
/// Despacha las acciones.
#[inline]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
dispatch_actions(
&ActionKey::new(
UniqueId::of::<Self>(),
Some(cx.theme().type_id()),
Some(UniqueId::of::<C>()),
None,
),
|action: &Self| (action.f)(component, cx),
);
}
}

View file

@ -14,7 +14,7 @@ mod all;
pub(crate) use all::add_action; pub(crate) use all::add_action;
pub use all::dispatch_actions; pub use all::dispatch_actions;
// **< actions_boxed! >***************************************************************************** // **< actions! >***********************************************************************************
/// Facilita la implementación del método [`actions()`](crate::core::extension::Extension::actions). /// Facilita la implementación del método [`actions()`](crate::core::extension::Extension::actions).
/// ///
@ -23,23 +23,28 @@ pub use all::dispatch_actions;
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// Extensión que ajusta un botón antes de renderizarlo y transforma su HTML final:
///
/// ```rust,ignore /// ```rust,ignore
/// impl Extension for MyTheme { /// impl Extension for MyExtension {
/// fn actions(&self) -> Vec<ActionBox> { /// fn actions(&self) -> Vec<ActionBox> {
/// actions_boxed![ /// actions![
/// action::theme::BeforeRender::<Button>::new(&Self, before_render_button), /// action::component::BeforeRender::<Button>::new(before_render_button),
/// action::theme::AfterRender::<Button>::new(&Self, after_render_button), /// action::component::TransformMarkup::<Button>::new(transform_button_markup),
/// ] /// ]
/// } /// }
/// } /// }
/// ///
/// impl Theme for MyTheme {} /// fn before_render_button(c: &mut Button, cx: &mut Context) {
/// todo!()
/// }
/// ///
/// fn before_render_button(c: &mut Button, cx: &mut Context) { todo!() } /// fn transform_button_markup(c: &Button, cx: &mut Context, markup: Markup) -> Markup {
/// fn after_render_button(c: &mut Button, cx: &mut Context) { todo!() } /// todo!()
/// }
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! actions_boxed { macro_rules! actions {
() => { () => {
Vec::<ActionBox>::new() Vec::<ActionBox>::new()
}; };

View file

@ -22,7 +22,6 @@ static ACTIONS: LazyLock<RwLock<HashMap<ActionKey, ActionsList>>> =
pub(crate) fn add_action(action: ActionBox) { pub(crate) fn add_action(action: ActionBox) {
let key = ActionKey::new( let key = ActionKey::new(
action.type_id(), action.type_id(),
action.theme_type_id(),
action.referer_type_id(), action.referer_type_id(),
action.referer_id(), action.referer_id(),
); );
@ -55,7 +54,6 @@ pub(crate) fn add_action(action: ActionBox) {
/// dispatch_actions( /// dispatch_actions(
/// &ActionKey::new( /// &ActionKey::new(
/// UniqueId::of::<Self>(), /// UniqueId::of::<Self>(),
/// Some(cx.theme().type_id()),
/// Some(UniqueId::of::<C>()), /// Some(UniqueId::of::<C>()),
/// None, /// None,
/// ), /// ),

View file

@ -11,7 +11,6 @@ pub type ActionBox = Box<dyn ActionDispatcher>;
#[derive(Eq, PartialEq, Hash)] #[derive(Eq, PartialEq, Hash)]
pub struct ActionKey { pub struct ActionKey {
action_type_id: UniqueId, action_type_id: UniqueId,
theme_type_id: Option<UniqueId>,
referer_type_id: Option<UniqueId>, referer_type_id: Option<UniqueId>,
referer_id: Option<String>, referer_id: Option<String>,
} }
@ -22,22 +21,19 @@ impl ActionKey {
/// Se crea con los siguientes campos: /// Se crea con los siguientes campos:
/// ///
/// - `action_type_id`: Tipo de la acción. /// - `action_type_id`: Tipo de la acción.
/// - `theme_type_id`: Opcional, identificador de tipo ([`UniqueId`]) del tema asociado.
/// - `referer_type_id`: Opcional, identificador de tipo ([`UniqueId`]) del componente referido. /// - `referer_type_id`: Opcional, identificador de tipo ([`UniqueId`]) del componente referido.
/// - `referer_id`: Opcional, identificador de la instancia (p. ej. para asociar la acción a un /// - `referer_id`: Opcional, identificador de la instancia (p. ej. para asociar la acción a un
/// componente concreto). /// componente concreto).
/// ///
/// Esta clave permitirá seleccionar las funciones a ejecutar para ese tipo de acción, con /// Esta clave permitirá seleccionar las funciones a ejecutar para ese tipo de acción, con
/// filtros opcionales por tema, componente, o una instancia concreta según su identificador. /// filtros opcionales por componente o por una instancia concreta según su identificador.
pub fn new( pub fn new(
action_type_id: UniqueId, action_type_id: UniqueId,
theme_type_id: Option<UniqueId>,
referer_type_id: Option<UniqueId>, referer_type_id: Option<UniqueId>,
referer_id: Option<String>, referer_id: Option<String>,
) -> Self { ) -> Self {
ActionKey { ActionKey {
action_type_id, action_type_id,
theme_type_id,
referer_type_id, referer_type_id,
referer_id, referer_id,
} }
@ -49,11 +45,6 @@ impl ActionKey {
/// Las acciones tienen que sobrescribir los métodos para el filtro que apliquen. Por defecto /// Las acciones tienen que sobrescribir los métodos para el filtro que apliquen. Por defecto
/// implementa un filtro nulo. /// implementa un filtro nulo.
pub trait ActionDispatcher: AnyInfo + Send + Sync { pub trait ActionDispatcher: AnyInfo + Send + Sync {
/// Identificador de tipo ([`UniqueId`]) del tema asociado. En este caso devuelve `None`.
fn theme_type_id(&self) -> Option<UniqueId> {
None
}
/// Identificador de tipo ([`UniqueId`]) del objeto referido. En este caso devuelve `None`. /// Identificador de tipo ([`UniqueId`]) del objeto referido. En este caso devuelve `None`.
fn referer_type_id(&self) -> Option<UniqueId> { fn referer_type_id(&self) -> Option<UniqueId> {
None None

View file

@ -75,10 +75,10 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
/// ///
/// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente /// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente
/// durante el proceso de construcción del documento cuando ningún tema sobrescribe el /// durante el proceso de construcción del documento cuando ningún tema sobrescribe el
/// renderizado mediante [`Theme::prepare_component()`](crate::core::theme::Theme::prepare_component). /// renderizado mediante [`Theme::handle_component()`](crate::core::theme::Theme::handle_component).
/// ///
/// Se recomienda obtener los datos del componente a través de sus propios métodos para que los /// Se recomienda obtener los datos del componente a través de sus propios métodos para que los
/// temas puedan implementar [`Theme::prepare_component()`](crate::core::theme::Theme::prepare_component) /// temas puedan implementar [`Theme::handle_component()`](crate::core::theme::Theme::handle_component)
/// sin depender de los detalles internos del componente. /// sin depender de los detalles internos del componente.
/// ///
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`). /// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`).
@ -99,21 +99,17 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
/// contexto actual. Si no es así, devuelve un [`Markup`] vacío. /// contexto actual. Si no es así, devuelve un [`Markup`] vacío.
/// 2. Ejecuta [`setup_before_prepare()`](Component::setup_before_prepare) para que el componente /// 2. Ejecuta [`setup_before_prepare()`](Component::setup_before_prepare) para que el componente
/// pueda ajustar su estructura interna o modificar el contexto. /// pueda ajustar su estructura interna o modificar el contexto.
/// 3. Despacha [`action::theme::BeforeRender<C>`](crate::base::action::theme::BeforeRender) para /// 3. Despacha [`action::component::BeforeRender<C>`](crate::base::action::component::BeforeRender)
/// permitir que el tema realice ajustes previos. /// para que las extensiones puedan hacer ajustes previos.
/// 4. Despacha [`action::component::BeforeRender<C>`](crate::base::action::component::BeforeRender) /// 4. **Prepara el renderizado del componente** recorriendo la cadena de temas (hijo → padre →
/// para que otras extensiones puedan también hacer ajustes previos. /// abuelo…) llamando a [`Theme::handle_component()`](crate::core::theme::Theme::handle_component)
/// 5. **Prepara el renderizado del componente**: /// en cada nivel hasta que uno devuelva `Some`. Si ninguno lo sobrescribe, llama a
/// - Recorre la cadena de temas llamando a /// [`Component::prepare_component()`](Component::prepare_component) del propio componente.
/// [`Theme::prepare_component()`](crate::core::theme::Theme::prepare_component) en cada nivel /// 5. Despacha [`action::component::AfterRender<C>`](crate::base::action::component::AfterRender)
/// (hijo → padre → abuelo…) hasta que uno devuelva `Some`. /// para que las extensiones puedan reaccionar con sus últimos ajustes.
/// - Si ningún tema lo sobrescribe, llama a /// 6. Despacha [`action::component::TransformMarkup<C>`](crate::base::action::component::TransformMarkup)
/// [`Component::prepare_component()`](Component::prepare_component) del propio componente. /// para que las extensiones puedan modificar el HTML final antes de devolverlo.
/// 6. Despacha [`action::theme::AfterRender<C>`](crate::base::action::theme::AfterRender) para /// 7. Devuelve el [`Markup`] resultante.
/// que el tema pueda aplicar ajustes finales.
/// 7. Despacha [`action::component::AfterRender<C>`](crate::base::action::component::AfterRender)
/// para que otras extensiones puedan hacer sus últimos ajustes.
/// 8. Devuelve el [`Markup`] generado en el paso 5.
impl<C: Component> ComponentRender for C { impl<C: Component> ComponentRender for C {
fn render(&mut self, cx: &mut Context) -> Markup { fn render(&mut self, cx: &mut Context) -> Markup {
// Si no es renderizable, devuelve un bloque HTML vacío. // Si no es renderizable, devuelve un bloque HTML vacío.
@ -124,9 +120,6 @@ impl<C: Component> ComponentRender for C {
// Configura el componente antes de preparar. // Configura el componente antes de preparar.
self.setup_before_prepare(cx); self.setup_before_prepare(cx);
// Acciones específicas del tema antes de renderizar el componente.
action::theme::BeforeRender::dispatch(self, cx);
// Acciones de las extensiones antes de renderizar el componente. // Acciones de las extensiones antes de renderizar el componente.
action::component::BeforeRender::dispatch(self, cx); action::component::BeforeRender::dispatch(self, cx);
@ -134,7 +127,7 @@ impl<C: Component> ComponentRender for C {
let prepare = match 'resolve: { let prepare = match 'resolve: {
let mut t: Option<ThemeRef> = Some(cx.theme()); let mut t: Option<ThemeRef> = Some(cx.theme());
while let Some(theme) = t { while let Some(theme) = t {
if let Some(r) = theme.prepare_component(self, cx) { if let Some(r) = theme.handle_component(self, cx) {
break 'resolve r; break 'resolve r;
} }
t = theme.parent(); t = theme.parent();
@ -154,13 +147,10 @@ impl<C: Component> ComponentRender for C {
} }
}; };
// Acciones específicas del tema después de renderizar el componente.
action::theme::AfterRender::dispatch(self, cx);
// Acciones de las extensiones después de renderizar el componente. // Acciones de las extensiones después de renderizar el componente.
action::component::AfterRender::dispatch(self, cx); action::component::AfterRender::dispatch(self, cx);
// Devuelve el marcado final. // Acciones de las extensiones que transforman el HTML final antes de devolverlo.
prepare action::component::TransformMarkup::dispatch(self, cx, prepare)
} }
} }

View file

@ -2,7 +2,7 @@ use crate::core::action::ActionBox;
use crate::core::theme::ThemeRef; use crate::core::theme::ThemeRef;
use crate::core::AnyInfo; use crate::core::AnyInfo;
use crate::locale::L10n; use crate::locale::L10n;
use crate::{actions_boxed, service}; use crate::{actions, service};
/// Interfaz común que debe implementar cualquier extensión de PageTop. /// Interfaz común que debe implementar cualquier extensión de PageTop.
/// ///
@ -74,10 +74,10 @@ pub trait Extension: AnyInfo + Send + Sync {
/// Devuelve la lista de acciones que la extensión registra. /// Devuelve la lista de acciones que la extensión registra.
/// ///
/// Estas [acciones](crate::core::action) se despachan por orden de registro o por /// Estas [acciones](crate::core::action) se despachan por orden de registro o por
/// [peso](crate::Weight) (ver [`actions_boxed!`](crate::actions_boxed)), permitiendo /// [peso](crate::Weight) (ver [`actions!`](crate::actions)), permitiendo
/// personalizar el comportamiento de la aplicación en puntos específicos. /// personalizar el comportamiento de la aplicación en puntos específicos.
fn actions(&self) -> Vec<ActionBox> { fn actions(&self) -> Vec<ActionBox> {
actions_boxed![] actions![]
} }
/// Inicializa la extensión durante la fase de arranque de la aplicación. /// Inicializa la extensión durante la fase de arranque de la aplicación.

View file

@ -20,7 +20,7 @@
//! //!
//! PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre. Un tema //! PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre. Un tema
//! hijo hereda automáticamente todos los métodos del padre y puede sobrescribirlos selectivamente: //! hijo hereda automáticamente todos los métodos del padre y puede sobrescribirlos selectivamente:
//! por ejemplo, puede redefinir el renderizado de un componente con [`Theme::prepare_component()`] //! por ejemplo, puede redefinir el renderizado de un componente con [`Theme::handle_component()`]
//! sin modificar el resto del comportamiento heredado. Un tema hijo puede ser a su vez padre de //! sin modificar el resto del comportamiento heredado. Un tema hijo puede ser a su vez padre de
//! otro, basta declararlo cada vez con [`Theme::parent()`]. //! otro, basta declararlo cada vez con [`Theme::parent()`].
//! //!
@ -208,27 +208,31 @@ impl Template for DefaultTemplate {}
// **< render_component! >************************************************************************** // **< render_component! >**************************************************************************
/// Sobrescribe el renderizado de componentes en la implementación de /// Sobrescribe el renderizado de componentes en
/// [`Theme::prepare_component()`](crate::core::theme::Theme::prepare_component). /// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component).
/// ///
/// Evalúa `$component` contra cada tipo listado en orden. En cuanto encuentra coincidencia, /// Evalúa `$component` contra cada tipo de componente listado en orden. En cuanto encuentra
/// devuelve `Some(Ok(markup))` o `Some(Err(e))` según el resultado de la expresión asociada. /// coincidencia, devuelve `Some(Ok(markup))` o `Some(Err(e))` según el resultado de la expresión
/// Si ningún tipo coincide, devuelve `None` para que el sistema continúe con la cadena de /// asociada. Si ningún tipo coincide, devuelve `None` para que el sistema continúe con la cadena de
/// herencia o con el renderizado por defecto del propio componente. /// herencia o con el renderizado por defecto del propio componente.
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,ignore /// ```rust,ignore
/// fn prepare_component( /// fn handle_component(
/// &self, /// &self,
/// component: &dyn Component, /// component: &dyn Component,
/// cx: &mut Context, /// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> { /// ) -> Option<Result<Markup, ComponentError>> {
/// render_component!(component, { /// render_component!(component, {
/// Button => |btn| Ok(html! { button.btn.btn-primary { (btn.label()) } }), /// Button => |btn| { Ok(html! { button.btn.btn-primary { (btn.label()) } }) },
/// Heading => |h| Ok(html! { h2.display-4 { (h.text()) } }), /// Heading => |h| self.render_heading(h, cx),
/// }) /// })
/// } /// }
///
/// fn render_heading(&self, h: &Heading, cx: &mut Context) -> Result<Markup, ComponentError> {
/// Ok(html! { h2.display-4 { (h.text()) } })
/// }
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! render_component { macro_rules! render_component {
@ -244,6 +248,66 @@ macro_rules! render_component {
}; };
} }
// **< setup_component! >***************************************************************************
/// Muta un componente dentro de
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component).
///
/// Evalúa `$component` contra cada tipo de componente listado en orden. En cuanto encuentra
/// coincidencia, ejecuta el bloque asociado y detiene la evaluación. Si ningún tipo coincide, no
/// hace nada.
///
/// Usa acceso mutable al componente mediante [`downcast_mut`](crate::core::AnyCast::downcast_mut),
/// lo que permite modificar su estado. El tema puede devolver `None` tras la mutación para que otro
/// nivel de la cadena se encargue del renderizado.
///
/// # Ejemplos
///
/// Solo mutación: el tema ajusta el componente y delega el renderizado al siguiente nivel:
///
/// ```rust,ignore
/// fn handle_component(
/// &self,
/// component: &mut dyn Component,
/// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> {
/// setup_component!(component, { Button => |btn| { btn.add_class("btn-primary"); } });
/// None
/// }
/// ```
///
/// Mutación y renderizado combinados: el `Button` se muta y se renderiza aquí; el `Heading` se
/// muta pero continúa la cadena para que otro nivel lo renderice:
///
/// ```rust,ignore
/// fn handle_component(
/// &self,
/// component: &mut dyn Component,
/// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> {
/// setup_component!(component, {
/// Button => |btn| { btn.add_class("btn-primary"); },
/// Heading => |h| { h.add_class("display-4"); },
/// });
/// render_component!(component, {
/// Button => |btn| Ok(html! { button.btn { (btn.label()) } }),
/// })
/// }
/// ```
#[macro_export]
macro_rules! setup_component {
($component:expr, { $($type:ty => |$var:ident| $body:expr),* $(,)? }) => {
'setup_component: {
$(
if let Some($var) = ($component).downcast_mut::<$type>() {
$body;
break 'setup_component;
}
)*
}
};
}
// **< Definitions >******************************************************************************** // **< Definitions >********************************************************************************
mod definition; mod definition;

View file

@ -184,30 +184,42 @@ pub trait Theme: Extension + Send + Sync {
} }
} }
/// Permite sobrescribir el renderizado de un componente. /// Permite al tema intervenir en el ciclo de renderizado de un componente.
/// ///
/// Este método tiene especial utilidad en los **temas hijo** porque permite sobrescribir el /// Este método tiene especial utilidad en los **temas hijo** porque permite sobrescribir el
/// renderizado que el propio componente o el tema padre ofrece para un componente concreto, sin /// renderizado que el propio componente o el tema padre ofrece para un componente concreto, sin
/// modificar el resto del comportamiento heredado. /// modificar el resto del comportamiento heredado.
/// ///
/// Recibe una referencia al componente (como objeto dinámico [`Component`]) y el contexto de /// Recibe una referencia mutable al componente (como objeto dinámico [`Component`]) y el
/// renderizado. Devuelve: /// contexto de renderizado. Devuelve:
/// ///
/// - `None` si este tema no sobrescribe el componente. Es la implementación por defecto. En /// - `None` si este tema no sobrescribe el renderizado. Es la implementación por defecto. El
/// este caso se recorre la cadena de temas padre y, si ninguno lo sobrescribe, se usa /// sistema continúa con el siguiente tema de la cadena y, si ninguno lo sobrescribe, usa
/// [`Component::prepare_component()`](crate::core::component::Component::prepare_component). /// [`Component::prepare_component()`](crate::core::component::Component::prepare_component).
/// El tema puede mutar el componente antes de devolver `None`, dejando que otro nivel de la
/// cadena se encargue del renderizado.
/// - `Some(Ok(markup))` con el HTML generado por el tema para el componente. /// - `Some(Ok(markup))` con el HTML generado por el tema para el componente.
///
/// > **Nota para componentes en región:** los componentes registrados con `InRegion` son
/// > instancias únicas compartidas entre peticiones. Cualquier mutación realizada aquí debe
/// > ser idempotente — sobrescribir valores, nunca acumular — o el estado se corromperá a
/// > partir de la segunda petición.
/// - `Some(Err(e))` si el tema intentó renderizarlo pero falló. /// - `Some(Err(e))` si el tema intentó renderizarlo pero falló.
/// ///
/// La mejor manera de implementar este método es usando la macro [`render_component!`], que /// Para renderizar usa [`render_component!`], que devuelve `None` si ningún tipo coincide. Para
/// determina el componente a renderizar y devuelve `None` si ninguno coincide: /// mutar sin renderizar usa [`setup_component!`] y devuelve `None` explícitamente:
/// ///
/// ```rust,ignore /// ```rust,ignore
/// fn prepare_component( /// fn handle_component(
/// &self, /// &self,
/// component: &dyn Component, /// component: &mut dyn Component,
/// cx: &mut Context, /// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> { /// ) -> Option<Result<Markup, ComponentError>> {
/// // Solo mutación: ajusta el componente y deja que otro nivel lo renderice.
/// setup_component!(component, {
/// Button => |btn| { btn.add_class("btn-primary"); },
/// });
/// // O renderizado completo:
/// render_component!(component, { /// render_component!(component, {
/// Button => |btn| Ok(html! { button.btn.btn-primary { (btn.label()) } }), /// Button => |btn| Ok(html! { button.btn.btn-primary { (btn.label()) } }),
/// Heading => |h| Ok(html! { h2.display-4 { (h.text()) } }), /// Heading => |h| Ok(html! { h2.display-4 { (h.text()) } }),
@ -215,9 +227,9 @@ pub trait Theme: Extension + Send + Sync {
/// } /// }
/// ``` /// ```
#[allow(unused_variables)] #[allow(unused_variables)]
fn prepare_component( fn handle_component(
&self, &self,
component: &dyn Component, component: &mut dyn Component,
cx: &mut Context, cx: &mut Context,
) -> Option<Result<Markup, ComponentError>> { ) -> Option<Result<Markup, ComponentError>> {
None None

View file

@ -17,9 +17,9 @@ pub use crate::include_locales;
// crate::service // crate::service
pub use crate::static_files_service; pub use crate::static_files_service;
// crate::core::action // crate::core::action
pub use crate::actions_boxed; pub use crate::actions;
// crate::core::theme // crate::core::theme
pub use crate::render_component; pub use crate::{render_component, setup_component};
// API. // API.