♻️ [html] Cambia tipos Option... por Attr...

Renombra los tipos para atributos HTML `Id`, `Name`, `Value` (`String`),
`L10n` (`Translate`) y `Classes`. Y mueve `OptionComponent` al *core* de
componentes como `TypedSlot`.
This commit is contained in:
Manuel Cillero 2025-08-23 18:52:45 +02:00
parent 75eec8bebc
commit bb34ba5887
17 changed files with 367 additions and 311 deletions

View file

@ -6,7 +6,7 @@ use crate::base::action::FnActionWithComponent;
pub struct AfterRender<C: Component> { pub struct AfterRender<C: Component> {
f: FnActionWithComponent<C>, f: FnActionWithComponent<C>,
referer_type_id: Option<UniqueId>, referer_type_id: Option<UniqueId>,
referer_id: OptionId, referer_id: AttrId,
weight: Weight, weight: Weight,
} }
@ -34,7 +34,7 @@ impl<C: Component> AfterRender<C> {
AfterRender { AfterRender {
f, f,
referer_type_id: Some(UniqueId::of::<C>()), referer_type_id: Some(UniqueId::of::<C>()),
referer_id: OptionId::default(), referer_id: AttrId::default(),
weight: 0, weight: 0,
} }
} }

View file

@ -6,7 +6,7 @@ use crate::base::action::FnActionWithComponent;
pub struct BeforeRender<C: Component> { pub struct BeforeRender<C: Component> {
f: FnActionWithComponent<C>, f: FnActionWithComponent<C>,
referer_type_id: Option<UniqueId>, referer_type_id: Option<UniqueId>,
referer_id: OptionId, referer_id: AttrId,
weight: Weight, weight: Weight,
} }
@ -34,7 +34,7 @@ impl<C: Component> BeforeRender<C> {
BeforeRender { BeforeRender {
f, f,
referer_type_id: Some(UniqueId::of::<C>()), referer_type_id: Some(UniqueId::of::<C>()),
referer_id: OptionId::default(), referer_id: AttrId::default(),
weight: 0, weight: 0,
} }
} }

View file

@ -11,7 +11,7 @@ pub type FnIsRenderable<C> = fn(component: &C, cx: &Context) -> bool;
pub struct IsRenderable<C: Component> { pub struct IsRenderable<C: Component> {
f: FnIsRenderable<C>, f: FnIsRenderable<C>,
referer_type_id: Option<UniqueId>, referer_type_id: Option<UniqueId>,
referer_id: OptionId, referer_id: AttrId,
weight: Weight, weight: Weight,
} }
@ -39,7 +39,7 @@ impl<C: Component> IsRenderable<C> {
IsRenderable { IsRenderable {
f, f,
referer_type_id: Some(UniqueId::of::<C>()), referer_type_id: Some(UniqueId::of::<C>()),
referer_id: OptionId::default(), referer_id: AttrId::default(),
weight: 0, weight: 0,
} }
} }

View file

@ -7,3 +7,6 @@ mod children;
pub use children::Children; pub use children::Children;
pub use children::{Child, ChildOp}; pub use children::{Child, ChildOp};
pub use children::{Typed, TypedOp}; pub use children::{Typed, TypedOp};
mod slot;
pub use slot::TypedSlot;

View file

