✨ Añade tipos para renderizar atributos HTML
This commit is contained in:
parent
613ab5243c
commit
f88513d67f
7 changed files with 393 additions and 2 deletions
15
src/html.rs
15
src/html.rs
|
@ -12,6 +12,21 @@ pub(crate) use assets::Assets;
|
|||
mod context;
|
||||
pub use context::{Context, ErrorParam};
|
||||
|
||||
mod opt_id;
|
||||
pub use opt_id::OptionId;
|
||||
|
||||
mod opt_name;
|
||||
pub use opt_name::OptionName;
|
||||
|
||||
mod opt_string;
|
||||
pub use opt_string::OptionString;
|
||||
|
||||
mod opt_translated;
|
||||
pub use opt_translated::OptionTranslated;
|
||||
|
||||
mod opt_classes;
|
||||
pub use opt_classes::{ClassesOp, OptionClasses};
|
||||
|
||||
use crate::AutoDefault;
|
||||
|
||||
/// Prepara contenido HTML para su conversión a [`Markup`].
|
||||
|
|
130
src/html/opt_classes.rs
Normal file
130
src/html/opt_classes.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
use crate::{builder_fn, AutoDefault};
|
||||
|
||||
/// Operaciones disponibles sobre la lista de clases en [`OptionClasses`].
|
||||
pub enum ClassesOp {
|
||||
/// Añade al final (si no existe).
|
||||
Add,
|
||||
/// Añade al principio.
|
||||
Prepend,
|
||||
/// Elimina coincidencias.
|
||||
Remove,
|
||||
/// Sustituye una o varias por las nuevas (`Replace("old other")`).
|
||||
Replace(String),
|
||||
/// Alterna presencia/ausencia.
|
||||
Toggle,
|
||||
/// Sustituye toda la lista.
|
||||
Set,
|
||||
}
|
||||
|
||||
/// Cadena de clases CSS normalizadas para el atributo `class` de HTML.
|
||||
///
|
||||
/// Permite construir y modificar dinámicamente con [`ClassesOp`] una lista de clases CSS
|
||||
/// normalizadas.
|
||||
///
|
||||
/// ### Normalización
|
||||
/// - El [orden de las clases no es relevante](https://stackoverflow.com/a/1321712) en CSS.
|
||||
/// - No se permiten clases duplicadas.
|
||||
/// - Las clases vacías se ignoran.
|
||||
///
|
||||
/// # Ejemplo
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let classes = OptionClasses::new("btn btn-primary")
|
||||
/// .with_value(ClassesOp::Add, "active")
|
||||
/// .with_value(ClassesOp::Remove, "btn-primary");
|
||||
///
|
||||
/// assert_eq!(classes.get(), Some(String::from("btn active")));
|
||||
/// assert!(classes.contains("active"));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub struct OptionClasses(Vec<String>);
|
||||
|
||||
impl OptionClasses {
|
||||
pub fn new(classes: impl AsRef<str>) -> Self {
|
||||
OptionClasses::default().with_value(ClassesOp::Prepend, classes)
|
||||
}
|
||||
|
||||
// OptionClasses BUILDER ***********************************************************************
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
let classes: &str = classes.as_ref();
|
||||
let classes: Vec<&str> = classes.split_ascii_whitespace().collect();
|
||||
|
||||
if classes.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
match op {
|
||||
ClassesOp::Add => {
|
||||
self.add(&classes, self.0.len());
|
||||
}
|
||||
ClassesOp::Prepend => {
|
||||
self.add(&classes, 0);
|
||||
}
|
||||
ClassesOp::Remove => {
|
||||
for class in classes {
|
||||
self.0.retain(|c| c.ne(&class.to_string()));
|
||||
}
|
||||
}
|
||||
ClassesOp::Replace(classes_to_replace) => {
|
||||
let mut pos = self.0.len();
|
||||
let replace: Vec<&str> = classes_to_replace.split_ascii_whitespace().collect();
|
||||
for class in replace {
|
||||
if let Some(replace_pos) = self.0.iter().position(|c| c.eq(class)) {
|
||||
self.0.remove(replace_pos);
|
||||
if pos > replace_pos {
|
||||
pos = replace_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.add(&classes, pos);
|
||||
}
|
||||
ClassesOp::Toggle => {
|
||||
for class in classes {
|
||||
if !class.is_empty() {
|
||||
if let Some(pos) = self.0.iter().position(|c| c.eq(class)) {
|
||||
self.0.remove(pos);
|
||||
} else {
|
||||
self.0.push(class.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ClassesOp::Set => {
|
||||
self.0.clear();
|
||||
self.add(&classes, 0);
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn add(&mut self, classes: &[&str], mut pos: usize) {
|
||||
for &class in classes {
|
||||
if !class.is_empty() && !self.0.iter().any(|c| c == class) {
|
||||
self.0.insert(pos, class.to_string());
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OptionClasses GETTERS ***********************************************************************
|
||||
|
||||
/// Devuele la cadena de clases, si existe.
|
||||
pub fn get(&self) -> Option<String> {
|
||||
if self.0.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.0.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
/// Devuelve `true` si la clase está presente.
|
||||
pub fn contains(&self, class: impl AsRef<str>) -> bool {
|
||||
let class = class.as_ref();
|
||||
self.0.iter().any(|c| c == class)
|
||||
}
|
||||
}
|
57
src/html/opt_id.rs
Normal file
57
src/html/opt_id.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
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 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().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
|
||||
}
|
||||
}
|
57
src/html/opt_name.rs
Normal file
57
src/html/opt_name.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
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 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().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
|
||||
}
|
||||
}
|
56
src/html/opt_string.rs
Normal file
56
src/html/opt_string.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
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
|
||||
}
|
||||
}
|
65
src/html/opt_translated.rs
Normal file
65
src/html/opt_translated.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use crate::html::Markup;
|
||||
use crate::locale::{L10n, LanguageIdentifier};
|
||||
use crate::{builder_fn, AutoDefault};
|
||||
|
||||
/// Cadena para traducir al renderizar ([`locale`](crate::locale)).
|
||||
///
|
||||
/// Encapsula un tipo [`L10n`] para manejar traducciones de forma segura.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// // Traducción por clave en las locales por defecto de PageTop.
|
||||
/// let hello = OptionTranslated::new(L10n::l("test-hello-world"));
|
||||
///
|
||||
/// // Español disponible.
|
||||
/// assert_eq!(
|
||||
/// hello.using(LangMatch::langid_or_default("es-ES")),
|
||||
/// Some(String::from("¡Hola mundo!"))
|
||||
/// );
|
||||
///
|
||||
/// // Japonés no disponible, traduce al idioma de respaldo ("en-US").
|
||||
/// assert_eq!(
|
||||
/// hello.using(LangMatch::langid_or_fallback("ja-JP")),
|
||||
/// Some(String::from("Hello world!"))
|
||||
/// );
|
||||
///
|
||||
/// // Para incrustar en HTML escapado:
|
||||
/// let markup = hello.escaped(LangMatch::langid_or_default("es-ES"));
|
||||
/// assert_eq!(markup.into_string(), "¡Hola mundo!");
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub struct OptionTranslated(L10n);
|
||||
|
||||
impl OptionTranslated {
|
||||
/// Crea una nueva instancia [`OptionTranslated`].
|
||||
pub fn new(value: L10n) -> Self {
|
||||
OptionTranslated(value)
|
||||
}
|
||||
|
||||
// OptionTranslated BUILDER ********************************************************************
|
||||
|
||||
/// Establece una traducción nueva.
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: L10n) -> Self {
|
||||
self.0 = value;
|
||||
self
|
||||
}
|
||||
|
||||
// OptionTranslated GETTERS ********************************************************************
|
||||
|
||||
/// Devuelve la traducción para `langid`, si existe.
|
||||
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> {
|
||||
self.0.using(langid)
|
||||
}
|
||||
|
||||
/// Devuelve la traducción *escapada* como [`Markup`] para `langid`, si existe.
|
||||
///
|
||||
/// Útil para incrustar el texto directamente en plantillas HTML sin riesgo de inyección de
|
||||
/// contenido.
|
||||
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup {
|
||||
self.0.escaped(langid)
|
||||
}
|
||||
}
|
|
@ -284,7 +284,7 @@ include_locales!(LOCALES_PAGETOP);
|
|||
// * `None` - No se aplica ninguna localización.
|
||||
// * `Text` - Con una cadena literal que se devolverá tal cual.
|
||||
// * `Translate` - Con la clave a resolver en el `Locales` indicado.
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
enum L10nOp {
|
||||
#[default]
|
||||
None,
|
||||
|
@ -322,7 +322,7 @@ enum L10nOp {
|
|||
/// // Traducción con clave, conjunto de traducciones e identificador de idioma a usar.
|
||||
/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(LangMatch::langid_or_default("it"));
|
||||
/// ```
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone)]
|
||||
pub struct L10n {
|
||||
op: L10nOp,
|
||||
#[default(&LOCALES_PAGETOP)]
|
||||
|
@ -411,6 +411,17 @@ impl L10n {
|
|||
}
|
||||
}
|
||||
|
||||
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 fmt::Display for L10n {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let content = match &self.op {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue