Compare commits
6 commits
a5ee0fecb1
...
af309930f7
| Author | SHA1 | Date | |
|---|---|---|---|
| af309930f7 | |||
| 04dbbc8858 | |||
| 34aeeab2d7 | |||
| a0b14aec36 | |||
| c3feff9efd | |||
| 3e1bc0fb0e |
43 changed files with 632 additions and 484 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -8,3 +8,5 @@
|
|||
**/local.*.toml
|
||||
**/local.toml
|
||||
.env
|
||||
.cargo
|
||||
.vscode
|
||||
|
|
|
|||
|
|
@ -105,17 +105,17 @@ impl Extension for Aliner {
|
|||
|
||||
impl Theme for Aliner {
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/basic.css")
|
||||
.with_version(PAGETOP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/aliner/css/styles.css")
|
||||
.with_version(env!("CARGO_PKG_VERSION"))
|
||||
.with_weight(-99),
|
||||
|
|
|
|||
|
|
@ -151,12 +151,12 @@ impl Theme for Bootsier {
|
|||
}
|
||||
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/bootsier/bs/bootstrap.min.css")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
))
|
||||
.alter_assets(ContextOp::AddJavaScript(
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::prelude::*;
|
|||
///
|
||||
/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza
|
||||
/// si existen componentes hijos (*children*).
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Container {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -33,10 +33,10 @@ impl Component for Container {
|
|||
self.alter_classes(ClassesOp::Prepend, self.container_width().to_class());
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let output = self.children().render(cx);
|
||||
if output.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
let style = match self.container_width() {
|
||||
container::Width::FluidMax(w) if w.is_measurable() => {
|
||||
|
|
@ -44,38 +44,38 @@ impl Component for Container {
|
|||
}
|
||||
_ => None,
|
||||
};
|
||||
match self.container_kind() {
|
||||
container::Kind::Default => PrepareMarkup::With(html! {
|
||||
Ok(match self.container_kind() {
|
||||
container::Kind::Default => html! {
|
||||
div id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Main => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Main => html! {
|
||||
main id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Header => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Header => html! {
|
||||
header id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Footer => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Footer => html! {
|
||||
footer id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Section => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Section => html! {
|
||||
section id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Article => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Article => html! {
|
||||
article id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// Ver ejemplo en el módulo [`dropdown`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Dropdown {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -63,17 +63,17 @@ impl Component for Dropdown {
|
|||
);
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
// Si no hay elementos en el menú, no se prepara.
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
// Título opcional para el menú desplegable.
|
||||
let title = self.title().using(cx);
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
div id=[self.id()] class=[self.classes().get()] {
|
||||
@if !title.is_empty() {
|
||||
@let mut btn_classes = Classes::new({
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||
/// con él.
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Debug)]
|
||||
pub enum ItemKind {
|
||||
/// Elemento vacío, no produce salida.
|
||||
#[default]
|
||||
|
|
@ -43,7 +43,7 @@ pub enum ItemKind {
|
|||
///
|
||||
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Item {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -62,17 +62,17 @@ impl Component for Item {
|
|||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.item_kind() {
|
||||
ItemKind::Void => PrepareMarkup::None,
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self.item_kind() {
|
||||
ItemKind::Void => html! {},
|
||||
|
||||
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||
ItemKind::Label(label) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
span class="dropdown-item-text" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
|
|
@ -100,7 +100,7 @@ impl Component for Item {
|
|||
let aria_disabled = disabled.then_some("true");
|
||||
let tabindex = disabled.then_some("-1");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
a
|
||||
class=(classes)
|
||||
|
|
@ -114,7 +114,7 @@ impl Component for Item {
|
|||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ItemKind::Button { label, disabled } => {
|
||||
|
|
@ -126,7 +126,7 @@ impl Component for Item {
|
|||
let aria_disabled = disabled.then_some("true");
|
||||
let disabled_attr = disabled.then_some("disabled");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
button
|
||||
class=(classes)
|
||||
|
|
@ -137,21 +137,21 @@ impl Component for Item {
|
|||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ItemKind::Header(label) => PrepareMarkup::With(html! {
|
||||
ItemKind::Header(label) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
h6 class="dropdown-header" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
ItemKind::Divider => PrepareMarkup::With(html! {
|
||||
ItemKind::Divider => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] { hr class="dropdown-divider" {} }
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use crate::theme::form;
|
|||
/// .with_classes(ClassesOp::Add, "mb-3")
|
||||
/// .add_child(Input::new().with_name("q"));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Form {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -52,12 +52,12 @@ impl Component for Form {
|
|||
self.alter_classes(ClassesOp::Prepend, "form");
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let method = match self.method() {
|
||||
form::Method::Post => Some("post"),
|
||||
form::Method::Get => None,
|
||||
};
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
form
|
||||
id=[self.id()]
|
||||
class=[self.classes().get()]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use pagetop::prelude::*;
|
|||
/// Agrupa controles relacionados de un formulario (`<fieldset>`).
|
||||
///
|
||||
/// Se usa para mejorar la accesibilidad cuando se acompaña de una leyenda que encabeza el grupo.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Fieldset {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -22,8 +22,8 @@ impl Component for Fieldset {
|
|||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
PrepareMarkup::With(html! {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(html! {
|
||||
fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] {
|
||||
@if let Some(legend) = self.legend().lookup(cx) {
|
||||
legend { (legend) }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use pagetop::prelude::*;
|
|||
use crate::theme::form;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Input {
|
||||
classes: Classes,
|
||||
input_type: form::InputType,
|
||||
|
|
@ -36,9 +36,9 @@ impl Component for Input {
|
|||
);
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let id = self.name().get().map(|name| util::join!("edit-", name));
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
div class=[self.classes().get()] {
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label for=[&id] class="form-label" {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub enum IconKind {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Icon {
|
||||
/// Devuelve las clases CSS asociadas al icono.
|
||||
classes: Classes,
|
||||
|
|
@ -35,26 +35,26 @@ impl Component for Icon {
|
|||
}
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.icon_kind() {
|
||||
IconKind::None => PrepareMarkup::None,
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self.icon_kind() {
|
||||
IconKind::None => html! {},
|
||||
IconKind::Font(_) => {
|
||||
let aria_label = self.aria_label().lookup(cx);
|
||||
let has_label = aria_label.is_some();
|
||||
PrepareMarkup::With(html! {
|
||||
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! {
|
||||
html! {
|
||||
svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox=(viewbox)
|
||||
|
|
@ -67,11 +67,11 @@ impl Component for Icon {
|
|||
{
|
||||
(shapes)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn font() -> Self {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::prelude::*;
|
|||
/// ([`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`].
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Image {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -36,13 +36,13 @@ impl Component for Image {
|
|||
self.alter_classes(ClassesOp::Prepend, self.source().to_class());
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
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! {
|
||||
return Ok(html! {
|
||||
span
|
||||
id=[self.id()]
|
||||
class=[self.classes().get()]
|
||||
|
|
@ -59,7 +59,7 @@ impl Component for Image {
|
|||
image::Source::Thumbnail(source) => Some(source),
|
||||
image::Source::Plain(source) => Some(source),
|
||||
};
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
img
|
||||
src=[source]
|
||||
alt=(alt_text)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::prelude::*;
|
|||
///
|
||||
/// Ver ejemplo en el módulo [`nav`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Nav {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -42,13 +42,13 @@ impl Component for Nav {
|
|||
});
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
ul id=[self.id()] class=[self.classes().get()] {
|
||||
(items)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||
/// con él.
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Debug)]
|
||||
pub enum ItemKind {
|
||||
/// Elemento vacío, no produce salida.
|
||||
#[default]
|
||||
|
|
@ -76,7 +76,7 @@ impl ItemKind {
|
|||
///
|
||||
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Item {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -99,17 +99,17 @@ impl Component for Item {
|
|||
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,
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self.item_kind() {
|
||||
ItemKind::Void => html! {},
|
||||
|
||||
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||
ItemKind::Label(label) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
span class="nav-link disabled" aria-disabled="true" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
|
|
@ -136,7 +136,7 @@ impl Component for Item {
|
|||
let aria_current = (href.is_some() && is_current).then_some("page");
|
||||
let aria_disabled = (*disabled).then_some("true");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
a
|
||||
class=(classes)
|
||||
|
|
@ -149,27 +149,27 @@ impl Component for Item {
|
|||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ItemKind::Html(html) => PrepareMarkup::With(html! {
|
||||
ItemKind::Html(html) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
(html.render(cx))
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
ItemKind::Dropdown(menu) => {
|
||||
if let Some(dd) = menu.borrow() {
|
||||
let items = dd.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
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! {
|
||||
html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
a
|
||||
class="nav-link dropdown-toggle"
|
||||
|
|
@ -184,12 +184,12 @@ impl Component for Item {
|
|||
(items)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
PrepareMarkup::None
|
||||
}
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::prelude::*;
|
|||
/// - 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.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Brand {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -36,14 +36,14 @@ impl Component for Brand {
|
|||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let image = self.image().render(cx);
|
||||
let title = self.title().using(cx);
|
||||
if title.is_empty() && image.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
let slogan = self.slogan().using(cx);
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
@if let Some(route) = self.route() {
|
||||
a class="navbar-brand" href=(route(cx)) { (image) (title) (slogan) }
|
||||
} @else {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
|||
///
|
||||
/// Ver ejemplos en el módulo [`navbar`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Navbar {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -48,7 +48,7 @@ impl Component for Navbar {
|
|||
});
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
// 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 = util::join!("#", id_content);
|
||||
|
|
@ -75,13 +75,13 @@ impl Component for Navbar {
|
|||
// 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;
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
// Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas.
|
||||
let id = cx.required_id::<Self>(self.id());
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
nav id=(id) class=[self.classes().get()] {
|
||||
div class="container-fluid" {
|
||||
@match self.layout() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::prelude::*;
|
|||
/// 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)]
|
||||
#[derive(AutoDefault, Debug)]
|
||||
pub enum Item {
|
||||
/// Sin contenido, no produce salida.
|
||||
#[default]
|
||||
|
|
@ -46,31 +46,31 @@ impl Component for Item {
|
|||
}
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self {
|
||||
Self::Void => PrepareMarkup::None,
|
||||
Self::Brand(brand) => PrepareMarkup::With(html! { (brand.render(cx)) }),
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self {
|
||||
Self::Void => html! {},
|
||||
Self::Brand(brand) => 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;
|
||||
return Ok(html! {});
|
||||
}
|
||||
PrepareMarkup::With(html! {
|
||||
html! {
|
||||
ul id=[nav.id()] class=[nav.classes().get()] {
|
||||
(items)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
PrepareMarkup::None
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
Self::Text(text) => PrepareMarkup::With(html! {
|
||||
Self::Text(text) => html! {
|
||||
span class="navbar-text" {
|
||||
(text.using(cx))
|
||||
}
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::prelude::*;
|
|||
// **< Layout >*************************************************************************************
|
||||
|
||||
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Debug)]
|
||||
pub enum Layout {
|
||||
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// Ver ejemplo en el módulo [`offcanvas`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[derive(AutoDefault, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Offcanvas {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -62,8 +62,8 @@ impl Component for Offcanvas {
|
|||
});
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
PrepareMarkup::With(self.render_offcanvas(cx, None))
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(self.render_offcanvas(cx, None))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::prelude::*;
|
|||
/// los componentes.
|
||||
///
|
||||
/// Recibe una referencia al componente `component` y una referencia mutable al contexto `cx`.
|
||||
pub type FnPrepareRender<C> = fn(component: &C, cx: &mut Context) -> PrepareMarkup;
|
||||
pub type FnPrepareRender<C> = fn(component: &C, cx: &mut Context) -> Result<Markup, ComponentError>;
|
||||
|
||||
/// Ejecuta [`FnPrepareRender`] para preparar el renderizado de un componente.
|
||||
///
|
||||
|
|
@ -41,23 +41,25 @@ impl<C: Component> PrepareRender<C> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Despacha las acciones. Se detiene en cuanto una renderiza.
|
||||
/// Despacha las acciones. Se detiene en cuanto una renderiza o produce un error.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &C, cx: &mut Context) -> PrepareMarkup {
|
||||
let mut render_component = PrepareMarkup::None;
|
||||
dispatch_actions(
|
||||
pub(crate) fn dispatch(component: &C, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let mut render_result: Result<Markup, ComponentError> = Ok(html! {});
|
||||
dispatch_actions_until(
|
||||
&ActionKey::new(
|
||||
UniqueId::of::<Self>(),
|
||||
Some(cx.theme().type_id()),
|
||||
Some(UniqueId::of::<C>()),
|
||||
None,
|
||||
),
|
||||
|action: &Self| {
|
||||
if render_component.is_empty() {
|
||||
render_component = (action.f)(component, cx);
|
||||
|action: &Self| match &render_result {
|
||||
Ok(markup) if markup.is_empty() => {
|
||||
render_result = (action.f)(component, cx);
|
||||
std::ops::ControlFlow::Continue(())
|
||||
}
|
||||
_ => std::ops::ControlFlow::Break(()),
|
||||
},
|
||||
);
|
||||
render_component
|
||||
render_result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct Block {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
|
|
@ -29,16 +29,16 @@ impl Component for Block {
|
|||
self.alter_classes(ClassesOp::Prepend, "block");
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let block_body = self.children().render(cx);
|
||||
|
||||
if block_body.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
let id = cx.required_id::<Block>(self.id());
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
div id=(id) class=[self.classes().get()] {
|
||||
@if let Some(title) = self.title().lookup(cx) {
|
||||
h2 class="block__title" { span { (title) } }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Componente básico que renderiza dinámicamente código HTML según el contexto.
|
||||
///
|
||||
/// Este componente permite generar contenido HTML arbitrario, usando la macro `html!` y accediendo
|
||||
|
|
@ -31,6 +33,14 @@ use crate::prelude::*;
|
|||
/// ```
|
||||
pub struct Html(Box<dyn Fn(&mut Context) -> Markup + Send + Sync>);
|
||||
|
||||
impl fmt::Debug for Html {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Html")
|
||||
.field(&"Fn(&mut Context) -> Markup")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Html {
|
||||
fn default() -> Self {
|
||||
Self::with(|_| html! {})
|
||||
|
|
@ -42,8 +52,8 @@ impl Component for Html {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
PrepareMarkup::With(self.html(cx))
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(self.html(cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ pub enum IntroOpening {
|
|||
/// })),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Getters)]
|
||||
#[derive(Debug, Getters)]
|
||||
pub struct Intro {
|
||||
/// Devuelve el título de entrada.
|
||||
title: L10n,
|
||||
|
|
@ -110,14 +110,14 @@ impl Component for Intro {
|
|||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, cx: &mut Context) {
|
||||
cx.alter_assets(ContextOp::AddStyleSheet(
|
||||
cx.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION),
|
||||
));
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
if *self.opening() == IntroOpening::PageTop {
|
||||
cx.alter_assets(ContextOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
||||
cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
||||
util::indoc!(r#"
|
||||
try {
|
||||
const resp = await fetch("https://crates.io/api/v1/crates/pagetop");
|
||||
|
|
@ -135,7 +135,7 @@ impl Component for Intro {
|
|||
)));
|
||||
}
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
div class="intro" {
|
||||
div class="intro-header" {
|
||||
section class="intro-header__body" {
|
||||
|
|
|
|||
|
|
@ -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, Getters)]
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct PoweredBy {
|
||||
/// Devuelve el texto de copyright actual, si existe.
|
||||
copyright: Option<String>,
|
||||
|
|
@ -25,8 +25,8 @@ impl Component for PoweredBy {
|
|||
PoweredBy { copyright: Some(c) }
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
PrepareMarkup::With(html! {
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(html! {
|
||||
div id=[self.id()] class="poweredby" {
|
||||
@if let Some(c) = self.copyright() {
|
||||
span class="poweredby__copyright" { (c) "." } " "
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ impl Extension for Basic {
|
|||
|
||||
impl Theme for Basic {
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/basic.css")
|
||||
.with_version(PAGETOP_VERSION)
|
||||
.with_weight(-99),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use list::ActionsList;
|
|||
|
||||
mod all;
|
||||
pub(crate) use all::add_action;
|
||||
pub use all::dispatch_actions;
|
||||
pub use all::{dispatch_actions, dispatch_actions_until};
|
||||
|
||||
/// Facilita la implementación del método [`actions()`](crate::core::extension::Extension::actions).
|
||||
///
|
||||
|
|
@ -35,7 +35,7 @@ pub use all::dispatch_actions;
|
|||
/// impl Theme for MyTheme {}
|
||||
///
|
||||
/// fn before_render_button(c: &mut Button, cx: &mut Context) { todo!() }
|
||||
/// fn render_error404(c: &Error404, cx: &mut Context) -> PrepareMarkup { todo!() }
|
||||
/// fn render_error404(c: &Error404, cx: &mut Context) -> Markup { todo!() }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! actions_boxed {
|
||||
|
|
|
|||
|
|
@ -72,3 +72,18 @@ where
|
|||
list.iter_map(f);
|
||||
}
|
||||
}
|
||||
|
||||
/// Despacha las funciones asociadas a una [`ActionKey`] con posible salida anticipada.
|
||||
///
|
||||
/// Funciona igual que [`dispatch_actions`], pero el *closure* puede devolver
|
||||
/// [`std::ops::ControlFlow::Continue`] para continuar ejecutando la siguiente acción; o
|
||||
/// [`std::ops::ControlFlow::Break`] para detener la iteración inmediatamente.
|
||||
pub fn dispatch_actions_until<A, F>(key: &ActionKey, f: F)
|
||||
where
|
||||
A: ActionDispatcher,
|
||||
F: FnMut(&A) -> std::ops::ControlFlow<()>,
|
||||
{
|
||||
if let Some(list) = ACTIONS.read().get(key) {
|
||||
list.iter_try_map(f);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,4 +39,21 @@ impl ActionsList {
|
|||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub fn iter_try_map<A, F>(&self, mut f: F)
|
||||
where
|
||||
A: ActionDispatcher,
|
||||
F: FnMut(&A) -> std::ops::ControlFlow<()>,
|
||||
{
|
||||
let list = self.0.read();
|
||||
for a in list.iter().rev() {
|
||||
if let Some(action) = (**a).downcast_ref::<A>() {
|
||||
if f(action).is_break() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
trace::error!("Failed to downcast action of type {}", (**a).type_name());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
use crate::html::RoutePath;
|
||||
|
||||
mod error;
|
||||
pub use error::ComponentError;
|
||||
|
||||
mod definition;
|
||||
pub use definition::{Component, ComponentRender};
|
||||
|
||||
|
|
@ -10,8 +13,11 @@ pub use children::Children;
|
|||
pub use children::{Child, ChildOp};
|
||||
pub use children::{Typed, TypedOp};
|
||||
|
||||
mod message;
|
||||
pub use message::{MessageLevel, StatusMessage};
|
||||
|
||||
mod context;
|
||||
pub use context::{Context, ContextError, ContextOp, Contextual};
|
||||
pub use context::{AssetsOp, Context, ContextError, Contextual};
|
||||
|
||||
/// Alias de función (*callback*) para **determinar si un componente se renderiza o no**.
|
||||
///
|
||||
|
|
@ -38,8 +44,8 @@ pub use context::{Context, ContextError, ContextOp, Contextual};
|
|||
/// self.renderable.map_or(true, |f| f(cx))
|
||||
/// }
|
||||
///
|
||||
/// fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
|
||||
/// PrepareMarkup::Escaped("Visible component".into())
|
||||
/// fn prepare_component(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
/// Ok(html! { "Visible component" })
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use parking_lot::RwLock;
|
|||
pub use parking_lot::RwLockReadGuard as ComponentReadGuard;
|
||||
pub use parking_lot::RwLockWriteGuard as ComponentWriteGuard;
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::vec::IntoIter;
|
||||
|
||||
|
|
@ -17,6 +18,15 @@ use std::vec::IntoIter;
|
|||
#[derive(AutoDefault, Clone)]
|
||||
pub struct Child(Option<Arc<RwLock<dyn Component>>>);
|
||||
|
||||
impl fmt::Debug for Child {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.0 {
|
||||
None => write!(f, "Child(None)"),
|
||||
Some(c) => write!(f, "Child({})", c.read().name()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Child {
|
||||
/// Crea un nuevo `Child` a partir de un componente.
|
||||
pub fn with(component: impl Component) -> Self {
|
||||
|
|
@ -71,6 +81,15 @@ impl Child {
|
|||
#[derive(AutoDefault, Clone)]
|
||||
pub struct Typed<C: Component>(Option<Arc<RwLock<C>>>);
|
||||
|
||||
impl<C: Component> fmt::Debug for Typed<C> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.0 {
|
||||
None => write!(f, "Typed(None)"),
|
||||
Some(c) => write!(f, "Typed({})", c.read().name()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> Typed<C> {
|
||||
/// Crea un nuevo `Typed` a partir de un componente.
|
||||
pub fn with(component: C) -> Self {
|
||||
|
|
@ -202,7 +221,7 @@ pub enum TypedOp<C: Component> {
|
|||
/// Esta lista permite añadir, modificar, renderizar y consultar componentes hijo en orden de
|
||||
/// inserción, soportando operaciones avanzadas como inserción relativa o reemplazo por
|
||||
/// identificador.
|
||||
#[derive(AutoDefault, Clone)]
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub struct Children(Vec<Child>);
|
||||
|
||||
impl Children {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
use crate::core::component::ChildOp;
|
||||
use crate::core::component::{ChildOp, MessageLevel, StatusMessage};
|
||||
use crate::core::theme::all::DEFAULT_THEME;
|
||||
use crate::core::theme::{ChildrenInRegions, RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::core::TypeInfo;
|
||||
use crate::html::{html, Markup, RoutePath};
|
||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||
use crate::locale::L10n;
|
||||
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||
use crate::service::HttpRequest;
|
||||
use crate::{builder_fn, util, CowStr};
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
||||
/// Operaciones para modificar recursos asociados al [`Context`] de un documento.
|
||||
pub enum ContextOp {
|
||||
pub enum AssetsOp {
|
||||
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
|
||||
SetFavicon(Option<Favicon>),
|
||||
/// Define el *favicon* solo si no se ha establecido previamente.
|
||||
|
|
@ -45,6 +47,26 @@ pub enum ContextError {
|
|||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for ContextError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ContextError::ParamNotFound => {
|
||||
write!(f, "parameter not found")
|
||||
}
|
||||
ContextError::ParamTypeMismatch {
|
||||
key,
|
||||
expected,
|
||||
saved,
|
||||
} => write!(
|
||||
f,
|
||||
"type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\""
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ContextError {}
|
||||
|
||||
/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
|
||||
///
|
||||
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
|
||||
|
|
@ -52,7 +74,7 @@ pub enum ContextError {
|
|||
/// - Almacenar la **petición HTTP** de origen.
|
||||
/// - Seleccionar el **tema** y la **plantilla** de renderizado.
|
||||
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
|
||||
/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`ContextOp`].
|
||||
/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`].
|
||||
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
|
||||
/// - Generar **identificadores únicos** por tipo de componente.
|
||||
///
|
||||
|
|
@ -68,9 +90,9 @@ pub enum ContextError {
|
|||
/// cx.with_langid(&Locale::resolve("es-ES"))
|
||||
/// .with_theme(&Aliner)
|
||||
/// .with_template(&DefaultTemplate::Standard)
|
||||
/// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||
/// .with_assets(ContextOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
||||
/// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
||||
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
||||
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
||||
/// .with_param("usuario_id", 42_i32)
|
||||
/// }
|
||||
/// ```
|
||||
|
|
@ -94,12 +116,25 @@ pub trait Contextual: LangId {
|
|||
fn with_template(self, template: TemplateRef) -> Self;
|
||||
|
||||
/// Añade o modifica un parámetro dinámico del contexto.
|
||||
///
|
||||
/// El valor se guardará conservando el *nombre del tipo* real para mejorar los mensajes de
|
||||
/// error posteriores.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let cx = Context::new(None)
|
||||
/// .with_param("usuario_id", 42_i32)
|
||||
/// .with_param("titulo", "Hola".to_string())
|
||||
/// .with_param("flags", vec!["a", "b"]);
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
fn with_param<T: 'static>(self, key: &'static str, value: T) -> Self;
|
||||
|
||||
/// Define los recursos del contexto usando [`ContextOp`].
|
||||
/// Define los recursos del contexto usando [`AssetsOp`].
|
||||
#[builder_fn]
|
||||
fn with_assets(self, op: ContextOp) -> Self;
|
||||
fn with_assets(self, op: AssetsOp) -> Self;
|
||||
|
||||
/// Opera con [`ChildOp`] en una región del documento.
|
||||
#[builder_fn]
|
||||
|
|
@ -116,7 +151,34 @@ pub trait Contextual: LangId {
|
|||
/// Devuelve la plantilla configurada para renderizar el documento.
|
||||
fn template(&self) -> TemplateRef;
|
||||
|
||||
/// Recupera un parámetro como [`Option`].
|
||||
/// Recupera un parámetro como [`Option`], simplificando el acceso.
|
||||
///
|
||||
/// A diferencia de [`get_param`](Context::get_param), que devuelve un [`Result`] con
|
||||
/// información detallada de error, este método devuelve `None` tanto si la clave no existe como
|
||||
/// si el valor guardado no coincide con el tipo solicitado.
|
||||
///
|
||||
/// Resulta útil en escenarios donde sólo interesa saber si el valor existe y es del tipo
|
||||
/// correcto, sin necesidad de diferenciar entre error de ausencia o de tipo.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let cx = Context::new(None).with_param("username", "Alice".to_string());
|
||||
///
|
||||
/// // Devuelve Some(&String) si existe y coincide el tipo.
|
||||
/// assert_eq!(cx.param::<String>("username").map(|s| s.as_str()), Some("Alice"));
|
||||
///
|
||||
/// // Devuelve None si no existe o si el tipo no coincide.
|
||||
/// assert!(cx.param::<i32>("username").is_none());
|
||||
/// assert!(cx.param::<String>("missing").is_none());
|
||||
///
|
||||
/// // Acceso con valor por defecto.
|
||||
/// let user = cx.param::<String>("missing")
|
||||
/// .cloned()
|
||||
/// .unwrap_or_else(|| "visitor".to_string());
|
||||
/// assert_eq!(user, "visitor");
|
||||
/// ```
|
||||
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>;
|
||||
|
||||
/// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
|
||||
|
|
@ -145,11 +207,27 @@ pub trait Contextual: LangId {
|
|||
|
||||
// **< Contextual HELPERS >*********************************************************************
|
||||
|
||||
/// Genera un identificador único por tipo (`<tipo>-<n>`) cuando no se aporta uno explícito.
|
||||
/// Devuelve el `id` proporcionado tal cual, o genera uno único para el tipo `T` si no se
|
||||
/// proporciona ninguno.
|
||||
///
|
||||
/// Es útil para componentes u otros elementos HTML que necesitan un identificador predecible si
|
||||
/// no se proporciona ninguno.
|
||||
/// Si `id` es `None`, construye un identificador en la forma `<tipo>-<n>`, donde `<tipo>` es el
|
||||
/// nombre corto del tipo en minúsculas y `<n>` un contador incremental interno del contexto. Es
|
||||
/// útil para asignar identificadores HTML predecibles cuando el componente no recibe uno
|
||||
/// explícito.
|
||||
fn required_id<T>(&mut self, id: Option<String>) -> String;
|
||||
|
||||
/// Acumula un [`StatusMessage`] en el contexto para notificar al visitante.
|
||||
///
|
||||
/// Pueden generarse en cualquier punto del ciclo de una petición web (manejadores, renderizado,
|
||||
/// lógica de negocio, etc.) que tengan acceso al contexto, y mostrarlos luego, por ejemplo, en
|
||||
/// la página final devuelta al usuario.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// cx.push_message(MessageLevel::Warning, L10n::l("session-not-valid"));
|
||||
/// ```
|
||||
fn push_message(&mut self, level: MessageLevel, text: L10n);
|
||||
}
|
||||
|
||||
/// Implementa un **contexto de renderizado** para un documento HTML.
|
||||
|
|
@ -173,11 +251,11 @@ pub trait Contextual: LangId {
|
|||
/// // Establece el tema para renderizar.
|
||||
/// .with_theme(&Aliner)
|
||||
/// // Asigna un favicon.
|
||||
/// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||
/// // Añade una hoja de estilo externa.
|
||||
/// .with_assets(ContextOp::AddStyleSheet(StyleSheet::from("/css/style.css")))
|
||||
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/style.css")))
|
||||
/// // Añade un script JavaScript.
|
||||
/// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/main.js")))
|
||||
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js")))
|
||||
/// // Añade un parámetro dinámico al contexto.
|
||||
/// .with_param("usuario_id", 42)
|
||||
/// }
|
||||
|
|
@ -214,6 +292,7 @@ pub struct Context {
|
|||
regions : ChildrenInRegions, // Regiones de componentes para renderizar.
|
||||
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
|
||||
id_counter : usize, // Contador para generar identificadores únicos.
|
||||
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
|
||||
}
|
||||
|
||||
impl Default for Context {
|
||||
|
|
@ -241,6 +320,7 @@ impl Context {
|
|||
regions : ChildrenInRegions::default(),
|
||||
params : HashMap::default(),
|
||||
id_counter : 0,
|
||||
messages : Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -382,6 +462,16 @@ impl Context {
|
|||
}
|
||||
route
|
||||
}
|
||||
|
||||
/// Devuelve todos los mensajes de usuario acumulados.
|
||||
pub fn messages(&self) -> &[StatusMessage] {
|
||||
&self.messages
|
||||
}
|
||||
|
||||
/// Indica si hay mensajes de usuario acumulados.
|
||||
pub fn has_messages(&self) -> bool {
|
||||
!self.messages.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Permite a [`Context`](crate::core::component::Context) actuar como proveedor de idioma.
|
||||
|
|
@ -428,20 +518,6 @@ impl Contextual for Context {
|
|||
self
|
||||
}
|
||||
|
||||
/// Añade o modifica un parámetro dinámico del contexto.
|
||||
///
|
||||
/// El valor se guarda conservando el *nombre del tipo* real para mejorar los mensajes de error
|
||||
/// posteriores.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let cx = Context::new(None)
|
||||
/// .with_param("usuario_id", 42_i32)
|
||||
/// .with_param("titulo", "Hola".to_string())
|
||||
/// .with_param("flags", vec!["a", "b"]);
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
|
||||
let type_name = TypeInfo::FullName.of::<T>();
|
||||
|
|
@ -450,29 +526,29 @@ impl Contextual for Context {
|
|||
}
|
||||
|
||||
#[builder_fn]
|
||||
fn with_assets(mut self, op: ContextOp) -> Self {
|
||||
fn with_assets(mut self, op: AssetsOp) -> Self {
|
||||
match op {
|
||||
// Favicon.
|
||||
ContextOp::SetFavicon(favicon) => {
|
||||
AssetsOp::SetFavicon(favicon) => {
|
||||
self.favicon = favicon;
|
||||
}
|
||||
ContextOp::SetFaviconIfNone(icon) => {
|
||||
AssetsOp::SetFaviconIfNone(icon) => {
|
||||
if self.favicon.is_none() {
|
||||
self.favicon = Some(icon);
|
||||
}
|
||||
}
|
||||
// Stylesheets.
|
||||
ContextOp::AddStyleSheet(css) => {
|
||||
AssetsOp::AddStyleSheet(css) => {
|
||||
self.stylesheets.add(css);
|
||||
}
|
||||
ContextOp::RemoveStyleSheet(path) => {
|
||||
AssetsOp::RemoveStyleSheet(path) => {
|
||||
self.stylesheets.remove(path);
|
||||
}
|
||||
// Scripts JavaScript.
|
||||
ContextOp::AddJavaScript(js) => {
|
||||
AssetsOp::AddJavaScript(js) => {
|
||||
self.javascripts.add(js);
|
||||
}
|
||||
ContextOp::RemoveJavaScript(path) => {
|
||||
AssetsOp::RemoveJavaScript(path) => {
|
||||
self.javascripts.remove(path);
|
||||
}
|
||||
}
|
||||
|
|
@ -499,34 +575,6 @@ impl Contextual for Context {
|
|||
self.template
|
||||
}
|
||||
|
||||
/// Recupera un parámetro como [`Option`], simplificando el acceso.
|
||||
///
|
||||
/// A diferencia de [`get_param`](Self::get_param), que devuelve un [`Result`] con información
|
||||
/// detallada de error, este método devuelve `None` tanto si la clave no existe como si el valor
|
||||
/// guardado no coincide con el tipo solicitado.
|
||||
///
|
||||
/// Resulta útil en escenarios donde sólo interesa saber si el valor existe y es del tipo
|
||||
/// correcto, sin necesidad de diferenciar entre error de ausencia o de tipo.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let cx = Context::new(None).with_param("username", "Alice".to_string());
|
||||
///
|
||||
/// // Devuelve Some(&String) si existe y coincide el tipo.
|
||||
/// assert_eq!(cx.param::<String>("username").map(|s| s.as_str()), Some("Alice"));
|
||||
///
|
||||
/// // Devuelve None si no existe o si el tipo no coincide.
|
||||
/// assert!(cx.param::<i32>("username").is_none());
|
||||
/// assert!(cx.param::<String>("missing").is_none());
|
||||
///
|
||||
/// // Acceso con valor por defecto.
|
||||
/// let user = cx.param::<String>("missing")
|
||||
/// .cloned()
|
||||
/// .unwrap_or_else(|| "visitor".to_string());
|
||||
/// assert_eq!(user, "visitor");
|
||||
/// ```
|
||||
fn param<T: 'static>(&self, key: &'static str) -> Option<&T> {
|
||||
self.get_param::<T>(key).ok()
|
||||
}
|
||||
|
|
@ -545,12 +593,6 @@ impl Contextual for Context {
|
|||
|
||||
// **< Contextual HELPERS >*********************************************************************
|
||||
|
||||
/// Devuelve un identificador único dentro del contexto para el tipo `T`, si no se proporciona
|
||||
/// un `id` explícito.
|
||||
///
|
||||
/// Si no se proporciona un `id`, se genera un identificador único en la forma `<tipo>-<número>`
|
||||
/// donde `<tipo>` es el nombre corto del tipo en minúsculas (sin espacios) y `<número>` es un
|
||||
/// contador interno incremental.
|
||||
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
||||
if let Some(id) = id {
|
||||
id
|
||||
|
|
@ -569,4 +611,8 @@ impl Contextual for Context {
|
|||
util::join!(prefix, "-", self.id_counter.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn push_message(&mut self, level: MessageLevel, text: L10n) {
|
||||
self.messages.push(StatusMessage::new(level, text));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::base::action;
|
||||
use crate::core::component::Context;
|
||||
use crate::core::component::{ComponentError, Context, Contextual};
|
||||
use crate::core::{AnyInfo, TypeInfo};
|
||||
use crate::html::{html, Markup, PrepareMarkup};
|
||||
use crate::html::{html, Markup};
|
||||
|
||||
/// Define la función de renderizado para todos los componentes.
|
||||
///
|
||||
|
|
@ -14,11 +14,15 @@ pub trait ComponentRender {
|
|||
|
||||
/// Interfaz común que debe implementar un componente renderizable en PageTop.
|
||||
///
|
||||
/// Se recomienda que los componentes deriven [`AutoDefault`](crate::AutoDefault). También deben
|
||||
/// implementar explícitamente el método [`new()`](Self::new) y pueden sobrescribir los otros
|
||||
/// métodos para personalizar su comportamiento.
|
||||
/// Se recomienda que los componentes declaren sus campos como privados, que deriven
|
||||
/// [`AutoDefault`](crate::AutoDefault) o implementen [`Default`] para inicializarlos por defecto, y
|
||||
/// [`Getters`](crate::Getters) para acceder a sus datos. Deberán implementar explícitamente el
|
||||
/// método [`new()`](Self::new) y podrán sobrescribir los demás métodos para personalizar su
|
||||
/// comportamiento.
|
||||
pub trait Component: AnyInfo + ComponentRender + Send + Sync {
|
||||
/// Crea una nueva instancia del componente.
|
||||
///
|
||||
/// Por convención suele devolver `Self::default()`.
|
||||
fn new() -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
|
|
@ -51,9 +55,9 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
|
|||
/// puede sobrescribirse para decidir dinámicamente si los componentes de este tipo se
|
||||
/// renderizan o no en función del contexto de renderizado.
|
||||
///
|
||||
/// También puede usarse junto con un alias de función como
|
||||
/// ([`FnIsRenderable`](crate::core::component::FnIsRenderable)) para permitir que instancias
|
||||
/// concretas del componente decidan si se renderizan o no.
|
||||
/// También puede asignarse una función [`FnIsRenderable`](super::FnIsRenderable) a un campo del
|
||||
/// componente para permitir que instancias concretas del mismo puedan decidir dinámicamente si
|
||||
/// se renderizan o no.
|
||||
#[allow(unused_variables)]
|
||||
fn is_renderable(&self, cx: &mut Context) -> bool {
|
||||
true
|
||||
|
|
@ -62,25 +66,28 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
|
|||
/// Configura el componente justo antes de preparar el renderizado.
|
||||
///
|
||||
/// Este método puede sobrescribirse para modificar la estructura interna del componente o el
|
||||
/// contexto antes de preparar la renderización del componente. Por defecto no hace nada.
|
||||
/// contexto antes de renderizarlo. Por defecto no hace nada.
|
||||
#[allow(unused_variables)]
|
||||
fn setup_before_prepare(&mut self, cx: &mut Context) {}
|
||||
|
||||
/// Devuelve una representación renderizada del componente.
|
||||
/// Devuelve el marcado HTML del componente usando el contexto proporcionado.
|
||||
///
|
||||
/// 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. Puede sobrescribirse para generar
|
||||
/// dinámicamente el contenido HTML con acceso al contexto de renderizado.
|
||||
/// durante el proceso de construcción del documento. Cada componente lo implementa para generar
|
||||
/// su propio contenido HTML. Los temas hijo pueden sobrescribir opcionalmente su resultado
|
||||
/// mediante la acción [`PrepareRender`](crate::base::action::theme::PrepareRender).
|
||||
///
|
||||
/// Este método debe ser capaz de preparar el renderizado del componente con los métodos del
|
||||
/// propio componente y el contexto proporcionado, no debería hacerlo accediendo directamente a
|
||||
/// los campos de la estructura del componente. Es una forma de garantizar que los programadores
|
||||
/// podrán sobrescribir este método sin preocuparse por los detalles internos del componente.
|
||||
/// Se recomienda obtener los datos del componente a través de sus propios métodos para que los
|
||||
/// temas hijo que implementen dicha acción puedan generar el nuevo HTML sin depender de los
|
||||
/// detalles internos del componente.
|
||||
///
|
||||
/// Por defecto, devuelve [`PrepareMarkup::None`].
|
||||
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`).
|
||||
///
|
||||
/// En caso de error, devuelve un [`ComponentError`] que puede incluir un marcado alternativo
|
||||
/// (*fallback*) para sustituir al componente fallido.
|
||||
#[allow(unused_variables)]
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
PrepareMarkup::None
|
||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(html! {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,11 +130,33 @@ impl<C: Component> ComponentRender for C {
|
|||
action::component::BeforeRender::dispatch(self, cx);
|
||||
|
||||
// Prepara el renderizado del componente.
|
||||
let prepare = action::theme::PrepareRender::dispatch(self, cx);
|
||||
let prepare = if prepare.is_empty() {
|
||||
self.prepare_component(cx)
|
||||
} else {
|
||||
prepare
|
||||
let prepare = match action::theme::PrepareRender::dispatch(self, cx) {
|
||||
Ok(markup) if !markup.is_empty() => markup,
|
||||
Ok(_) => match self.prepare_component(cx) {
|
||||
Ok(markup) => markup,
|
||||
Err(error) => {
|
||||
crate::trace::error!(
|
||||
path = cx.request().map(|r| r.path()).unwrap_or("<unknown>"),
|
||||
component = self.name(),
|
||||
id = self.id().as_deref().unwrap_or("<not set>"),
|
||||
source = "prepare_component",
|
||||
"render failed, using fallback: {}",
|
||||
error.message()
|
||||
);
|
||||
error.into_fallback()
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
crate::trace::error!(
|
||||
path = cx.request().map(|r| r.path()).unwrap_or("<unknown>"),
|
||||
component = self.name(),
|
||||
id = self.id().as_deref().unwrap_or("<not set>"),
|
||||
source = "PrepareRender",
|
||||
"render failed, using fallback: {}",
|
||||
error.message()
|
||||
);
|
||||
error.into_fallback()
|
||||
}
|
||||
};
|
||||
|
||||
// Acciones específicas del tema después de renderizar el componente.
|
||||
|
|
@ -137,6 +166,6 @@ impl<C: Component> ComponentRender for C {
|
|||
action::component::AfterRender::dispatch(self, cx);
|
||||
|
||||
// Devuelve el marcado final.
|
||||
prepare.render()
|
||||
prepare
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
src/core/component/error.rs
Normal file
66
src/core/component/error.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
use crate::html::{html, Markup};
|
||||
use crate::{AutoDefault, Getters};
|
||||
|
||||
/// Error producido durante el renderizado de un componente.
|
||||
///
|
||||
/// Se usa en [`Component::prepare_component()`](super::Component::prepare_component) para devolver
|
||||
/// un [`Err`]. Puede incluir un marcado HTML alternativo para renderizar el componente de manera
|
||||
/// diferente en caso de error.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # struct MyComponent;
|
||||
/// # impl Component for MyComponent {
|
||||
/// # fn new() -> Self { MyComponent }
|
||||
/// fn prepare_component(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
/// Err(ComponentError::new("Database connection failed")
|
||||
/// .with_fallback(html! { p class="error" { "Content temporarily unavailable." } }))
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Debug, Getters)]
|
||||
pub struct ComponentError {
|
||||
/// Mensaje descriptivo del error.
|
||||
message: String,
|
||||
/// Marcado HTML alternativo para mostrar si el componente falla.
|
||||
fallback: Markup,
|
||||
}
|
||||
|
||||
impl ComponentError {
|
||||
/// Crea un nuevo error para un componente con un marcado alternativo vacío.
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
ComponentError {
|
||||
message: message.into(),
|
||||
fallback: html! {},
|
||||
}
|
||||
}
|
||||
|
||||
// **< ComponentError BUILDER >*****************************************************************
|
||||
|
||||
/// Asigna el marcado HTML alternativo (*fallback*) que se mostrará si el componente falla.
|
||||
///
|
||||
/// Si no se proporciona, no se renderizará nada del componente.
|
||||
pub fn with_fallback(mut self, fallback: Markup) -> Self {
|
||||
self.fallback = fallback;
|
||||
self
|
||||
}
|
||||
|
||||
// **< ComponentError GETTERS >*****************************************************************
|
||||
|
||||
/// Consume el error y devuelve su marcado alternativo.
|
||||
///
|
||||
/// Se invoca internamente en [`ComponentRender`](crate::core::component::ComponentRender).
|
||||
pub(crate) fn into_fallback(self) -> Markup {
|
||||
self.fallback
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ComponentError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ComponentError {}
|
||||
49
src/core/component/message.rs
Normal file
49
src/core/component/message.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use crate::locale::L10n;
|
||||
use crate::{AutoDefault, Getters};
|
||||
|
||||
/// Nivel de severidad de un [`StatusMessage`].
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum MessageLevel {
|
||||
/// Mensaje informativo para el usuario.
|
||||
#[default]
|
||||
Info,
|
||||
/// Aviso o advertencia para el usuario.
|
||||
Warning,
|
||||
/// Error comunicado al usuario.
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Notificación amigable para el usuario generada al procesar una petición web.
|
||||
///
|
||||
/// Representa un mensaje con carácter informativo, una advertencia o un error. A diferencia de
|
||||
/// [`ComponentError`](super::ComponentError), no está ligado a un fallo interno de renderizado,
|
||||
/// puede generarse en cualquier punto del procesamiento de una petición web (manejadores,
|
||||
/// renderizado, lógica de negocio, etc.).
|
||||
///
|
||||
/// El texto se almacena como [`L10n`] para resolverse con el idioma del contexto en el momento de
|
||||
/// la visualización.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// // Mensaje informativo con clave traducible.
|
||||
/// let info = StatusMessage::new(MessageLevel::Info, L10n::l("saved-successfully"));
|
||||
///
|
||||
/// // Aviso con texto literal sin traducción.
|
||||
/// let warn = StatusMessage::new(MessageLevel::Warning, L10n::n("Formulario incompleto."));
|
||||
/// ```
|
||||
#[derive(Debug, Getters)]
|
||||
pub struct StatusMessage {
|
||||
/// Nivel de severidad del mensaje.
|
||||
level: MessageLevel,
|
||||
/// Texto del mensaje.
|
||||
text: L10n,
|
||||
}
|
||||
|
||||
impl StatusMessage {
|
||||
/// Crea un nuevo mensaje de usuario con el nivel y texto indicados.
|
||||
pub fn new(level: MessageLevel, text: L10n) -> Self {
|
||||
StatusMessage { level, text }
|
||||
}
|
||||
}
|
||||
80
src/html.rs
80
src/html.rs
|
|
@ -1,7 +1,5 @@
|
|||
//! HTML en código.
|
||||
|
||||
use crate::AutoDefault;
|
||||
|
||||
mod maud;
|
||||
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE};
|
||||
|
||||
|
|
@ -29,81 +27,3 @@ pub use classes::{Classes, ClassesOp};
|
|||
|
||||
mod unit;
|
||||
pub use unit::UnitValue;
|
||||
|
||||
// **< HTML PrepareMarkup >*************************************************************************
|
||||
|
||||
/// Prepara contenido HTML para su conversión a [`Markup`].
|
||||
///
|
||||
/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML sin escapar o
|
||||
/// fragmentos ya procesados) para renderizarlos de forma homogénea en plantillas, sin interferir
|
||||
/// con el uso estándar de [`Markup`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// // Texto normal, se escapa automáticamente para evitar inyección de HTML.
|
||||
/// let fragment = PrepareMarkup::Escaped("Hola <b>mundo</b>".to_string());
|
||||
/// assert_eq!(fragment.into_string(), "Hola <b>mundo</b>");
|
||||
///
|
||||
/// // HTML literal, se inserta directamente, sin escapado adicional.
|
||||
/// let raw_html = PrepareMarkup::Raw("<b>negrita</b>".to_string());
|
||||
/// assert_eq!(raw_html.into_string(), "<b>negrita</b>");
|
||||
///
|
||||
/// // Fragmento ya preparado con la macro `html!`.
|
||||
/// let prepared = PrepareMarkup::With(html! {
|
||||
/// h2 { "Título de ejemplo" }
|
||||
/// p { "Este es un párrafo con contenido dinámico." }
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// prepared.into_string(),
|
||||
/// "<h2>Título de ejemplo</h2><p>Este es un párrafo con contenido dinámico.</p>"
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone)]
|
||||
pub enum PrepareMarkup {
|
||||
/// No se genera contenido HTML (equivale a `html! {}`).
|
||||
#[default]
|
||||
None,
|
||||
/// Texto plano que se **escapará automáticamente** para que no sea interpretado como HTML.
|
||||
///
|
||||
/// Úsalo con textos que provengan de usuarios u otras fuentes externas para garantizar la
|
||||
/// seguridad contra inyección de código.
|
||||
Escaped(String),
|
||||
/// HTML literal que se inserta **sin escapado adicional**.
|
||||
///
|
||||
/// Úsalo únicamente para contenido generado de forma confiable o controlada, ya que cualquier
|
||||
/// etiqueta o script incluido será renderizado directamente en el documento.
|
||||
Raw(String),
|
||||
/// Fragmento HTML ya preparado como [`Markup`], listo para insertarse directamente.
|
||||
///
|
||||
/// Normalmente proviene de expresiones `html! { ... }`.
|
||||
With(Markup),
|
||||
}
|
||||
|
||||
impl PrepareMarkup {
|
||||
/// Devuelve `true` si el contenido está vacío y no generará HTML al renderizar.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
PrepareMarkup::None => true,
|
||||
PrepareMarkup::Escaped(text) => text.is_empty(),
|
||||
PrepareMarkup::Raw(string) => string.is_empty(),
|
||||
PrepareMarkup::With(markup) => markup.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convierte el contenido en una cadena HTML renderizada. Usar sólo para pruebas o depuración.
|
||||
pub fn into_string(&self) -> String {
|
||||
self.render().into_string()
|
||||
}
|
||||
|
||||
/// Integra el renderizado fácilmente en la macro [`html!`].
|
||||
pub(crate) fn render(&self) -> Markup {
|
||||
match self {
|
||||
PrepareMarkup::None => html! {},
|
||||
PrepareMarkup::Escaped(text) => html! { (text) },
|
||||
PrepareMarkup::Raw(string) => html! { (PreEscaped(string)) },
|
||||
PrepareMarkup::With(markup) => html! { (markup) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ use crate::AutoDefault;
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// fn render_logo(cx: &mut Context) -> PrepareMarkup {
|
||||
/// PrepareMarkup::With(html! {
|
||||
/// fn render_logo(cx: &mut Context) -> Markup {
|
||||
/// html! {
|
||||
/// div class="logo_color" {
|
||||
/// (PageTopSvg::Color.render(cx))
|
||||
/// }
|
||||
|
|
@ -23,7 +23,7 @@ use crate::AutoDefault;
|
|||
/// div class="line_red" {
|
||||
/// (PageTopSvg::LineRGB(255, 0, 0).render(cx))
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
/// };
|
||||
/// ```
|
||||
|
||||
|
|
|
|||
|
|
@ -213,12 +213,12 @@ impl FromStr for UnitValue {
|
|||
None => {
|
||||
let n: f32 = s
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid number `{s}`: {e}"))?;
|
||||
.map_err(|e| format!("invalid number `{s}`: {e}"))?;
|
||||
if n == 0.0 {
|
||||
Ok(UnitValue::Zero)
|
||||
} else {
|
||||
Err(
|
||||
"Missing unit (expected one of cm,in,mm,pc,pt,px,em,rem,vh,vw, or %)"
|
||||
"missing unit (expected one of cm,in,mm,pc,pt,px,em,rem,vh,vw, or %)"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
|
@ -230,11 +230,11 @@ impl FromStr for UnitValue {
|
|||
|
||||
let parse_abs = |n_s: &str| -> Result<isize, String> {
|
||||
n_s.parse::<isize>()
|
||||
.map_err(|e| format!("Invalid integer `{n_s}`: {e}"))
|
||||
.map_err(|e| format!("invalid integer `{n_s}`: {e}"))
|
||||
};
|
||||
let parse_rel = |n_s: &str| -> Result<f32, String> {
|
||||
n_s.parse::<f32>()
|
||||
.map_err(|e| format!("Invalid float `{n_s}`: {e}"))
|
||||
.map_err(|e| format!("invalid float `{n_s}`: {e}"))
|
||||
};
|
||||
|
||||
match u.to_ascii_lowercase().as_str() {
|
||||
|
|
@ -253,7 +253,7 @@ impl FromStr for UnitValue {
|
|||
// Porcentaje como unidad.
|
||||
"%" => Ok(UnitValue::RelPct(parse_rel(n)?)),
|
||||
// Unidad desconocida.
|
||||
_ => Err(format!("Unknown unit: `{u}`")),
|
||||
_ => Err(format!("unknown unit: `{u}`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,13 +117,13 @@ use std::ops::Deref;
|
|||
/// impl Theme for MyTheme {
|
||||
/// fn before_render_page_body(&self, page: &mut Page) {
|
||||
/// page
|
||||
/// .alter_assets(ContextOp::AddStyleSheet(
|
||||
/// .alter_assets(AssetsOp::AddStyleSheet(
|
||||
/// StyleSheet::from("/css/normalize.css").with_version("8.0.1"),
|
||||
/// ))
|
||||
/// .alter_assets(ContextOp::AddStyleSheet(
|
||||
/// .alter_assets(AssetsOp::AddStyleSheet(
|
||||
/// StyleSheet::from("/css/basic.css").with_version(PAGETOP_VERSION),
|
||||
/// ))
|
||||
/// .alter_assets(ContextOp::AddStyleSheet(
|
||||
/// .alter_assets(AssetsOp::AddStyleSheet(
|
||||
/// StyleSheet::from("/mytheme/styles.css").with_version(env!("CARGO_PKG_VERSION")),
|
||||
/// ));
|
||||
/// }
|
||||
|
|
|
|||
|
|
@ -62,6 +62,17 @@ pub struct L10n {
|
|||
args: Vec<(CowStr, CowStr)>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for L10n {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("L10n")
|
||||
.field("op", &self.op)
|
||||
.field("args", &self.args)
|
||||
// No se puede mostrar `locales`; se representa con un texto fijo.
|
||||
.field("locales", &"<StaticLoader>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl L10n {
|
||||
/// **n** = *“native”*. Crea una instancia con una cadena literal sin traducción.
|
||||
pub fn n(text: impl Into<CowStr>) -> Self {
|
||||
|
|
@ -177,14 +188,3 @@ impl L10n {
|
|||
PreEscaped(self.lookup(language).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for L10n {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("L10n")
|
||||
.field("op", &self.op)
|
||||
.field("args", &self.args)
|
||||
// No se puede mostrar `locales`; se representa con un texto fijo.
|
||||
.field("locales", &"<StaticLoader>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ pub use error::ErrorPage;
|
|||
pub use actix_web::Result as ResultPage;
|
||||
|
||||
use crate::base::action;
|
||||
use crate::core::component::{AssetsOp, Context, Contextual};
|
||||
use crate::core::component::{Child, ChildOp, Component};
|
||||
use crate::core::component::{Context, ContextOp, Contextual};
|
||||
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::html::{html, Markup, DOCTYPE};
|
||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||
|
|
@ -334,7 +334,7 @@ impl Contextual for Page {
|
|||
}
|
||||
|
||||
#[builder_fn]
|
||||
fn with_assets(mut self, op: ContextOp) -> Self {
|
||||
fn with_assets(mut self, op: AssetsOp) -> Self {
|
||||
self.context.alter_assets(op);
|
||||
self
|
||||
}
|
||||
|
|
@ -380,4 +380,8 @@ impl Contextual for Page {
|
|||
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
||||
self.context.required_id::<T>(id)
|
||||
}
|
||||
|
||||
fn push_message(&mut self, level: crate::prelude::MessageLevel, text: L10n) {
|
||||
self.context.push_message(level, text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ pub fn resolve_absolute_dir<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
|
|||
Ok(absolute_dir)
|
||||
} else {
|
||||
Err({
|
||||
let msg = format!("Path \"{}\" is not a directory", absolute_dir.display());
|
||||
let msg = format!("path \"{}\" is not a directory", absolute_dir.display());
|
||||
trace::warn!(msg);
|
||||
io::Error::new(io::ErrorKind::InvalidInput, msg)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ async fn poweredby_getter_reflects_internal_state() {
|
|||
|
||||
// Y `new()` lo inicializa con año + nombre de app.
|
||||
let p1 = PoweredBy::new();
|
||||
let c1 = p1.copyright().expect("Expected copyright to exis");
|
||||
let c1 = p1.copyright().expect("Expected copyright to exist");
|
||||
assert!(c1.contains(&Utc::now().format("%Y").to_string()));
|
||||
assert!(c1.contains(&global::SETTINGS.app.name));
|
||||
}
|
||||
|
|
|
|||
107
tests/html_markup.rs
Normal file
107
tests/html_markup.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Componente mínimo para probar `Markup` pasando por el ciclo real de renderizado de componentes
|
||||
/// (`ComponentRender`). El parámetro de contexto `"renderable"` se usará para controlar si el
|
||||
/// componente se renderiza (`true` por defecto).
|
||||
#[derive(AutoDefault)]
|
||||
struct TestMarkupComponent {
|
||||
markup: Markup,
|
||||
}
|
||||
|
||||
impl Component for TestMarkupComponent {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn is_renderable(&self, cx: &mut Context) -> bool {
|
||||
cx.param_or::<bool>("renderable", true)
|
||||
}
|
||||
|
||||
fn prepare_component(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(self.markup.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// **< Comportamiento de Markup >*******************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn string_in_html_macro_escapes_html_entities() {
|
||||
let markup = html! { ("<b>& \" ' </b>") };
|
||||
assert_eq!(markup.into_string(), "<b>& " ' </b>");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn preescaped_in_html_macro_is_inserted_verbatim() {
|
||||
let markup = html! { (PreEscaped("<b>bold</b><script>1<2</script>")) };
|
||||
assert_eq!(markup.into_string(), "<b>bold</b><script>1<2</script>");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn unicode_is_preserved_in_markup() {
|
||||
// Texto con acentos y emojis: sólo se escapan los signos HTML.
|
||||
let esc = html! { ("Hello, tomorrow coffee ☕ & donuts!") };
|
||||
assert_eq!(esc.into_string(), "Hello, tomorrow coffee ☕ & donuts!");
|
||||
|
||||
// PreEscaped debe pasar íntegro.
|
||||
let raw = html! { (PreEscaped("Title — section © 2025")) };
|
||||
assert_eq!(raw.into_string(), "Title — section © 2025");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn markup_is_empty_semantics() {
|
||||
assert!(html! {}.is_empty());
|
||||
|
||||
assert!(html! { ("") }.is_empty());
|
||||
assert!(!html! { ("x") }.is_empty());
|
||||
|
||||
assert!(html! { (PreEscaped(String::new())) }.is_empty());
|
||||
assert!(!html! { (PreEscaped("a")) }.is_empty());
|
||||
|
||||
assert!(html! { (String::new()) }.is_empty());
|
||||
|
||||
assert!(!html! { span { "!" } }.is_empty());
|
||||
|
||||
// Espacios NO se consideran vacíos.
|
||||
assert!(!html! { (" ") }.is_empty());
|
||||
assert!(!html! { (PreEscaped(" ")) }.is_empty());
|
||||
}
|
||||
|
||||
// **< Markup a través del ciclo de componente >****************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn non_renderable_component_produces_empty_markup() {
|
||||
let mut cx = Context::default().with_param("renderable", false);
|
||||
let mut comp = TestMarkupComponent {
|
||||
markup: html! { p { "Should never be rendered" } },
|
||||
};
|
||||
assert_eq!(comp.render(&mut cx).into_string(), "");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn markup_from_component_equals_markup_reinjected_in_html_macro() {
|
||||
let cases = [
|
||||
html! {},
|
||||
html! { ("<b>x</b>") },
|
||||
html! { (PreEscaped("<b>x</b>")) },
|
||||
html! { b { "x" } },
|
||||
];
|
||||
|
||||
for markup in cases {
|
||||
// Vía 1: renderizamos a través del ciclo de componente.
|
||||
let via_component = {
|
||||
let mut cx = Context::default();
|
||||
let mut comp = TestMarkupComponent {
|
||||
markup: markup.clone(),
|
||||
};
|
||||
comp.render(&mut cx).into_string()
|
||||
};
|
||||
|
||||
// Vía 2: reinyectamos el Markup en `html!` directamente.
|
||||
let via_macro = html! { (markup) }.into_string();
|
||||
|
||||
assert_eq!(
|
||||
via_component, via_macro,
|
||||
"The output of component render and (Markup) inside html! must match"
|
||||
);
|
||||
}
|
||||
}
|
||||
144
tests/html_pm.rs
144
tests/html_pm.rs
|
|
@ -1,144 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Componente mínimo para probar `PrepareMarkup` pasando por el ciclo real
|
||||
/// de renderizado de componentes (`ComponentRender`).
|
||||
#[derive(AutoDefault)]
|
||||
struct TestPrepareComponent {
|
||||
pm: PrepareMarkup,
|
||||
}
|
||||
|
||||
impl Component for TestPrepareComponent {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
pm: PrepareMarkup::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
|
||||
self.pm.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl TestPrepareComponent {
|
||||
fn render_pm(pm: PrepareMarkup) -> String {
|
||||
let mut c = TestPrepareComponent { pm };
|
||||
c.render(&mut Context::default()).into_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn prepare_markup_none_is_empty_string() {
|
||||
assert_eq!(PrepareMarkup::None.into_string(), "");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn prepare_markup_escaped_escapes_html_and_ampersands() {
|
||||
let pm = PrepareMarkup::Escaped("<b>& \" ' </b>".to_string());
|
||||
assert_eq!(pm.into_string(), "<b>& " ' </b>");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn prepare_markup_raw_is_inserted_verbatim() {
|
||||
let pm = PrepareMarkup::Raw("<b>bold</b><script>1<2</script>".to_string());
|
||||
assert_eq!(pm.into_string(), "<b>bold</b><script>1<2</script>");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn prepare_markup_with_keeps_structure() {
|
||||
let pm = PrepareMarkup::With(html! {
|
||||
h2 { "Sample title" }
|
||||
p { "This is a paragraph." }
|
||||
});
|
||||
assert_eq!(
|
||||
pm.into_string(),
|
||||
"<h2>Sample title</h2><p>This is a paragraph.</p>"
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn prepare_markup_unicode_is_preserved() {
|
||||
// Texto con acentos y emojis debe conservarse (salvo el escape HTML de signos).
|
||||
let esc = PrepareMarkup::Escaped("Hello, tomorrow coffee ☕ & donuts!".into());
|
||||
assert_eq!(esc.into_string(), "Hello, tomorrow coffee ☕ & donuts!");
|
||||
|
||||
// Raw debe pasar íntegro.
|
||||
let raw = PrepareMarkup::Raw("Title — section © 2025".into());
|
||||
assert_eq!(raw.into_string(), "Title — section © 2025");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn prepare_markup_is_empty_semantics() {
|
||||
assert!(PrepareMarkup::None.is_empty());
|
||||
|
||||
assert!(PrepareMarkup::Escaped(String::new()).is_empty());
|
||||
assert!(PrepareMarkup::Escaped("".to_string()).is_empty());
|
||||
assert!(!PrepareMarkup::Escaped("x".to_string()).is_empty());
|
||||
|
||||
assert!(PrepareMarkup::Raw(String::new()).is_empty());
|
||||
assert!(PrepareMarkup::Raw("".to_string()).is_empty());
|
||||
assert!(!PrepareMarkup::Raw("a".into()).is_empty());
|
||||
|
||||
assert!(PrepareMarkup::With(html! {}).is_empty());
|
||||
assert!(!PrepareMarkup::With(html! { span { "!" } }).is_empty());
|
||||
|
||||
// Ojo: espacios NO deberían considerarse vacíos (comportamiento actual).
|
||||
assert!(!PrepareMarkup::Escaped(" ".into()).is_empty());
|
||||
assert!(!PrepareMarkup::Raw(" ".into()).is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn prepare_markup_does_not_double_escape_when_markup_is_reinjected_in_html_macro() {
|
||||
let mut cx = Context::default();
|
||||
|
||||
// Escaped: dentro de `html!` no debe volver a escaparse.
|
||||
let mut comp = TestPrepareComponent {
|
||||
pm: PrepareMarkup::Escaped("<i>x</i>".into()),
|
||||
};
|
||||
let markup = comp.render(&mut cx); // Markup
|
||||
let wrapped_escaped = html! { div { (markup) } }.into_string();
|
||||
assert_eq!(wrapped_escaped, "<div><i>x</i></div>");
|
||||
|
||||
// Raw: tampoco debe escaparse al integrarlo.
|
||||
let mut comp = TestPrepareComponent {
|
||||
pm: PrepareMarkup::Raw("<i>x</i>".into()),
|
||||
};
|
||||
let markup = comp.render(&mut cx);
|
||||
let wrapped_raw = html! { div { (markup) } }.into_string();
|
||||
assert_eq!(wrapped_raw, "<div><i>x</i></div>");
|
||||
|
||||
// With: debe incrustar el Markup tal cual.
|
||||
let mut comp = TestPrepareComponent {
|
||||
pm: PrepareMarkup::With(html! { span.title { "ok" } }),
|
||||
};
|
||||
let markup = comp.render(&mut cx);
|
||||
let wrapped_with = html! { div { (markup) } }.into_string();
|
||||
assert_eq!(wrapped_with, "<div><span class=\"title\">ok</span></div>");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn prepare_markup_equivalence_between_component_render_and_markup_reinjected_in_html_macro() {
|
||||
let cases = [
|
||||
PrepareMarkup::None,
|
||||
PrepareMarkup::Escaped("<b>x</b>".into()),
|
||||
PrepareMarkup::Raw("<b>x</b>".into()),
|
||||
PrepareMarkup::With(html! { b { "x" } }),
|
||||
];
|
||||
|
||||
for pm in cases {
|
||||
// Vía 1: renderizamos y obtenemos directamente el String.
|
||||
let via_component = TestPrepareComponent::render_pm(pm.clone());
|
||||
|
||||
// Vía 2: renderizamos, reinyectamos el Markup en `html!` y volvemos a obtener String.
|
||||
let via_macro = {
|
||||
let mut cx = Context::default();
|
||||
let mut comp = TestPrepareComponent { pm };
|
||||
let markup = comp.render(&mut cx);
|
||||
html! { (markup) }.into_string()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
via_component, via_macro,
|
||||
"The output of component render and (Markup) inside html! must match"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue