diff --git a/src/base/action/component/after_render_component.rs b/src/base/action/component/after_render_component.rs index 0cb0334..917f322 100644 --- a/src/base/action/component/after_render_component.rs +++ b/src/base/action/component/after_render_component.rs @@ -6,7 +6,7 @@ use crate::base::action::FnActionWithComponent; pub struct AfterRender { f: FnActionWithComponent, referer_type_id: Option, - referer_id: AttrId, + referer_id: OptionId, weight: Weight, } @@ -34,7 +34,7 @@ impl AfterRender { AfterRender { f, referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), + referer_id: OptionId::default(), weight: 0, } } diff --git a/src/base/action/component/before_render_component.rs b/src/base/action/component/before_render_component.rs index 46ff9aa..8c2e38d 100644 --- a/src/base/action/component/before_render_component.rs +++ b/src/base/action/component/before_render_component.rs @@ -6,7 +6,7 @@ use crate::base::action::FnActionWithComponent; pub struct BeforeRender { f: FnActionWithComponent, referer_type_id: Option, - referer_id: AttrId, + referer_id: OptionId, weight: Weight, } @@ -34,7 +34,7 @@ impl BeforeRender { BeforeRender { f, referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), + referer_id: OptionId::default(), weight: 0, } } diff --git a/src/base/action/component/is_renderable.rs b/src/base/action/component/is_renderable.rs index 5a0e244..baa86f1 100644 --- a/src/base/action/component/is_renderable.rs +++ b/src/base/action/component/is_renderable.rs @@ -11,7 +11,7 @@ pub type FnIsRenderable = fn(component: &C, cx: &Context) -> bool; pub struct IsRenderable { f: FnIsRenderable, referer_type_id: Option, - referer_id: AttrId, + referer_id: OptionId, weight: Weight, } @@ -39,7 +39,7 @@ impl IsRenderable { IsRenderable { f, referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), + referer_id: OptionId::default(), weight: 0, } } diff --git a/src/core/component.rs b/src/core/component.rs index 3691472..17b9b73 100644 --- a/src/core/component.rs +++ b/src/core/component.rs @@ -7,6 +7,3 @@ mod children; pub use children::Children; pub use children::{Child, ChildOp}; pub use children::{Typed, TypedOp}; - -mod slot; -pub use slot::TypedSlot; diff --git a/src/core/component/children.rs b/src/core/component/children.rs index cb112e1..fb85db7 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -9,13 +9,13 @@ use std::vec::IntoIter; /// Representa un componente encapsulado de forma segura y compartida. /// -/// Esta estructura permite manipular y renderizar un componente que implemente [`Component`], y -/// habilita acceso concurrente mediante [`Arc>`]. +/// Esta estructura permite manipular y renderizar cualquier tipo que implemente [`Component`], +/// garantizando acceso concurrente a través de [`Arc>`]. #[derive(Clone)] pub struct Child(Arc>); 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 { Child(Arc::new(RwLock::new(component))) } @@ -46,8 +46,7 @@ impl Child { /// Variante tipada de [`Child`] para evitar conversiones durante el uso. /// -/// Esta estructura permite manipular y renderizar un componente concreto que implemente -/// [`Component`], y habilita acceso concurrente mediante [`Arc>`]. +/// Facilita el acceso a componentes del mismo tipo sin necesidad de hacer `downcast`. pub struct Typed(Arc>); impl Clone for Typed { @@ -57,7 +56,7 @@ impl Clone for Typed { } impl Typed { - /// Crea un nuevo `Typed` a partir de un componente. + /// Crea un nuevo [`Typed`] a partir de un componente. pub fn with(component: C) -> Self { Typed(Arc::new(RwLock::new(component))) } @@ -285,7 +284,7 @@ impl IntoIterator for Children { /// /// # Ejemplo de uso: /// - /// ```rust,ignore + /// ```rust#ignore /// let children = Children::new().with(child1).with(child2); /// for child in children { /// println!("{:?}", child.id()); @@ -304,7 +303,7 @@ impl<'a> IntoIterator for &'a Children { /// /// # Ejemplo de uso: /// - /// ```rust,ignore + /// ```rust#ignore /// let children = Children::new().with(child1).with(child2); /// for child in &children { /// println!("{:?}", child.id()); @@ -323,7 +322,7 @@ impl<'a> IntoIterator for &'a mut Children { /// /// # Ejemplo de uso: /// - /// ```rust,ignore + /// ```rust#ignore /// let mut children = Children::new().with(child1).with(child2); /// for child in &mut children { /// child.render(&mut context); diff --git a/src/core/component/slot.rs b/src/core/component/slot.rs deleted file mode 100644 index 19ed72a..0000000 --- a/src/core/component/slot.rs +++ /dev/null @@ -1,64 +0,0 @@ -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>`, 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(Option>); - -impl Default for TypedSlot { - fn default() -> Self { - TypedSlot(None) - } -} - -impl TypedSlot { - /// 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) -> Self { - self.0 = component.map(Typed::with); - self - } - - // TypedSlot GETTERS ********************************************************************* - - /// Devuelve un clon (incrementa el contador `Arc`) de [`Typed`], si existe. - pub fn get(&self) -> Option> { - 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! {} - } - } -} diff --git a/src/html.rs b/src/html.rs index 784457e..82fa906 100644 --- a/src/html.rs +++ b/src/html.rs @@ -3,82 +3,52 @@ mod maud; pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE}; -// HTML DOCUMENT ASSETS **************************************************************************** - mod assets; pub use assets::favicon::Favicon; pub use assets::javascript::JavaScript; pub use assets::stylesheet::{StyleSheet, TargetMedia}; pub(crate) use assets::Assets; -// HTML DOCUMENT CONTEXT *************************************************************************** - mod context; pub use context::{AssetsOp, Context, ErrorParam}; -// HTML ATTRIBUTES ********************************************************************************* +mod opt_id; +pub use opt_id::OptionId; -mod attr_id; -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_name; +pub use opt_name::OptionName; -mod attr_name; -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_string; +pub use opt_string::OptionString; -mod attr_value; -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_translated; +pub use opt_translated::OptionTranslated; -mod attr_l10n; -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_classes; +pub use opt_classes::{ClassesOp, OptionClasses}; -mod attr_classes; -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; +mod opt_component; +pub use opt_component::OptionComponent; -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 = core::component::TypedSlot; +use crate::AutoDefault; /// 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`]. +/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML escapado o marcado +/// ya procesado) para renderizar 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(String::from("Hola mundo")); +/// let fragment = PrepareMarkup::Text(String::from("Hola mundo")); /// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>"); /// -/// // HTML literal, se inserta directamente, sin escapado adicional. -/// let raw_html = PrepareMarkup::Raw(String::from("negrita")); +/// let raw_html = PrepareMarkup::Escaped(String::from("negrita")); /// assert_eq!(raw_html.render().into_string(), "negrita"); /// -/// // 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." } @@ -90,22 +60,14 @@ pub type OptionComponent = core::component::Typed /// ``` #[derive(AutoDefault)] pub enum PrepareMarkup { - /// No se genera contenido HTML (equivale a `html! {}`). + /// No se genera contenido HTML (devuelve `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. + /// Texto estático que se escapará automáticamente para no ser interpretado como HTML. + Text(String), + /// Contenido sin escapado adicional, útil para HTML generado externamente. 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), } @@ -114,8 +76,8 @@ impl PrepareMarkup { pub fn is_empty(&self) -> bool { match self { PrepareMarkup::None => true, - PrepareMarkup::Escaped(text) => text.is_empty(), - PrepareMarkup::Raw(string) => string.is_empty(), + PrepareMarkup::Text(text) => text.is_empty(), + PrepareMarkup::Escaped(string) => string.is_empty(), PrepareMarkup::With(markup) => markup.is_empty(), } } @@ -126,8 +88,8 @@ impl Render for PrepareMarkup { fn render(&self) -> Markup { match self { PrepareMarkup::None => html! {}, - PrepareMarkup::Escaped(text) => html! { (text) }, - PrepareMarkup::Raw(string) => html! { (PreEscaped(string)) }, + PrepareMarkup::Text(text) => html! { (text) }, + PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) }, PrepareMarkup::With(markup) => html! { (markup) }, } } diff --git a/src/html/attr_id.rs b/src/html/attr_id.rs deleted file mode 100644 index 8bb1d33..0000000 --- a/src/html/attr_id.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{builder_fn, AutoDefault}; - -/// Identificador normalizado para el atributo `id` o similar de HTML. -/// -/// Este tipo encapsula `Option` 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); - -impl AttrId { - /// Crea un nuevo `AttrId` normalizando el valor. - pub fn new(value: impl AsRef) -> 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) -> 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 { - 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 { - self.0 - } - - /// `true` si no hay valor. - pub fn is_empty(&self) -> bool { - self.0.is_none() - } -} diff --git a/src/html/attr_name.rs b/src/html/attr_name.rs deleted file mode 100644 index 928f841..0000000 --- a/src/html/attr_name.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{builder_fn, AutoDefault}; - -/// Nombre normalizado para el atributo `name` o similar de HTML. -/// -/// Este tipo encapsula `Option` 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); - -impl AttrName { - /// Crea un nuevo `AttrName` normalizando el valor. - pub fn new(value: impl AsRef) -> 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) -> 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 { - 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 { - self.0 - } - - /// `true` si no hay valor. - pub fn is_empty(&self) -> bool { - self.0.is_none() - } -} diff --git a/src/html/attr_value.rs b/src/html/attr_value.rs deleted file mode 100644 index c70229f..0000000 --- a/src/html/attr_value.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::{builder_fn, AutoDefault}; - -/// Cadena normalizada para renderizar en atributos HTML. -/// -/// Este tipo encapsula `Option` 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); - -impl AttrValue { - /// Crea un nuevo `AttrValue` normalizando el valor. - pub fn new(value: impl AsRef) -> 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) -> 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 { - 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 { - self.0 - } - - /// `true` si no hay valor. - pub fn is_empty(&self) -> bool { - self.0.is_none() - } -} diff --git a/src/html/attr_classes.rs b/src/html/opt_classes.rs similarity index 90% rename from src/html/attr_classes.rs rename to src/html/opt_classes.rs index 92851aa..a985762 100644 --- a/src/html/attr_classes.rs +++ b/src/html/opt_classes.rs @@ -1,6 +1,6 @@ use crate::{builder_fn, AutoDefault}; -/// Operaciones disponibles sobre la lista de clases en [`AttrClasses`]. +/// Operaciones disponibles sobre la lista de clases en [`OptionClasses`]. pub enum ClassesOp { /// Añade al final (si no existe). Add, @@ -33,7 +33,7 @@ pub enum ClassesOp { /// ```rust /// use pagetop::prelude::*; /// -/// let classes = AttrClasses::new("Btn btn-primary") +/// let classes = OptionClasses::new("Btn btn-primary") /// .with_value(ClassesOp::Add, "Active") /// .with_value(ClassesOp::Remove, "btn-primary"); /// @@ -41,14 +41,14 @@ pub enum ClassesOp { /// assert!(classes.contains("active")); /// ``` #[derive(AutoDefault, Clone, Debug)] -pub struct AttrClasses(Vec); +pub struct OptionClasses(Vec); -impl AttrClasses { +impl OptionClasses { pub fn new(classes: impl AsRef) -> Self { - AttrClasses::default().with_value(ClassesOp::Prepend, classes) + OptionClasses::default().with_value(ClassesOp::Prepend, classes) } - // AttrClasses BUILDER ************************************************************************* + // OptionClasses BUILDER *********************************************************************** #[builder_fn] pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef) -> Self { @@ -114,7 +114,7 @@ impl AttrClasses { } } - // AttrClasses GETTERS ************************************************************************* + // OptionClasses GETTERS *********************************************************************** /// Devuele la cadena de clases, si existe. pub fn get(&self) -> Option { diff --git a/src/html/opt_component.rs b/src/html/opt_component.rs new file mode 100644 index 0000000..39106d9 --- /dev/null +++ b/src/html/opt_component.rs @@ -0,0 +1,68 @@ +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>` 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(Option>); + +impl Default for OptionComponent { + fn default() -> Self { + OptionComponent(None) + } +} + +impl OptionComponent { + /// 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) -> 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> { + 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! {} + } + } +} diff --git a/src/html/opt_id.rs b/src/html/opt_id.rs new file mode 100644 index 0000000..139fdcd --- /dev/null +++ b/src/html/opt_id.rs @@ -0,0 +1,59 @@ +use crate::{builder_fn, AutoDefault}; + +/// Identificador normalizado para el atributo `id` o similar de HTML. +/// +/// Este tipo encapsula `Option` 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); + +impl OptionId { + /// Crea un nuevo [`OptionId`]. + /// + /// El valor se normaliza automáticamente. + pub fn new(value: impl AsRef) -> 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) -> 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 { + if let Some(value) = &self.0 { + if !value.is_empty() { + return Some(value.to_owned()); + } + } + None + } +} diff --git a/src/html/opt_name.rs b/src/html/opt_name.rs new file mode 100644 index 0000000..ffb0b98 --- /dev/null +++ b/src/html/opt_name.rs @@ -0,0 +1,59 @@ +use crate::{builder_fn, AutoDefault}; + +/// Nombre normalizado para el atributo `name` o similar de HTML. +/// +/// Este tipo encapsula `Option` 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); + +impl OptionName { + /// Crea un nuevo [`OptionName`]. + /// + /// El valor se normaliza automáticamente. + pub fn new(value: impl AsRef) -> 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) -> 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 { + if let Some(value) = &self.0 { + if !value.is_empty() { + return Some(value.to_owned()); + } + } + None + } +} diff --git a/src/html/opt_string.rs b/src/html/opt_string.rs new file mode 100644 index 0000000..5bfd9c7 --- /dev/null +++ b/src/html/opt_string.rs @@ -0,0 +1,57 @@ +use crate::{builder_fn, AutoDefault}; + +/// Cadena normalizada para renderizar en atributos HTML. +/// +/// Este tipo encapsula `Option` 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); + +impl OptionString { + /// Crea un nuevo [`OptionString`]. + /// + /// El valor se normaliza automáticamente. + pub fn new(value: impl AsRef) -> 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) -> 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 { + if let Some(value) = &self.0 { + if !value.is_empty() { + return Some(value.to_owned()); + } + } + None + } +} diff --git a/src/html/attr_l10n.rs b/src/html/opt_translated.rs similarity index 79% rename from src/html/attr_l10n.rs rename to src/html/opt_translated.rs index cd5b389..b15ea18 100644 --- a/src/html/attr_l10n.rs +++ b/src/html/opt_translated.rs @@ -2,7 +2,7 @@ use crate::html::Markup; use crate::locale::{L10n, LangId}; use crate::{builder_fn, AutoDefault}; -/// Texto para [traducir](crate::locale) en atributos HTML. +/// Cadena para traducir al renderizar ([`locale`](crate::locale)). /// /// Encapsula un tipo [`L10n`] para manejar traducciones de forma segura. /// @@ -12,7 +12,7 @@ use crate::{builder_fn, AutoDefault}; /// use pagetop::prelude::*; /// /// // Traducción por clave en las locales por defecto de PageTop. -/// let hello = AttrL10n::new(L10n::l("test-hello-world")); +/// let hello = OptionTranslated::new(L10n::l("test-hello-world")); /// /// // Español disponible. /// assert_eq!( @@ -31,15 +31,15 @@ use crate::{builder_fn, AutoDefault}; /// assert_eq!(markup.into_string(), "¡Hola mundo!"); /// ``` #[derive(AutoDefault, Clone, Debug)] -pub struct AttrL10n(L10n); +pub struct OptionTranslated(L10n); -impl AttrL10n { - /// Crea una nueva instancia `AttrL10n`. +impl OptionTranslated { + /// Crea una nueva instancia [`OptionTranslated`]. pub fn new(value: L10n) -> Self { - AttrL10n(value) + OptionTranslated(value) } - // AttrL10n BUILDER **************************************************************************** + // OptionTranslated BUILDER ******************************************************************** /// Establece una traducción nueva. #[builder_fn] @@ -48,7 +48,7 @@ impl AttrL10n { self } - // AttrL10n GETTERS **************************************************************************** + // OptionTranslated GETTERS ******************************************************************** /// Devuelve la traducción para `language`, si existe. pub fn using(&self, language: &impl LangId) -> Option { diff --git a/src/response/page.rs b/src/response/page.rs index ea88e84..f30e299 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -7,10 +7,8 @@ use crate::base::action; use crate::builder_fn; use crate::core::component::{Child, ChildOp, Component}; use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT}; -use crate::html::{html, Markup, DOCTYPE}; -use crate::html::{AssetsOp, Context}; -use crate::html::{AttrClasses, ClassesOp}; -use crate::html::{AttrId, AttrL10n}; +use crate::html::{html, AssetsOp, Context, Markup, DOCTYPE}; +use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; use crate::service::HttpRequest; @@ -21,13 +19,13 @@ use crate::service::HttpRequest; /// renderizado. #[rustfmt::skip] pub struct Page { - title : AttrL10n, - description : AttrL10n, + title : OptionTranslated, + description : OptionTranslated, metadata : Vec<(&'static str, &'static str)>, properties : Vec<(&'static str, &'static str)>, context : Context, - body_id : AttrId, - body_classes: AttrClasses, + body_id : OptionId, + body_classes: OptionClasses, regions : ChildrenInRegions, } @@ -39,13 +37,13 @@ impl Page { #[rustfmt::skip] pub fn new(request: Option) -> Self { Page { - title : AttrL10n::default(), - description : AttrL10n::default(), + title : OptionTranslated::default(), + description : OptionTranslated::default(), metadata : Vec::default(), properties : Vec::default(), context : Context::new(request), - body_id : AttrId::default(), - body_classes: AttrClasses::default(), + body_id : OptionId::default(), + body_classes: OptionClasses::default(), regions : ChildrenInRegions::default(), } } @@ -115,7 +113,7 @@ impl Page { self } - /// Modifica las clases CSS del elemento `` con una operación sobre [`AttrClasses`]. + /// Modifica las clases CSS del elemento `` con una operación sobre [`OptionClasses`]. #[builder_fn] pub fn with_body_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { self.body_classes.alter_value(op, classes); @@ -185,12 +183,12 @@ impl Page { } /// Devuelve el identificador del elemento ``. - pub fn body_id(&self) -> &AttrId { + pub fn body_id(&self) -> &OptionId { &self.body_id } /// Devuelve las clases CSS del elemento ``. - pub fn body_classes(&self) -> &AttrClasses { + pub fn body_classes(&self) -> &OptionClasses { &self.body_classes } diff --git a/tests/html.rs b/tests/html.rs index 1499c70..315f74a 100644 --- a/tests/html.rs +++ b/tests/html.rs @@ -1,110 +1,17 @@ use pagetop::prelude::*; #[pagetop::test] -async fn prepare_markup_render_none_is_empty_string() { - assert_eq!(render(&PrepareMarkup::None), ""); -} +async fn prepare_markup_is_empty() { + let _app = service::test::init_service(Application::new().test()).await; -#[pagetop::test] -async fn prepare_markup_render_escaped_escapes_html_and_ampersands() { - let pm = PrepareMarkup::Escaped(String::from("& \" ' ")); - assert_eq!(render(&pm), "<b>& " ' </b>"); -} - -#[pagetop::test] -async fn prepare_markup_render_raw_is_inserted_verbatim() { - let pm = PrepareMarkup::Raw(String::from("bold")); - assert_eq!(render(&pm), "bold"); -} - -#[pagetop::test] -async fn prepare_markup_render_with_keeps_structure() { - let pm = PrepareMarkup::With(html! { - h2 { "Sample title" } - p { "This is a paragraph." } - }); - assert_eq!( - render(&pm), - "

