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.
This commit is contained in:
Manuel Cillero 2026-03-22 12:15:31 +01:00
parent 02bcf29eaa
commit 0c648fb95a
7 changed files with 141 additions and 9 deletions

View file

@ -4,12 +4,26 @@ 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.
/// Se usa en acciones definidas en [`action::component`] y [`action::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);
/// Tipo de función para modificar el [`Markup`] generado por un componente.
///
/// Se usa en [`action::component::AlterMarkup`] para permitir a las extensiones modificar 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 referencias mutables del componente `component` y del contexto `cx`, y toma
/// posesión del `markup` producido hasta ese momento. Devuelve el nuevo [`Markup`] modificado, que
/// se encadena como entrada para la siguiente acción registrada, si la hay.
pub type FnActionAlterMarkup<C> = fn(component: &mut C, cx: &mut Context, markup: Markup) -> Markup;
// **< Acciones por tipo >**************************************************************************
pub mod component;
pub mod theme;

View file

@ -5,3 +5,6 @@ pub use before_render_component::*;
mod after_render_component;
pub use after_render_component::*;
mod alter_markup_component;
pub use alter_markup_component::*;

View file

@ -65,6 +65,7 @@ impl<C: Component> AfterRender<C> {
),
|action: &Self| (action.f)(component, cx),
);
// Y luego despacha las acciones para el tipo de componente con un identificador dado.
if let Some(id) = component.id() {
dispatch_actions(

View file

@ -0,0 +1,93 @@
use crate::prelude::*;
use crate::base::action::FnActionAlterMarkup;
use crate::html::html;
/// Ejecuta [`FnActionAlterMarkup`] para modificar el renderizado de un componente.
pub struct AlterMarkup<C: Component> {
f: FnActionAlterMarkup<C>,
referer_type_id: Option<UniqueId>,
referer_id: AttrId,
weight: Weight,
}
/// Filtro para despachar [`FnActionAlterMarkup`] sobre el renderizado de un componente `C`.
impl<C: Component> ActionDispatcher for AlterMarkup<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> AlterMarkup<C> {
/// Permite [registrar](Extension::actions) una nueva acción [`FnActionAlterMarkup`].
pub fn new(f: FnActionAlterMarkup<C>) -> Self {
AlterMarkup {
f,
referer_type_id: Some(UniqueId::of::<C>()),
referer_id: AttrId::default(),
weight: 0,
}
}
/// Afina el registro para ejecutar la acción [`FnActionAlterMarkup`] 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: &mut 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>(),
None,
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>(),
None,
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

@ -65,6 +65,7 @@ impl<C: Component> BeforeRender<C> {
),
|action: &Self| (action.f)(component, cx),
);
// Y luego despacha las aciones para el tipo de componente con un identificador dado.
if let Some(id) = component.id() {
dispatch_actions(

View file

@ -21,7 +21,9 @@ pub use all::dispatch_actions;
/// Evita escribir repetidamente `Box::new(...)` para cada acción de la lista, manteniendo el código
/// más limpio.
///
/// # Ejemplo
/// # Ejemplos
///
/// Acciones de tema que ajustan un componente antes y después de renderizarlo:
///
/// ```rust,ignore
/// impl Extension for MyTheme {
@ -38,6 +40,22 @@ pub use all::dispatch_actions;
/// fn before_render_button(c: &mut Button, cx: &mut Context) { todo!() }
/// fn after_render_button(c: &mut Button, cx: &mut Context) { todo!() }
/// ```
///
/// Acción de extensión que transforma el HTML final de un componente mediante edición de texto:
///
/// ```rust,ignore
/// impl Extension for MyExtension {
/// fn actions(&self) -> Vec<ActionBox> {
/// actions_boxed![
/// action::component::AlterMarkup::<Button>::new(alter_button_markup),
/// ]
/// }
/// }
///
/// fn alter_button_markup(c: &mut Button, cx: &mut Context, markup: Markup) -> Markup {
/// todo!()
/// }
/// ```
#[macro_export]
macro_rules! actions_boxed {
() => {

View file

@ -109,11 +109,13 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
/// (hijo → padre → abuelo…) hasta que uno devuelva `Some`.
/// - Si ningún tema lo sobrescribe, llama a
/// [`Component::prepare_component()`](Component::prepare_component) del propio componente.
/// 6. Despacha [`action::theme::AfterRender<C>`](crate::base::action::theme::AfterRender) para
/// que el tema pueda aplicar ajustes finales.
/// 6. Despacha [`action::theme::AfterRender<C>`](crate::base::action::theme::AfterRender) para que
/// el tema pueda reaccionar tras el renderizado.
/// 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.
/// para que otras extensiones puedan reaccionar con sus últimos ajustes.
/// 8. Despacha [`action::component::AlterMarkup<C>`](crate::base::action::component::AlterMarkup)
/// para que las extensiones puedan modificar el HTML final antes de devolverlo.
/// 9. Devuelve el [`Markup`] resultante.
impl<C: Component> ComponentRender for C {
fn render(&mut self, cx: &mut Context) -> Markup {
// Si no es renderizable, devuelve un bloque HTML vacío.
@ -160,7 +162,7 @@ impl<C: Component> ComponentRender for C {
// Acciones de las extensiones después de renderizar el componente.
action::component::AfterRender::dispatch(self, cx);
// Devuelve el marcado final.
prepare
// Acciones de las extensiones que transforman el HTML final antes de devolverlo.
action::component::AlterMarkup::dispatch(self, cx, prepare)
}
}