@ -9,13 +9,13 @@ use std::vec::IntoIter;
/// Representa un componente encapsulado de forma segura y compartida. /// Representa un componente encapsulado de forma segura y compartida.
/// ///
/// Esta estructura permite manipular y renderizar cualquier tipo que implemente [`Component`], /// Esta estructura permite manipular y renderizar un componente que implemente [`Component`], y
/// garantizando acceso concurrente a través de [`Arc<RwLock<_>>`]. /// habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
#[derive(Clone)] #[derive(Clone)]
pub struct Child(Arc<RwLock<dyn Component>>); pub struct Child(Arc<RwLock<dyn Component>>);
impl Child { impl Child {
/// Crea un nuevo [`Child`] a partir de un componente. /// Crea un nuevo `Child` a partir de un componente.
pub fn with(component: impl Component) -> Self { pub fn with(component: impl Component) -> Self {
Child(Arc::new(RwLock::new(component))) Child(Arc::new(RwLock::new(component)))
} }
@ -46,7 +46,8 @@ impl Child {
/// Variante tipada de [`Child`] para evitar conversiones durante el uso. /// Variante tipada de [`Child`] para evitar conversiones durante el uso.
/// ///
/// Facilita el acceso a componentes del mismo tipo sin necesidad de hacer `downcast`. /// Esta estructura permite manipular y renderizar un componente concreto que implemente
/// [`Component`], y habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
pub struct Typed<C: Component>(Arc<RwLock<C>>); pub struct Typed<C: Component>(Arc<RwLock<C>>);
impl<C: Component> Clone for Typed<C> { impl<C: Component> Clone for Typed<C> {
@ -56,7 +57,7 @@ impl<C: Component> Clone for Typed<C> {
} }
impl<C: Component> Typed<C> { impl<C: Component> Typed<C> {
/// Crea un nuevo [`Typed`] a partir de un componente. /// Crea un nuevo `Typed` a partir de un componente.
pub fn with(component: C) -> Self { pub fn with(component: C) -> Self {
Typed(Arc::new(RwLock::new(component))) Typed(Arc::new(RwLock::new(component)))
} }
@ -284,7 +285,7 @@ impl IntoIterator for Children {
/// ///
/// # Ejemplo de uso: /// # Ejemplo de uso:
/// ///
/// ```rust#ignore /// ```rust,ignore
/// let children = Children::new().with(child1).with(child2); /// let children = Children::new().with(child1).with(child2);
/// for child in children { /// for child in children {
/// println!("{:?}", child.id()); /// println!("{:?}", child.id());
@ -303,7 +304,7 @@ impl<'a> IntoIterator for &'a Children {
/// ///
/// # Ejemplo de uso: /// # Ejemplo de uso:
/// ///
/// ```rust#ignore /// ```rust,ignore
/// let children = Children::new().with(child1).with(child2); /// let children = Children::new().with(child1).with(child2);
/// for child in &children { /// for child in &children {
/// println!("{:?}", child.id()); /// println!("{:?}", child.id());
@ -322,7 +323,7 @@ impl<'a> IntoIterator for &'a mut Children {
/// ///
/// # Ejemplo de uso: /// # Ejemplo de uso:
/// ///
/// ```rust#ignore /// ```rust,ignore
/// let mut children = Children::new().with(child1).with(child2); /// let mut children = Children::new().with(child1).with(child2);
/// for child in &mut children { /// for child in &mut children {
/// child.render(&mut context); /// child.render(&mut context);

View file

@ -0,0 +1,64 @@
use crate::builder_fn;
use crate::core::component::{Component, Typed};
use crate::html::{html, Context, Markup};
/// Contenedor para un componente [`Typed`] opcional.
///
/// Un `TypedSlot` actúa como un contenedor dentro de otro componente para incluir o no un
/// subcomponente. Internamente encapsula `Option<Typed<C>>`, pero proporciona una API más sencilla
/// para construir estructuras jerárquicas.
///
/// # Ejemplo
///
/// ```rust,ignore
/// use pagetop::prelude::*;
///
/// let comp = MyComponent::new();
/// let opt = TypedSlot::new(comp);
/// assert!(opt.get().is_some());
/// ```
pub struct TypedSlot<C: Component>(Option<Typed<C>>);
impl<C: Component> Default for TypedSlot<C> {
fn default() -> Self {
TypedSlot(None)
}
}
impl<C: Component> TypedSlot<C> {
/// Crea un nuevo [`TypedSlot`].
///
/// El componente se envuelve automáticamente en un [`Typed`] y se almacena.
pub fn new(component: C) -> Self {
TypedSlot(Some(Typed::with(component)))
}
// TypedSlot BUILDER *********************************************************************
/// Establece un componente nuevo, o lo vacía.
///
/// Si se proporciona `Some(component)`, se guarda en [`Typed`]; y si es `None`, se limpia.
#[builder_fn]
pub fn with_value(mut self, component: Option<C>) -> Self {
self.0 = component.map(Typed::with);
self
}
// TypedSlot GETTERS *********************************************************************
/// Devuelve un clon (incrementa el contador `Arc`) de [`Typed<C>`], si existe.
pub fn get(&self) -> Option<Typed<C>> {
self.0.clone()
}
// TypedSlot RENDER ************************************************************************
/// Renderiza el componente, si existe.
pub fn render(&self, cx: &mut Context) -> Markup {
if let Some(component) = &self.0 {
component.render(cx)
} else {
html! {}
}
}
}

View file

@ -3,52 +3,82 @@
mod maud; mod maud;
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE}; pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE};
// HTML DOCUMENT ASSETS ****************************************************************************
mod assets; mod assets;
pub use assets::favicon::Favicon; pub use assets::favicon::Favicon;
pub use assets::javascript::JavaScript; pub use assets::javascript::JavaScript;
pub use assets::stylesheet::{StyleSheet, TargetMedia}; pub use assets::stylesheet::{StyleSheet, TargetMedia};
pub(crate) use assets::Assets; pub(crate) use assets::Assets;
// HTML DOCUMENT CONTEXT ***************************************************************************
mod context; mod context;
pub use context::{AssetsOp, Context, ErrorParam}; pub use context::{AssetsOp, Context, ErrorParam};
mod opt_id; // HTML ATTRIBUTES *********************************************************************************
pub use opt_id::OptionId;
mod opt_name; mod attr_id;
pub use opt_name::OptionName; pub use attr_id::AttrId;
/// **Obsoleto desde la versión 0.4.0**: usar [`AttrId`] en su lugar.
#[deprecated(since = "0.4.0", note = "Use `AttrId` instead")]
pub type OptionId = AttrId;
mod opt_string; mod attr_name;
pub use opt_string::OptionString; pub use attr_name::AttrName;
/// **Obsoleto desde la versión 0.4.0**: usar [`AttrName`] en su lugar.
#[deprecated(since = "0.4.0", note = "Use `AttrName` instead")]
pub type OptionName = AttrName;
mod opt_translated; mod attr_value;
pub use opt_translated::OptionTranslated; pub use attr_value::AttrValue;
/// **Obsoleto desde la versión 0.4.0**: usar [`AttrValue`] en su lugar.
#[deprecated(since = "0.4.0", note = "Use `AttrValue` instead")]
pub type OptionString = AttrValue;
mod opt_classes; mod attr_l10n;
pub use opt_classes::{ClassesOp, OptionClasses}; pub use attr_l10n::AttrL10n;
/// **Obsoleto desde la versión 0.4.0**: usar [`AttrL10n`] en su lugar.
#[deprecated(since = "0.4.0", note = "Use `AttrL10n` instead")]
pub type OptionTranslated = AttrL10n;
mod opt_component; mod attr_classes;
pub use opt_component::OptionComponent; pub use attr_classes::{AttrClasses, ClassesOp};
/// **Obsoleto desde la versión 0.4.0**: usar [`AttrClasses`] en su lugar.
#[deprecated(since = "0.4.0", note = "Use `AttrClasses` instead")]
pub type OptionClasses = AttrClasses;
use crate::AutoDefault; use crate::{core, AutoDefault};
/// **Obsoleto desde la versión 0.4.0**: usar [`TypedSlot`](crate::core::component::TypedSlot) en su
/// lugar.
#[deprecated(
since = "0.4.0",
note = "Use `pagetop::core::component::TypedSlot` instead"
)]
#[allow(type_alias_bounds)]
pub type OptionComponent<C: core::component::Component> = core::component::TypedSlot<C>;
/// Prepara contenido HTML para su conversión a [`Markup`]. /// Prepara contenido HTML para su conversión a [`Markup`].
/// ///
/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML escapado o marcado /// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML sin escapar o
/// ya procesado) para renderizar de forma homogénea en plantillas sin interferir con el uso /// fragmentos ya procesados) para renderizarlos de forma homogénea en plantillas, sin interferir
/// estándar de [`Markup`]. /// con el uso estándar de [`Markup`].
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust /// ```rust
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// let fragment = PrepareMarkup::Text(String::from("Hola <b>mundo</b>")); /// // Texto normal, se escapa automáticamente para evitar inyección de HTML.
/// let fragment = PrepareMarkup::Escaped(String::from("Hola <b>mundo</b>"));
/// assert_eq!(fragment.render().into_string(), "Hola &lt;b&gt;mundo&lt;/b&gt;"); /// assert_eq!(fragment.render().into_string(), "Hola &lt;b&gt;mundo&lt;/b&gt;");
/// ///
/// let raw_html = PrepareMarkup::Escaped(String::from("<b>negrita</b>")); /// // HTML literal, se inserta directamente, sin escapado adicional.
/// let raw_html = PrepareMarkup::Raw(String::from("<b>negrita</b>"));
/// assert_eq!(raw_html.render().into_string(), "<b>negrita</b>"); /// assert_eq!(raw_html.render().into_string(), "<b>negrita</b>");
/// ///
/// // Fragmento ya preparado con la macro `html!`.
/// let prepared = PrepareMarkup::With(html! { /// let prepared = PrepareMarkup::With(html! {
/// h2 { "Título de ejemplo" } /// h2 { "Título de ejemplo" }
/// p { "Este es un párrafo con contenido dinámico." } /// p { "Este es un párrafo con contenido dinámico." }
@ -60,14 +90,22 @@ use crate::AutoDefault;
/// ``` /// ```
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub enum PrepareMarkup { pub enum PrepareMarkup {
/// No se genera contenido HTML (devuelve `html! {}`). /// No se genera contenido HTML (equivale a `html! {}`).
#[default] #[default]
None, None,
/// Texto estático que se escapará automáticamente para no ser interpretado como HTML. /// Texto plano que se **escapará automáticamente** para que no sea interpretado como HTML.
Text(String), ///
/// Contenido sin escapado adicional, útil para HTML generado externamente. /// Úsalo con textos que provengan de usuarios u otras fuentes externas para garantizar la
/// seguridad contra inyección de código.
Escaped(String), 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. /// Fragmento HTML ya preparado como [`Markup`], listo para insertarse directamente.
///
/// Normalmente proviene de expresiones `html! { ... }`.
With(Markup), With(Markup),
} }
@ -76,8 +114,8 @@ impl PrepareMarkup {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
match self { match self {
PrepareMarkup::None => true, PrepareMarkup::None => true,
PrepareMarkup::Text(text) => text.is_empty(), PrepareMarkup::Escaped(text) => text.is_empty(),
PrepareMarkup::Escaped(string) => string.is_empty(), PrepareMarkup::Raw(string) => string.is_empty(),
PrepareMarkup::With(markup) => markup.is_empty(), PrepareMarkup::With(markup) => markup.is_empty(),
} }
} }
@ -88,8 +126,8 @@ impl Render for PrepareMarkup {
fn render(&self) -> Markup { fn render(&self) -> Markup {
match self { match self {
PrepareMarkup::None => html! {}, PrepareMarkup::None => html! {},
PrepareMarkup::Text(text) => html! { (text) }, PrepareMarkup::Escaped(text) => html! { (text) },
PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) }, PrepareMarkup::Raw(string) => html! { (PreEscaped(string)) },
PrepareMarkup::With(markup) => html! { (markup) }, PrepareMarkup::With(markup) => html! { (markup) },
} }
} }

View file

@ -1,6 +1,6 @@
use crate::{builder_fn, AutoDefault}; use crate::{builder_fn, AutoDefault};
/// Operaciones disponibles sobre la lista de clases en [`OptionClasses`]. /// Operaciones disponibles sobre la lista de clases en [`AttrClasses`].
pub enum ClassesOp { pub enum ClassesOp {
/// Añade al final (si no existe). /// Añade al final (si no existe).
Add, Add,
@ -33,7 +33,7 @@ pub enum ClassesOp {
/// ```rust /// ```rust
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// let classes = OptionClasses::new("Btn btn-primary") /// let classes = AttrClasses::new("Btn btn-primary")
/// .with_value(ClassesOp::Add, "Active") /// .with_value(ClassesOp::Add, "Active")
/// .with_value(ClassesOp::Remove, "btn-primary"); /// .with_value(ClassesOp::Remove, "btn-primary");
/// ///
@ -41,14 +41,14 @@ pub enum ClassesOp {
/// assert!(classes.contains("active")); /// assert!(classes.contains("active"));
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]
pub struct OptionClasses(Vec<String>); pub struct AttrClasses(Vec<String>);
impl OptionClasses { impl AttrClasses {
pub fn new(classes: impl AsRef<str>) -> Self { pub fn new(classes: impl AsRef<str>) -> Self {
OptionClasses::default().with_value(ClassesOp::Prepend, classes) AttrClasses::default().with_value(ClassesOp::Prepend, classes)
} }
// OptionClasses BUILDER *********************************************************************** // AttrClasses BUILDER *************************************************************************
#[builder_fn] #[builder_fn]
pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self { pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
@ -114,7 +114,7 @@ impl OptionClasses {
} }
} }
// OptionClasses GETTERS *********************************************************************** // AttrClasses GETTERS *************************************************************************
/// Devuele la cadena de clases, si existe. /// Devuele la cadena de clases, si existe.
pub fn get(&self) -> Option<String> { pub fn get(&self) -> Option<String> {

63
src/html/attr_id.rs Normal file
View file

@ -0,0 +1,63 @@
use crate::{builder_fn, AutoDefault};
/// Identificador normalizado para el atributo `id` o similar de HTML.
///
/// Este tipo encapsula `Option<String>` garantizando un valor normalizado para su uso:
///
/// - Se eliminan los espacios al principio y al final.
/// - Se convierte a minúsculas.
/// - Se sustituyen los espacios intermedios por guiones bajos (`_`).
/// - Si el resultado es una cadena vacía, se guarda `None`.
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let id = AttrId::new(" main Section ");
/// assert_eq!(id.as_str(), Some("main_section"));
///
/// let empty = AttrId::default();
/// assert_eq!(empty.get(), None);
/// ```
#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
pub struct AttrId(Option<String>);
impl AttrId {
/// Crea un nuevo `AttrId` normalizando el valor.
pub fn new(value: impl AsRef<str>) -> Self {
AttrId::default().with_value(value)
}
// AttrId BUILDER ******************************************************************************
/// Establece un identificador nuevo normalizando el valor.
#[builder_fn]
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
let value = value.as_ref().trim().to_ascii_lowercase().replace(' ', "_");
self.0 = if value.is_empty() { None } else { Some(value) };
self
}
// AttrId GETTERS ******************************************************************************
/// Devuelve el identificador normalizado, si existe.
pub fn get(&self) -> Option<String> {
self.0.as_ref().cloned()
}
/// Devuelve el identificador normalizado (sin clonar), si existe.
pub fn as_str(&self) -> Option<&str> {
self.0.as_deref()
}
/// Devuelve el identificador normalizado (propiedad), si existe.
pub fn into_inner(self) -> Option<String> {
self.0
}
/// `true` si no hay valor.
pub fn is_empty(&self) -> bool {
self.0.is_none()
}
}

View file

@ -2,7 +2,7 @@ use crate::html::Markup;
use crate::locale::{L10n, LangId}; use crate::locale::{L10n, LangId};
use crate::{builder_fn, AutoDefault}; use crate::{builder_fn, AutoDefault};
/// Cadena para traducir al renderizar ([`locale`](crate::locale)). /// Texto para [traducir](crate::locale) en atributos HTML.
/// ///
/// Encapsula un tipo [`L10n`] para manejar traducciones de forma segura. /// Encapsula un tipo [`L10n`] para manejar traducciones de forma segura.
/// ///
@ -12,7 +12,7 @@ use crate::{builder_fn, AutoDefault};
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// // Traducción por clave en las locales por defecto de PageTop. /// // Traducción por clave en las locales por defecto de PageTop.
/// let hello = OptionTranslated::new(L10n::l("test-hello-world")); /// let hello = AttrL10n::new(L10n::l("test-hello-world"));
/// ///
/// // Español disponible. /// // Español disponible.
/// assert_eq!( /// assert_eq!(
@ -31,15 +31,15 @@ use crate::{builder_fn, AutoDefault};
/// assert_eq!(markup.into_string(), "¡Hola mundo!"); /// assert_eq!(markup.into_string(), "¡Hola mundo!");
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]
pub struct OptionTranslated(L10n); pub struct AttrL10n(L10n);
impl OptionTranslated { impl AttrL10n {
/// Crea una nueva instancia [`OptionTranslated`]. /// Crea una nueva instancia `AttrL10n`.
pub fn new(value: L10n) -> Self { pub fn new(value: L10n) -> Self {
OptionTranslated(value) AttrL10n(value)
} }
// OptionTranslated BUILDER ******************************************************************** // AttrL10n BUILDER ****************************************************************************
/// Establece una traducción nueva. /// Establece una traducción nueva.
#[builder_fn] #[builder_fn]
@ -48,7 +48,7 @@ impl OptionTranslated {
self self
} }
// OptionTranslated GETTERS ******************************************************************** // AttrL10n GETTERS ****************************************************************************
/// Devuelve la traducción para `language`, si existe. /// Devuelve la traducción para `language`, si existe.
pub fn using(&self, language: &impl LangId) -> Option<String> { pub fn using(&self, language: &impl LangId) -> Option<String> {

63
src/html/attr_name.rs Normal file
View file

@ -0,0 +1,63 @@
use crate::{builder_fn, AutoDefault};
/// Nombre normalizado para el atributo `name` o similar de HTML.
///
/// Este tipo encapsula `Option<String>` garantizando un valor normalizado para su uso:
///
/// - Se eliminan los espacios al principio y al final.
/// - Se convierte a minúsculas.
/// - Se sustituyen los espacios intermedios por guiones bajos (`_`).
/// - Si el resultado es una cadena vacía, se guarda `None`.
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let name = AttrName::new(" DISplay name ");
/// assert_eq!(name.as_str(), Some("display_name"));
///
/// let empty = AttrName::default();
/// assert_eq!(empty.get(), None);
/// ```
#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
pub struct AttrName(Option<String>);
impl AttrName {
/// Crea un nuevo `AttrName` normalizando el valor.
pub fn new(value: impl AsRef<str>) -> Self {
AttrName::default().with_value(value)
}
// AttrName BUILDER ****************************************************************************
/// Establece un nombre nuevo normalizando el valor.
#[builder_fn]
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
let value = value.as_ref().trim().to_ascii_lowercase().replace(' ', "_");
self.0 = if value.is_empty() { None } else { Some(value) };
self
}
// AttrName GETTERS ****************************************************************************
/// Devuelve el nombre normalizado, si existe.
pub fn get(&self) -> Option<String> {
self.0.as_ref().cloned()
}
/// Devuelve el nombre normalizado (sin clonar), si existe.
pub fn as_str(&self) -> Option<&str> {
self.0.as_deref()
}
/// Devuelve el nombre normalizado (propiedad), si existe.
pub fn into_inner(self) -> Option<String> {
self.0
}
/// `true` si no hay valor.
pub fn is_empty(&self) -> bool {
self.0.is_none()
}
}

65
src/html/attr_value.rs Normal file
View file

@ -0,0 +1,65 @@
use crate::{builder_fn, AutoDefault};
/// Cadena normalizada para renderizar en atributos HTML.
///
/// Este tipo encapsula `Option<String>` garantizando un valor normalizado para su uso:
///
/// - Se eliminan los espacios al principio y al final.
/// - Si el resultado es una cadena vacía, se guarda `None`.
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let s = AttrValue::new(" a new string ");
/// assert_eq!(s.as_str(), Some("a new string"));
///
/// let empty = AttrValue::default();
/// assert_eq!(empty.get(), None);
/// ```
#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
pub struct AttrValue(Option<String>);
impl AttrValue {
/// Crea un nuevo `AttrValue` normalizando el valor.
pub fn new(value: impl AsRef<str>) -> Self {
AttrValue::default().with_value(value)
}
// AttrValue BUILDER ***************************************************************************
/// Establece una cadena nueva normalizando el valor.
#[builder_fn]
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
let value = value.as_ref().trim();
self.0 = if value.is_empty() {
None
} else {
Some(value.to_owned())
};
self
}
// AttrValue GETTERS ***************************************************************************
/// Devuelve la cadena normalizada, si existe.
pub fn get(&self) -> Option<String> {
self.0.as_ref().cloned()
}
/// Devuelve la cadena normalizada (sin clonar), si existe.
pub fn as_str(&self) -> Option<&str> {
self.0.as_deref()
}
/// Devuelve la cadena normalizada (propiedad), si existe.
pub fn into_inner(self) -> Option<String> {
self.0
}
/// `true` si no hay valor.
pub fn is_empty(&self) -> bool {
self.0.is_none()
}
}

View file

@ -1,68 +0,0 @@
use crate::builder_fn;
use crate::core::component::{Component, Typed};
use crate::html::{html, Context, Markup};
/// Contenedor de componente para incluir en otros componentes.
///
/// Este tipo encapsula `Option<Typed<C>>` para incluir un componente de manera segura en otros
/// componentes, útil para representar estructuras complejas.
///
/// # Ejemplo
///
/// ```rust,ignore
/// use pagetop::prelude::*;
///
/// let comp = MyComponent::new();
/// let opt = OptionComponent::new(comp);
/// assert!(opt.get().is_some());
/// ```
pub struct OptionComponent<C: Component>(Option<Typed<C>>);
impl<C: Component> Default for OptionComponent<C> {
fn default() -> Self {
OptionComponent(None)
}
}
impl<C: Component> OptionComponent<C> {
/// Crea un nuevo [`OptionComponent`].
///
/// El componente se envuelve automáticamente en un [`Typed`] y se almacena.
pub fn new(component: C) -> Self {
OptionComponent::default().with_value(Some(component))
}
// OptionComponent BUILDER *********************************************************************
/// Establece un componente nuevo, o lo vacía.
///
/// Si se proporciona `Some(component)`, se guarda en [`Typed`]; y si es `None`, se limpia.
#[builder_fn]
pub fn with_value(mut self, component: Option<C>) -> Self {
if let Some(component) = component {
self.0 = Some(Typed::with(component));
} else {
self.0 = None;
}
self
}
// OptionComponent GETTERS *********************************************************************
/// Devuelve el componente, si existe.
pub fn get(&self) -> Option<Typed<C>> {
if let Some(value) = &self.0 {
return Some(value.clone());
}
None
}
/// Renderiza el componente, si existe.
pub fn render(&self, cx: &mut Context) -> Markup {
if let Some(component) = &self.0 {
component.render(cx)
} else {
html! {}
}
}
}

View file

@ -1,59 +0,0 @@
use crate::{builder_fn, AutoDefault};
/// Identificador normalizado para el atributo `id` o similar de HTML.
///
/// Este tipo encapsula `Option<String>` garantizando un valor normalizado para su uso.
///
/// # Normalización
///
/// - Se eliminan los espacios al principio y al final.
/// - Se convierte a minúsculas.
/// - Se sustituyen los espacios intermedios por guiones bajos (`_`).
/// - Si el resultado es una cadena vacía, se guarda `None`.
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let id = OptionId::new(" main Section ");
/// assert_eq!(id.get(), Some(String::from("main_section")));
///
/// let empty = OptionId::default();
/// assert_eq!(empty.get(), None);
/// ```
#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
pub struct OptionId(Option<String>);
impl OptionId {
/// Crea un nuevo [`OptionId`].
///
/// El valor se normaliza automáticamente.
pub fn new(value: impl AsRef<str>) -> Self {
OptionId::default().with_value(value)
}
// OptionId BUILDER ****************************************************************************
/// Establece un identificador nuevo.
///
/// El valor se normaliza automáticamente.
#[builder_fn]
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
let value = value.as_ref().trim().to_ascii_lowercase().replace(' ', "_");
self.0 = (!value.is_empty()).then_some(value);
self
}
// OptionId GETTERS ****************************************************************************
/// Devuelve el identificador, si existe.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

View file

@ -1,59 +0,0 @@
use crate::{builder_fn, AutoDefault};
/// Nombre normalizado para el atributo `name` o similar de HTML.
///
/// Este tipo encapsula `Option<String>` garantizando un valor normalizado para su uso.
///
/// # Normalización
///
/// - Se eliminan los espacios al principio y al final.
/// - Se convierte a minúsculas.
/// - Se sustituyen los espacios intermedios por guiones bajos (`_`).
/// - Si el resultado es una cadena vacía, se guarda `None`.
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let name = OptionName::new(" DISplay name ");
/// assert_eq!(name.get(), Some(String::from("display_name")));
///
/// let empty = OptionName::default();
/// assert_eq!(empty.get(), None);
/// ```
#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
pub struct OptionName(Option<String>);
impl OptionName {
/// Crea un nuevo [`OptionName`].
///
/// El valor se normaliza automáticamente.
pub fn new(value: impl AsRef<str>) -> Self {
OptionName::default().with_value(value)
}
// OptionName BUILDER **************************************************************************
/// Establece un nombre nuevo.
///
/// El valor se normaliza automáticamente.
#[builder_fn]
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
let value = value.as_ref().trim().to_ascii_lowercase().replace(' ', "_");
self.0 = (!value.is_empty()).then_some(value);
self
}
// OptionName GETTERS **************************************************************************
/// Devuelve el nombre, si existe.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

View file

@ -1,57 +0,0 @@
use crate::{builder_fn, AutoDefault};
/// Cadena normalizada para renderizar en atributos HTML.
///
/// Este tipo encapsula `Option<String>` garantizando un valor normalizado para su uso.
///
/// # Normalización
///
/// - Se eliminan los espacios al principio y al final.
/// - Si el resultado es una cadena vacía, se guarda `None`.
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let s = OptionString::new(" a new string ");
/// assert_eq!(s.get(), Some(String::from("a new string")));
///
/// let empty = OptionString::default();
/// assert_eq!(empty.get(), None);
/// ```
#[derive(AutoDefault, Clone, Debug, Hash, Eq, PartialEq)]
pub struct OptionString(Option<String>);
impl OptionString {
/// Crea un nuevo [`OptionString`].
///
/// El valor se normaliza automáticamente.
pub fn new(value: impl AsRef<str>) -> Self {
OptionString::default().with_value(value)
}
// OptionString BUILDER ************************************************************************
/// Establece una cadena nueva.
///
/// El valor se normaliza automáticamente.
#[builder_fn]
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
let value = value.as_ref().trim().to_owned();
self.0 = (!value.is_empty()).then_some(value);
self
}
// OptionString GETTERS ************************************************************************
/// Devuelve la cadena, si existe.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

View file

@ -7,8 +7,10 @@ use crate::base::action;
use crate::builder_fn; use crate::builder_fn;
use crate::core::component::{Child, ChildOp, Component}; use crate::core::component::{Child, ChildOp, Component};
use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT}; use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT};
use crate::html::{html, AssetsOp, Context, Markup, DOCTYPE}; use crate::html::{html, Markup, DOCTYPE};
use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated}; use crate::html::{AssetsOp, Context};
use crate::html::{AttrClasses, ClassesOp};
use crate::html::{AttrId, AttrL10n};
use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
use crate::service::HttpRequest; use crate::service::HttpRequest;
@ -19,13 +21,13 @@ use crate::service::HttpRequest;
/// renderizado. /// renderizado.
#[rustfmt::skip] #[rustfmt::skip]
pub struct Page { pub struct Page {
title : OptionTranslated, title : AttrL10n,
description : OptionTranslated, description : AttrL10n,
metadata : Vec<(&'static str, &'static str)>, metadata : Vec<(&'static str, &'static str)>,
properties : Vec<(&'static str, &'static str)>, properties : Vec<(&'static str, &'static str)>,
context : Context, context : Context,
body_id : OptionId, body_id : AttrId,
body_classes: OptionClasses, body_classes: AttrClasses,
regions : ChildrenInRegions, regions : ChildrenInRegions,
} }
@ -37,13 +39,13 @@ impl Page {
#[rustfmt::skip] #[rustfmt::skip]
pub fn new(request: Option<HttpRequest>) -> Self { pub fn new(request: Option<HttpRequest>) -> Self {
Page { Page {
title : OptionTranslated::default(), title : AttrL10n::default(),
description : OptionTranslated::default(), description : AttrL10n::default(),
metadata : Vec::default(), metadata : Vec::default(),
properties : Vec::default(), properties : Vec::default(),
context : Context::new(request), context : Context::new(request),
body_id : OptionId::default(), body_id : AttrId::default(),
body_classes: OptionClasses::default(), body_classes: AttrClasses::default(),
regions : ChildrenInRegions::default(), regions : ChildrenInRegions::default(),
} }
} }
@ -113,7 +115,7 @@ impl Page {
self self
} }
/// Modifica las clases CSS del elemento `<body>` con una operación sobre [`OptionClasses`]. /// Modifica las clases CSS del elemento `<body>` con una operación sobre [`AttrClasses`].
#[builder_fn] #[builder_fn]
pub fn with_body_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self { pub fn with_body_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.body_classes.alter_value(op, classes); self.body_classes.alter_value(op, classes);
@ -183,12 +185,12 @@ impl Page {
} }
/// Devuelve el identificador del elemento `<body>`. /// Devuelve el identificador del elemento `<body>`.
pub fn body_id(&self) -> &OptionId { pub fn body_id(&self) -> &AttrId {
&self.body_id &self.body_id
} }
/// Devuelve las clases CSS del elemento `<body>`. /// Devuelve las clases CSS del elemento `<body>`.
pub fn body_classes(&self) -> &OptionClasses { pub fn body_classes(&self) -> &AttrClasses {
&self.body_classes &self.body_classes
} }