Sample title

This is a paragraph.

" - ); -} - -#[pagetop::test] -async fn prepare_markup_does_not_double_escape_when_wrapped_in_html_macro() { - // Escaped: dentro de `html!` no debe volver a escaparse. - let escaped = PrepareMarkup::Escaped("x".into()); - let wrapped_escaped = html! { div { (escaped) } }; - assert_eq!( - wrapped_escaped.into_string(), - "
<i>x</i>
" - ); - - // Raw: tampoco debe escaparse al integrarlo. - let raw = PrepareMarkup::Raw("x".into()); - let wrapped_raw = html! { div { (raw) } }; - assert_eq!(wrapped_raw.into_string(), "
x
"); - - // With: debe incrustar el Markup tal cual. - let with = PrepareMarkup::With(html! { span.title { "ok" } }); - let wrapped_with = html! { div { (with) } }; - assert_eq!( - wrapped_with.into_string(), - "
ok
" - ); -} - -#[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!(render(&esc), "Hello, tomorrow coffee ☕ & donuts!"); - - // Raw debe pasar íntegro. - let raw = PrepareMarkup::Raw("Title — section © 2025".into()); - assert_eq!(render(&raw), "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(String::from("")).is_empty()); - assert!(!PrepareMarkup::Escaped(String::from("x")).is_empty()); + assert!(PrepareMarkup::Text(String::from("")).is_empty()); + assert!(!PrepareMarkup::Text(String::from("x")).is_empty()); - assert!(PrepareMarkup::Raw(String::new()).is_empty()); - assert!(PrepareMarkup::Raw(String::from("")).is_empty()); - assert!(!PrepareMarkup::Raw("a".into()).is_empty()); + assert!(PrepareMarkup::Escaped(String::new()).is_empty()); + assert!(!PrepareMarkup::Escaped("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_equivalence_between_render_and_inline_in_html_macro() { - let cases = [ - PrepareMarkup::None, - PrepareMarkup::Escaped("x".into()), - PrepareMarkup::Raw("x".into()), - PrepareMarkup::With(html! { b { "x" } }), - ]; - - for pm in cases { - let rendered = render(&pm); - let in_macro = html! { (pm) }.into_string(); - assert_eq!( - rendered, in_macro, - "The output of Render and (pm) inside html! must match" - ); - } -} - -// HELPERS ***************************************************************************************** - -fn render(x: &impl Render) -> String { - x.render().into_string() }