Compare commits
No commits in common. "c3ff8a6ff83d2a60f4135f3713ba0f9cdc9a7ef1" and "946d33566440c0fe11f6cdff1e298cccc7426c33" have entirely different histories.
c3ff8a6ff8
...
946d335664
13 changed files with 164 additions and 519 deletions
|
|
@ -145,7 +145,6 @@ impl Theme for Bootsier {
|
||||||
setup_component!(component, {
|
setup_component!(component, {
|
||||||
Button => |c| theme::bs::button::setup(c),
|
Button => |c| theme::bs::button::setup(c),
|
||||||
Container => |c| theme::bs::container::setup(c),
|
Container => |c| theme::bs::container::setup(c),
|
||||||
Image => |c| theme::bs::image::setup(c),
|
|
||||||
form::input::Field => |c| theme::bs::form::input::setup(c),
|
form::input::Field => |c| theme::bs::form::input::setup(c),
|
||||||
form::select::Field => |c| theme::bs::form::select::setup(c),
|
form::select::Field => |c| theme::bs::form::select::setup(c),
|
||||||
form::Textarea => |c| theme::bs::form::textarea::setup(c),
|
form::Textarea => |c| theme::bs::form::textarea::setup(c),
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,7 @@
|
||||||
//! Definiciones para renderizar imágenes ([`Image`]).
|
//! Definiciones para renderizar imágenes ([`Image`]).
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
mod props;
|
||||||
|
pub use props::{Size, Source};
|
||||||
|
|
||||||
pub use pagetop::base::component::image::{Image, Size, Source};
|
mod component;
|
||||||
|
pub use component::Image;
|
||||||
// **< Image SETUP >********************************************************************************
|
|
||||||
|
|
||||||
pub(crate) fn setup(image: &mut Image) {
|
|
||||||
match image.source() {
|
|
||||||
Source::Logo(_) | Source::Responsive(_) => {
|
|
||||||
image.alter_prop(PropsOp::replace_classes("image image-fluid", "img-fluid"));
|
|
||||||
}
|
|
||||||
Source::Thumbnail(_) => {
|
|
||||||
image.alter_prop(PropsOp::replace_classes(
|
|
||||||
"image image-thumbnail",
|
|
||||||
"img-thumbnail",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Source::Plain(_) => {
|
|
||||||
image.alter_prop(PropsOp::remove_classes("image"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,25 @@
|
||||||
use crate::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::theme::*;
|
||||||
|
|
||||||
/// Componente para renderizar una **imagen**.
|
/// Componente para renderizar una **imagen**.
|
||||||
///
|
///
|
||||||
/// A una imagen se le puede:
|
/// A una imagen se le puede:
|
||||||
///
|
///
|
||||||
/// - Establecer su contenido a partir del origen definido en [`image::Source`].
|
/// - Establecer su contenido a partir del origen definido en
|
||||||
/// - Configurar sus **dimensiones** ([`with_size()`](Self::with_size)).
|
/// [`image::Source`](crate::theme::bs::image::Source).
|
||||||
|
/// - Configurar sus **dimensiones** ([`with_size()`](Self::with_size)), **borde**
|
||||||
|
/// ([`Border`](crate::theme::class::Border)) y **redondeo de esquinas**
|
||||||
|
/// ([`Rounded`](crate::theme::class::Rounded)).
|
||||||
/// - Aplicar el texto alternativo `alt` con **localización** mediante [`Lc`].
|
/// - Aplicar el texto alternativo `alt` con **localización** mediante [`Lc`].
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// let logo = Image::with(image::Source::logo(PageTopSvg::Color))
|
|
||||||
/// .with_alternative(Lc::n("PageTop"));
|
|
||||||
///
|
|
||||||
/// let photo = Image::with(image::Source::responsive("/files/photo.jpg"))
|
|
||||||
/// .with_size(image::Size::Width(UnitValue::Px(320)))
|
|
||||||
/// .with_alternative(Lc::n("Team photo"));
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||||
props: Props,
|
props: Props,
|
||||||
/// Devuelve las dimensiones de la imagen.
|
/// Devuelve las dimensiones de la imagen.
|
||||||
size: image::Size,
|
size: bs::image::Size,
|
||||||
/// Devuelve el origen de la imagen.
|
/// Devuelve el origen de la imagen.
|
||||||
source: image::Source,
|
source: bs::image::Source,
|
||||||
/// Devuelve el texto alternativo localizado.
|
/// Devuelve el texto alternativo localizado.
|
||||||
alternative: Attr<Lc>,
|
alternative: Attr<Lc>,
|
||||||
}
|
}
|
||||||
|
|
@ -43,40 +35,20 @@ impl Component for Image {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_prop(PropsOp::prepend_classes(match self.source() {
|
// Clases CSS por defecto para la imagen, según el origen seleccionado.
|
||||||
image::Source::Logo(_) => "image image-fluid",
|
self.alter_prop(PropsOp::prepend_classes(self.source().to_class()));
|
||||||
image::Source::Responsive(_) => "image image-fluid",
|
|
||||||
image::Source::Thumbnail(_) => "image image-thumbnail",
|
|
||||||
image::Source::Plain(_) => "image",
|
|
||||||
}));
|
|
||||||
// El tamaño se aplica como declaraciones `style` individuales sobre `Props`.
|
|
||||||
match *self.size() {
|
|
||||||
image::Size::Auto => {}
|
|
||||||
image::Size::Dimensions(w, h) => {
|
|
||||||
self.alter_prop(PropsOp::add_style("width", w.to_string()));
|
|
||||||
self.alter_prop(PropsOp::add_style("height", h.to_string()));
|
|
||||||
}
|
|
||||||
image::Size::Width(w) => {
|
|
||||||
self.alter_prop(PropsOp::add_style("width", w.to_string()));
|
|
||||||
}
|
|
||||||
image::Size::Height(h) => {
|
|
||||||
self.alter_prop(PropsOp::add_style("height", h.to_string()));
|
|
||||||
}
|
|
||||||
image::Size::Both(v) => {
|
|
||||||
self.alter_prop(PropsOp::add_style("width", v.to_string()));
|
|
||||||
self.alter_prop(PropsOp::add_style("height", v.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
let dimensions = self.size().to_style();
|
||||||
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
|
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
|
||||||
let source = match self.source() {
|
|
||||||
image::Source::Logo(logo) => {
|
|
||||||
let is_decorative = alt_text.is_empty();
|
let is_decorative = alt_text.is_empty();
|
||||||
|
let source = match self.source() {
|
||||||
|
bs::image::Source::Logo(logo) => {
|
||||||
return Ok(html! {
|
return Ok(html! {
|
||||||
span
|
span
|
||||||
(self.props())
|
(self.props())
|
||||||
|
style=[dimensions]
|
||||||
role=[(!is_decorative).then_some("img")]
|
role=[(!is_decorative).then_some("img")]
|
||||||
aria-label=[(!is_decorative).then_some(alt_text)]
|
aria-label=[(!is_decorative).then_some(alt_text)]
|
||||||
aria-hidden=[is_decorative.then_some("true")]
|
aria-hidden=[is_decorative.then_some("true")]
|
||||||
|
|
@ -85,22 +57,23 @@ impl Component for Image {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
image::Source::Responsive(source) => Some(source),
|
bs::image::Source::Responsive(source) => Some(source),
|
||||||
image::Source::Thumbnail(source) => Some(source),
|
bs::image::Source::Thumbnail(source) => Some(source),
|
||||||
image::Source::Plain(source) => Some(source),
|
bs::image::Source::Plain(source) => Some(source),
|
||||||
};
|
};
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
img
|
img
|
||||||
src=[source]
|
src=[source]
|
||||||
alt=(alt_text)
|
alt=(alt_text)
|
||||||
(self.props()) {}
|
(self.props())
|
||||||
|
style=[dimensions] {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
/// Crea rápidamente una imagen especificando su origen.
|
/// Crea rápidamente una imagen especificando su origen.
|
||||||
pub fn with(source: image::Source) -> Self {
|
pub fn with(source: bs::image::Source) -> Self {
|
||||||
Self::default().with_source(source)
|
Self::default().with_source(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,6 +87,11 @@ impl Image {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||||
|
///
|
||||||
|
/// También acepta clases predefinidas para:
|
||||||
|
///
|
||||||
|
/// - Establecer bordes ([`Border`]).
|
||||||
|
/// - Redondear las esquinas ([`Rounded`]).
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||||
self.props.alter_prop(op);
|
self.props.alter_prop(op);
|
||||||
|
|
@ -122,14 +100,14 @@ impl Image {
|
||||||
|
|
||||||
/// Define las dimensiones de la imagen (auto, ancho/alto, ambos).
|
/// Define las dimensiones de la imagen (auto, ancho/alto, ambos).
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_size(mut self, size: image::Size) -> Self {
|
pub fn with_size(mut self, size: bs::image::Size) -> Self {
|
||||||
self.size = size;
|
self.size = size;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece el origen de la imagen, influyendo en su disposición en el contenido.
|
/// Establece el origen de la imagen, influyendo en su disposición en el contenido.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_source(mut self, source: image::Source) -> Self {
|
pub fn with_source(mut self, source: bs::image::Source) -> Self {
|
||||||
self.source = source;
|
self.source = source;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
// **< Size >***************************************************************************************
|
// **< Size >***************************************************************************************
|
||||||
|
|
||||||
/// Define las **dimensiones** de una imagen ([`Image`](super::Image)).
|
/// Define las **dimensiones** de una imagen ([`Image`](crate::theme::bs::Image)).
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum Size {
|
pub enum Size {
|
||||||
/// Ajuste automático por defecto.
|
/// Ajuste automático por defecto.
|
||||||
|
|
@ -30,13 +30,23 @@ pub enum Size {
|
||||||
Both(UnitValue),
|
Both(UnitValue),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Size {
|
||||||
|
/// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
|
||||||
|
#[inline]
|
||||||
|
pub fn to_style(self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Self::Auto => None,
|
||||||
|
Self::Dimensions(w, h) => Some(format!("width: {w}; height: {h};")),
|
||||||
|
Self::Width(w) => Some(format!("width: {w};")),
|
||||||
|
Self::Height(h) => Some(format!("height: {h};")),
|
||||||
|
Self::Both(v) => Some(format!("width: {v}; height: {v};")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// **< Source >*************************************************************************************
|
// **< Source >*************************************************************************************
|
||||||
|
|
||||||
/// Especifica la **fuente** para publicar una imagen ([`Image`](super::Image)).
|
/// Especifica la **fuente** para publicar una imagen ([`Image`](crate::theme::bs::Image)).
|
||||||
///
|
|
||||||
/// Las variantes son puramente semánticas. El componente aplica una clase CSS base según la
|
|
||||||
/// variante en su propio `setup()`; los temas pueden sobrescribirla interceptando el renderizado
|
|
||||||
/// del componente.
|
|
||||||
#[derive(AutoDefault, Clone, Debug, PartialEq)]
|
#[derive(AutoDefault, Clone, Debug, PartialEq)]
|
||||||
pub enum Source {
|
pub enum Source {
|
||||||
/// Imagen con el logotipo de PageTop.
|
/// Imagen con el logotipo de PageTop.
|
||||||
|
|
@ -46,39 +56,71 @@ pub enum Source {
|
||||||
///
|
///
|
||||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
/// Lleva asociada la URL (o ruta) de la imagen.
|
||||||
Responsive(CowStr),
|
Responsive(CowStr),
|
||||||
/// Imagen que aplica un estilo de miniatura.
|
/// Imagen que aplica el estilo **miniatura** de Bootstrap.
|
||||||
///
|
///
|
||||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
/// Lleva asociada la URL (o ruta) de la imagen.
|
||||||
Thumbnail(CowStr),
|
Thumbnail(CowStr),
|
||||||
/// Imagen sin modificadores adicionales de estilo, útil para controlar la apariencia con CSS
|
/// Imagen sin clases específicas de Bootstrap, útil para controlar con CSS propio.
|
||||||
/// propio.
|
|
||||||
///
|
///
|
||||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
/// Lleva asociada la URL (o ruta) de la imagen.
|
||||||
Plain(CowStr),
|
Plain(CowStr),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Source {
|
impl Source {
|
||||||
|
const IMG_FLUID: &str = "img-fluid";
|
||||||
|
const IMG_THUMBNAIL: &str = "img-thumbnail";
|
||||||
|
|
||||||
/// Imagen con el logotipo de PageTop.
|
/// Imagen con el logotipo de PageTop.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn logo(svg: PageTopSvg) -> Self {
|
pub fn logo(svg: PageTopSvg) -> Self {
|
||||||
Self::Logo(svg)
|
Self::Logo(svg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Imagen responsive.
|
/// Imagen responsive (`img-fluid`).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn responsive(url: impl Into<CowStr>) -> Self {
|
pub fn responsive(url: impl Into<CowStr>) -> Self {
|
||||||
Self::Responsive(url.into())
|
Self::Responsive(url.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Imagen miniatura.
|
/// Imagen miniatura (`img-thumbnail`).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn thumbnail(url: impl Into<CowStr>) -> Self {
|
pub fn thumbnail(url: impl Into<CowStr>) -> Self {
|
||||||
Self::Thumbnail(url.into())
|
Self::Thumbnail(url.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Imagen sin modificadores adicionales de estilo.
|
/// Imagen sin clases adicionales.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn plain(url: impl Into<CowStr>) -> Self {
|
pub fn plain(url: impl Into<CowStr>) -> Self {
|
||||||
Self::Plain(url.into())
|
Self::Plain(url.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Devuelve la clase base asociada a la imagen según la fuente.
|
||||||
|
#[inline]
|
||||||
|
pub const fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Source::Logo(_) | Source::Responsive(_) => Self::IMG_FLUID,
|
||||||
|
Source::Thumbnail(_) => Self::IMG_THUMBNAIL,
|
||||||
|
Source::Plain(_) => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade la clase asociada al tipo de imagen a la cadena de clases.
|
||||||
|
#[inline]
|
||||||
|
pub fn push_to(&self, classes: &mut String) {
|
||||||
|
let s = self.as_str();
|
||||||
|
if s.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !classes.is_empty() {
|
||||||
|
classes.push(' ');
|
||||||
|
}
|
||||||
|
classes.push_str(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve la clase asociada al tipo de imagen.
|
||||||
|
pub fn to_class(&self) -> String {
|
||||||
|
let mut class = String::new();
|
||||||
|
self.push_to(&mut class);
|
||||||
|
class
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +206,6 @@ impl DiagnosticParse for Element {
|
||||||
},
|
},
|
||||||
attrs: {
|
attrs: {
|
||||||
let mut id_pushed = false;
|
let mut id_pushed = false;
|
||||||
let mut splice_pushed = false;
|
|
||||||
let mut attrs = Vec::new();
|
let mut attrs = Vec::new();
|
||||||
|
|
||||||
while input.peek(Ident::peek_any)
|
while input.peek(Ident::peek_any)
|
||||||
|
|
@ -227,16 +226,6 @@ impl DiagnosticParse for Element {
|
||||||
id_pushed = true;
|
id_pushed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Attribute::Splice { .. } = attr {
|
|
||||||
if splice_pushed {
|
|
||||||
return Err(Error::new_spanned(
|
|
||||||
attr,
|
|
||||||
"only one spliced attribute value is allowed per element",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
splice_pushed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs.push(attr);
|
attrs.push(attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use proc_macro2::{Ident, Span, TokenStream};
|
use proc_macro2::{Ident, Span, TokenStream};
|
||||||
use quote::{ToTokens, quote};
|
use quote::{ToTokens, quote};
|
||||||
use syn::{Expr, LitStr, Local, parse_quote, token::Brace};
|
use syn::{Expr, Local, parse_quote, token::Brace};
|
||||||
|
|
||||||
use crate::maud::{ast::*, escape};
|
use crate::maud::{ast::*, escape};
|
||||||
|
|
||||||
|
|
@ -71,17 +71,6 @@ impl Generator {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn splice_attrs(&self, expr: Expr, exclude: &[LitStr], build: &mut Builder) {
|
|
||||||
let output_ident = &self.output_ident;
|
|
||||||
build.push_tokens(quote!(
|
|
||||||
pagetop::html::html_private::render_attrs_to!(
|
|
||||||
&(#expr),
|
|
||||||
&[#(#exclude),*],
|
|
||||||
&mut #output_ident
|
|
||||||
);
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn element(&self, element: Element, build: &mut Builder) {
|
fn element(&self, element: Element, build: &mut Builder) {
|
||||||
let element_name = element.name.clone().unwrap_or_else(|| parse_quote!(div));
|
let element_name = element.name.clone().unwrap_or_else(|| parse_quote!(div));
|
||||||
build.push_str("<");
|
build.push_str("<");
|
||||||
|
|
@ -152,21 +141,6 @@ impl Generator {
|
||||||
fn attrs(&self, attrs: Vec<Attribute>, build: &mut Builder) {
|
fn attrs(&self, attrs: Vec<Attribute>, build: &mut Builder) {
|
||||||
let (classes, id, named_attrs, spliced) = split_attrs(attrs);
|
let (classes, id, named_attrs, spliced) = split_attrs(attrs);
|
||||||
|
|
||||||
// Must run before `classes`/`id`/`named_attrs` are consumed below.
|
|
||||||
let literal_attr_names: Vec<LitStr> = {
|
|
||||||
let mut names = Vec::new();
|
|
||||||
if !classes.is_empty() {
|
|
||||||
names.push(LitStr::new("class", Span::call_site()));
|
|
||||||
}
|
|
||||||
if id.is_some() {
|
|
||||||
names.push(LitStr::new("id", Span::call_site()));
|
|
||||||
}
|
|
||||||
for (name, _) in &named_attrs {
|
|
||||||
names.push(LitStr::new(&name.to_string(), Span::call_site()));
|
|
||||||
}
|
|
||||||
names
|
|
||||||
};
|
|
||||||
|
|
||||||
if !classes.is_empty() {
|
if !classes.is_empty() {
|
||||||
let mut toggle_class_exprs = vec![];
|
let mut toggle_class_exprs = vec![];
|
||||||
|
|
||||||
|
|
@ -211,7 +185,7 @@ impl Generator {
|
||||||
self.attr(name, attr_type, build);
|
self.attr(name, attr_type, build);
|
||||||
}
|
}
|
||||||
for expr in spliced {
|
for expr in spliced {
|
||||||
self.splice_attrs(expr, &literal_attr_names, build);
|
self.splice(expr, build);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,6 @@ pub use form::Form;
|
||||||
mod html;
|
mod html;
|
||||||
pub use html::Html;
|
pub use html::Html;
|
||||||
|
|
||||||
pub mod image;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use image::Image;
|
|
||||||
|
|
||||||
mod intro;
|
mod intro;
|
||||||
pub use intro::{Intro, IntroOpening};
|
pub use intro::{Intro, IntroOpening};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
//! Definiciones para renderizar imágenes ([`Image`]).
|
|
||||||
|
|
||||||
mod props;
|
|
||||||
pub use props::{Size, Source};
|
|
||||||
|
|
||||||
mod component;
|
|
||||||
pub use component::Image;
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
//! HTML en código.
|
//! HTML en código.
|
||||||
|
|
||||||
pub(crate) mod maud;
|
pub(crate) mod maud;
|
||||||
pub use maud::DOCTYPE;
|
pub use maud::{DOCTYPE, Escaper, Markup, PreEscaped, Render, display, html, html_private};
|
||||||
pub use maud::{Escaper, Markup, PreEscaped, Render, RenderAttrs};
|
|
||||||
pub use maud::{display, html, html_private};
|
|
||||||
|
|
||||||
mod route_path;
|
mod route_path;
|
||||||
pub use route_path::RoutePath;
|
pub use route_path::RoutePath;
|
||||||
|
|
|
||||||
125
src/html/maud.rs
125
src/html/maud.rs
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
//! A macro for writing HTML templates.
|
//! A macro for writing HTML templates.
|
||||||
//!
|
//!
|
||||||
//! This documentation only describes the runtime API. For a general guide, check out the [book]
|
//! This documentation only describes the runtime API. For a general
|
||||||
//! instead.
|
//! guide, check out the [book] instead.
|
||||||
//!
|
//!
|
||||||
//! [book]: https://maud.lambda.xyz/
|
//! [book]: https://maud.lambda.xyz/
|
||||||
|
|
||||||
|
|
@ -29,7 +29,8 @@ mod escape;
|
||||||
///
|
///
|
||||||
/// All other characters are passed through unchanged.
|
/// All other characters are passed through unchanged.
|
||||||
///
|
///
|
||||||
/// **Note:** In versions prior to 0.13, the single quote (`'`) was escaped as well.
|
/// **Note:** In versions prior to 0.13, the single quote (`'`) was
|
||||||
|
/// escaped as well.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
|
|
@ -58,14 +59,15 @@ impl fmt::Write for Escaper<'_> {
|
||||||
|
|
||||||
/// Representa un tipo que puede renderizarse como HTML.
|
/// Representa un tipo que puede renderizarse como HTML.
|
||||||
///
|
///
|
||||||
/// To implement this for your own type, override either the `.render()` or `.render_to()` methods;
|
/// To implement this for your own type, override either the `.render()`
|
||||||
/// since each is defined in terms of the other, you only need to implement one of them. See the
|
/// or `.render_to()` methods; since each is defined in terms of the
|
||||||
/// example below.
|
/// other, you only need to implement one of them. See the example below.
|
||||||
///
|
///
|
||||||
/// # Minimal implementation
|
/// # Minimal implementation
|
||||||
///
|
///
|
||||||
/// An implementation of this trait must override at least one of `.render()` or `.render_to()`.
|
/// An implementation of this trait must override at least one of
|
||||||
/// Since the default definitions of these methods call each other, not doing this will result in
|
/// `.render()` or `.render_to()`. Since the default definitions of
|
||||||
|
/// these methods call each other, not doing this will result in
|
||||||
/// infinite recursion.
|
/// infinite recursion.
|
||||||
pub trait Render {
|
pub trait Render {
|
||||||
/// Renders `self` as a block of `Markup`.
|
/// Renders `self` as a block of `Markup`.
|
||||||
|
|
@ -77,12 +79,13 @@ pub trait Render {
|
||||||
|
|
||||||
/// Appends a representation of `self` to the given buffer.
|
/// Appends a representation of `self` to the given buffer.
|
||||||
///
|
///
|
||||||
/// Its default implementation just calls `.render()`, but you may override it with something
|
/// Its default implementation just calls `.render()`, but you may
|
||||||
/// more efficient.
|
/// override it with something more efficient.
|
||||||
///
|
///
|
||||||
/// Note that no further escaping is performed on data written to the buffer. If you override
|
/// Note that no further escaping is performed on data written to
|
||||||
/// this method, you must make sure that any data written is properly escaped, whether by hand
|
/// the buffer. If you override this method, you must make sure that
|
||||||
/// or using the [`Escaper`](struct.Escaper.html) wrapper struct.
|
/// any data written is properly escaped, whether by hand or using
|
||||||
|
/// the [`Escaper`](struct.Escaper.html) wrapper struct.
|
||||||
fn render_to(&self, buffer: &mut String) {
|
fn render_to(&self, buffer: &mut String) {
|
||||||
buffer.push_str(&self.render().into_string());
|
buffer.push_str(&self.render().into_string());
|
||||||
}
|
}
|
||||||
|
|
@ -136,27 +139,6 @@ impl<T: Render + ?Sized> Render for Arc<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Representa un tipo que puede renderizarse como los atributos de un elemento HTML.
|
|
||||||
///
|
|
||||||
/// Exists so that a single value "spliced" into the attribute position of an element can avoid
|
|
||||||
/// duplicating an attribute the element already writes literally. The [`html!`](crate::html::html)
|
|
||||||
/// macro automatically computes the names of the element's literal attributes and passes them here;
|
|
||||||
/// no action is required from the programmer.
|
|
||||||
///
|
|
||||||
/// [`Props`](crate::html::Props) is the only implementation in PageTop.
|
|
||||||
pub trait RenderAttrs {
|
|
||||||
/// Same as [`Render::render_to()`], but omitting any attribute whose name is in `exclude`.
|
|
||||||
#[track_caller]
|
|
||||||
fn render_attrs_to(&self, buffer: &mut String, exclude: &[&str]);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: RenderAttrs + ?Sized> RenderAttrs for &T {
|
|
||||||
#[track_caller]
|
|
||||||
fn render_attrs_to(&self, buffer: &mut String, exclude: &[&str]) {
|
|
||||||
T::render_attrs_to(self, buffer, exclude);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_render_with_display {
|
macro_rules! impl_render_with_display {
|
||||||
($($ty:ty)*) => {
|
($($ty:ty)*) => {
|
||||||
$(
|
$(
|
||||||
|
|
@ -304,7 +286,7 @@ mod axum_support {
|
||||||
pub mod html_private {
|
pub mod html_private {
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
use super::{Render, RenderAttrs, display};
|
use super::{Render, display};
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use core::fmt::Display;
|
use core::fmt::Display;
|
||||||
|
|
||||||
|
|
@ -351,77 +333,4 @@ pub mod html_private {
|
||||||
display(value).render_to(buffer);
|
display(value).render_to(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! render_attrs_to {
|
|
||||||
($x:expr, $exclude:expr, $buffer:expr) => {{
|
|
||||||
use $crate::html::html_private::*;
|
|
||||||
match ChooseAttrsRenderOrDisplay($x) {
|
|
||||||
x => (&&&x)
|
|
||||||
.implements_attrs_render_or_display()
|
|
||||||
.render_to(x.0, $exclude, $buffer),
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use render_attrs_to;
|
|
||||||
|
|
||||||
pub struct ChooseAttrsRenderOrDisplay<T>(pub T);
|
|
||||||
|
|
||||||
pub struct ViaAttrsTag;
|
|
||||||
pub struct ViaAttrsRenderTag;
|
|
||||||
pub struct ViaAttrsDisplayTag;
|
|
||||||
|
|
||||||
pub trait ViaAttrs {
|
|
||||||
fn implements_attrs_render_or_display(&self) -> ViaAttrsTag {
|
|
||||||
ViaAttrsTag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub trait ViaAttrsRender {
|
|
||||||
fn implements_attrs_render_or_display(&self) -> ViaAttrsRenderTag {
|
|
||||||
ViaAttrsRenderTag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub trait ViaAttrsDisplay {
|
|
||||||
fn implements_attrs_render_or_display(&self) -> ViaAttrsDisplayTag {
|
|
||||||
ViaAttrsDisplayTag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: RenderAttrs> ViaAttrs for &&ChooseAttrsRenderOrDisplay<T> {}
|
|
||||||
impl<T: Render> ViaAttrsRender for &ChooseAttrsRenderOrDisplay<T> {}
|
|
||||||
impl<T: Display> ViaAttrsDisplay for ChooseAttrsRenderOrDisplay<T> {}
|
|
||||||
|
|
||||||
impl ViaAttrsTag {
|
|
||||||
#[track_caller]
|
|
||||||
pub fn render_to<T: RenderAttrs + ?Sized>(
|
|
||||||
self,
|
|
||||||
value: &T,
|
|
||||||
exclude: &[&str],
|
|
||||||
buffer: &mut String,
|
|
||||||
) {
|
|
||||||
value.render_attrs_to(buffer, exclude);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl ViaAttrsRenderTag {
|
|
||||||
pub fn render_to<T: Render + ?Sized>(
|
|
||||||
self,
|
|
||||||
value: &T,
|
|
||||||
_exclude: &[&str],
|
|
||||||
buffer: &mut String,
|
|
||||||
) {
|
|
||||||
value.render_to(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl ViaAttrsDisplayTag {
|
|
||||||
pub fn render_to<T: Display + ?Sized>(
|
|
||||||
self,
|
|
||||||
value: &T,
|
|
||||||
_exclude: &[&str],
|
|
||||||
buffer: &mut String,
|
|
||||||
) {
|
|
||||||
display(value).render_to(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
use crate::html::maud::{Escaper, RenderAttrs};
|
use crate::html::maud::{Escaper, Render};
|
||||||
use crate::{AutoDefault, CowStr, builder_fn, trace, util};
|
use crate::{AutoDefault, CowStr, builder_fn, trace, util};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
@ -7,7 +7,6 @@ use thiserror::Error;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{self, Write};
|
use std::fmt::{self, Write};
|
||||||
use std::panic::Location;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// **< PropsExtra >*********************************************************************************
|
// **< PropsExtra >*********************************************************************************
|
||||||
|
|
@ -100,19 +99,11 @@ pub enum PropsOp {
|
||||||
/// Añade la clase o clases que no existan al principio de la lista. La operación se ignora si
|
/// Añade la clase o clases que no existan al principio de la lista. La operación se ignora si
|
||||||
/// el valor contiene caracteres no ASCII.
|
/// el valor contiene caracteres no ASCII.
|
||||||
PrependClasses(CowStr),
|
PrependClasses(CowStr),
|
||||||
/// Sustituye **una o más** clases del primer valor por las clases indicadas en el segundo
|
/// Sustituye una o varias clases existentes (primer valor) por las clases indicadas (segundo
|
||||||
/// valor, insertando las nuevas en la posición de la primera clase a sustituir encontrada, con
|
/// valor), insertando las nuevas en la posición de la primera clase sustituida encontrada. Si
|
||||||
/// independencia del orden en que aparecen en el primer valor. Las que no existan se ignoran.
|
/// ninguna de las clases a sustituir existe, la operación no tiene efecto. Se ignora si alguno
|
||||||
/// Si **ninguna** de las clases a sustituir existe, la operación no tiene efecto y no se
|
/// de los dos valores contiene caracteres no ASCII.
|
||||||
/// inserta nada. Se ignora si alguno de los dos valores contiene caracteres no ASCII.
|
|
||||||
ReplaceClasses(CowStr, CowStr),
|
ReplaceClasses(CowStr, CowStr),
|
||||||
/// A diferencia de [`ReplaceClasses`](Self::ReplaceClasses), exige que **todas** las clases del
|
|
||||||
/// primer valor estén presentes, independientemente de su orden; si falta una sola, la
|
|
||||||
/// operación no tiene efecto: ninguna clase se elimina ni se inserta. Si todas están presentes,
|
|
||||||
/// las sustituye por las clases indicadas en el segundo valor, insertando las nuevas en la
|
|
||||||
/// posición de la primera clase a sustituir encontrada. Se ignora si alguno de los dos valores
|
|
||||||
/// contiene caracteres no ASCII.
|
|
||||||
ReplaceAllClasses(CowStr, CowStr),
|
|
||||||
/// Elimina la clase o clases indicadas de la lista. La operación se ignora si el valor contiene
|
/// Elimina la clase o clases indicadas de la lista. La operación se ignora si el valor contiene
|
||||||
/// caracteres no ASCII.
|
/// caracteres no ASCII.
|
||||||
RemoveClasses(CowStr),
|
RemoveClasses(CowStr),
|
||||||
|
|
@ -178,34 +169,11 @@ impl PropsOp {
|
||||||
/// let props = Props::classes("button primary")
|
/// let props = Props::classes("button primary")
|
||||||
/// .with_prop(PropsOp::replace_classes("button", "btn"));
|
/// .with_prop(PropsOp::replace_classes("button", "btn"));
|
||||||
/// assert_eq!(props.get_classes(), Some("btn primary".to_string()));
|
/// assert_eq!(props.get_classes(), Some("btn primary".to_string()));
|
||||||
///
|
|
||||||
/// // Basta con que exista alguna clase de `old` para aplicar el reemplazo.
|
|
||||||
/// let props = Props::classes("btn primary")
|
|
||||||
/// .with_prop(PropsOp::replace_classes("primary secondary", "danger"));
|
|
||||||
/// assert_eq!(props.get_classes(), Some("btn danger".to_string()));
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn replace_classes(old: impl Into<CowStr>, new: impl Into<CowStr>) -> Self {
|
pub fn replace_classes(old: impl Into<CowStr>, new: impl Into<CowStr>) -> Self {
|
||||||
Self::ReplaceClasses(old.into(), new.into())
|
Self::ReplaceClasses(old.into(), new.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea la variante [`ReplaceAllClasses`](Self::ReplaceAllClasses) con las clases a sustituir
|
|
||||||
/// (`old`) y las nuevas clases (`new`).
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// let props = Props::classes("btn primary")
|
|
||||||
/// .with_prop(PropsOp::replace_all_classes("btn primary", "btn danger"));
|
|
||||||
/// assert_eq!(props.get_classes(), Some("btn danger".to_string()));
|
|
||||||
///
|
|
||||||
/// // Si falta una sola clase de `old`, no hay reemplazo.
|
|
||||||
/// let props = Props::classes("btn primary")
|
|
||||||
/// .with_prop(PropsOp::replace_all_classes("primary secondary", "danger"));
|
|
||||||
/// assert_eq!(props.get_classes(), Some("btn primary".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub fn replace_all_classes(old: impl Into<CowStr>, new: impl Into<CowStr>) -> Self {
|
|
||||||
Self::ReplaceAllClasses(old.into(), new.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea la variante [`RemoveClasses`](Self::RemoveClasses) con la clase o clases indicadas.
|
/// Crea la variante [`RemoveClasses`](Self::RemoveClasses) con la clase o clases indicadas.
|
||||||
pub fn remove_classes(classes: impl Into<CowStr>) -> Self {
|
pub fn remove_classes(classes: impl Into<CowStr>) -> Self {
|
||||||
Self::RemoveClasses(classes.into())
|
Self::RemoveClasses(classes.into())
|
||||||
|
|
@ -351,24 +319,6 @@ impl PropsOp {
|
||||||
/// assert_eq!(markup.into_string(), r#"<button style="color: blue">OK</button>"#);
|
/// assert_eq!(markup.into_string(), r#"<button style="color: blue">OK</button>"#);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Atributos duplicados junto a `Props`
|
|
||||||
///
|
|
||||||
/// Cuando el componente combina `(self.props())` con un atributo literal del mismo nombre en el
|
|
||||||
/// mismo elemento (una clase, un `#id`, o `nombre=valor`), la macro [`html!`](crate::html::html)
|
|
||||||
/// evita automáticamente la duplicación. Recopila en tiempo de compilación los nombres de los
|
|
||||||
/// atributos del elemento y al renderizar se omiten los duplicados en tiempo de ejecución. No
|
|
||||||
/// depende del orden en que se escriban ni requiere ninguna acción del desarrollador.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// let props = Props::default().with_prop(PropsOp::set("title", "de Props"));
|
|
||||||
///
|
|
||||||
/// let markup = html! { span title="literal" (props) { "OK" } };
|
|
||||||
///
|
|
||||||
/// // El atributo literal prevalece; `Props` omite su propio "title" en vez de duplicarlo.
|
|
||||||
/// assert_eq!(markup.into_string(), r#"<span title="literal">OK</span>"#);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Valores extra
|
/// # Valores extra
|
||||||
///
|
///
|
||||||
/// Las variantes [`SetExtra`](PropsOp::SetExtra) y [`RemoveExtra`](PropsOp::RemoveExtra), usando
|
/// Las variantes [`SetExtra`](PropsOp::SetExtra) y [`RemoveExtra`](PropsOp::RemoveExtra), usando
|
||||||
|
|
@ -512,27 +462,6 @@ impl Props {
|
||||||
self.insert_classes(new.as_ref().split_ascii_whitespace(), pos);
|
self.insert_classes(new.as_ref().split_ascii_whitespace(), pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PropsOp::ReplaceAllClasses(old, new) => {
|
|
||||||
let Some(old) = util::normalize_ascii_or_empty(old.as_ref(), "Props::with_prop")
|
|
||||||
else {
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
let Some(new) = util::normalize_ascii_or_empty(new.as_ref(), "Props::with_prop")
|
|
||||||
else {
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
if !self.has_all_classes(old.as_ref()) {
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
let mut pos = self.classes.len();
|
|
||||||
for class in old.as_ref().split_ascii_whitespace() {
|
|
||||||
if let Some(replace_pos) = self.classes.iter().position(|c| c == class) {
|
|
||||||
self.classes.remove(replace_pos);
|
|
||||||
pos = pos.min(replace_pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.insert_classes(new.as_ref().split_ascii_whitespace(), pos);
|
|
||||||
}
|
|
||||||
PropsOp::RemoveClasses(classes) => {
|
PropsOp::RemoveClasses(classes) => {
|
||||||
let Some(normalized) =
|
let Some(normalized) =
|
||||||
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
|
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
|
||||||
|
|
@ -687,19 +616,8 @@ impl Props {
|
||||||
&& self.attrs.is_empty()
|
&& self.attrs.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente.
|
|
||||||
pub fn has_classes(&self, classes: impl AsRef<str>) -> bool {
|
|
||||||
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
normalized
|
|
||||||
.as_ref()
|
|
||||||
.split_ascii_whitespace()
|
|
||||||
.any(|class| self.classes.iter().any(|c| c == class))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
|
/// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
|
||||||
pub fn has_all_classes(&self, classes: impl AsRef<str>) -> bool {
|
pub fn has_class(&self, classes: impl AsRef<str>) -> bool {
|
||||||
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
@ -709,6 +627,17 @@ impl Props {
|
||||||
.all(|class| self.classes.iter().any(|c| c == class))
|
.all(|class| self.classes.iter().any(|c| c == class))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente.
|
||||||
|
pub fn has_any_class(&self, classes: impl AsRef<str>) -> bool {
|
||||||
|
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
normalized
|
||||||
|
.as_ref()
|
||||||
|
.split_ascii_whitespace()
|
||||||
|
.any(|class| self.classes.iter().any(|c| c == class))
|
||||||
|
}
|
||||||
|
|
||||||
/// Recupera una referencia tipada al valor extra asociado a la clave `key`.
|
/// Recupera una referencia tipada al valor extra asociado a la clave `key`.
|
||||||
///
|
///
|
||||||
/// Devuelve un [`Result`] que indica si la clave existe y si el tipo coincide:
|
/// Devuelve un [`Result`] que indica si la clave existe y si el tipo coincide:
|
||||||
|
|
@ -900,37 +829,14 @@ impl Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
impl RenderAttrs for Props {
|
impl Render for Props {
|
||||||
// Omite cualquier atributo que esté en `exclude` (recopilados por `html!` a partir de los
|
fn render_to(&self, w: &mut String) {
|
||||||
// atributos literales del elemento). Registra un `trace::debug!` por cada atributo duplicado,
|
|
||||||
// con la posición exacta del `html!` que lo produjo (propagado gracias a `#[track_caller]`)
|
|
||||||
// para facilitar la localización del problema.
|
|
||||||
#[track_caller]
|
|
||||||
fn render_attrs_to(&self, w: &mut String, exclude: &[&str]) {
|
|
||||||
if let Some(id) = self.id.as_deref() {
|
if let Some(id) = self.id.as_deref() {
|
||||||
if exclude.contains(&"id") {
|
|
||||||
trace::debug!(
|
|
||||||
caller = %Location::caller(),
|
|
||||||
attribute = "id",
|
|
||||||
discarded = %id,
|
|
||||||
"Ignoring Props attribute already set as a literal on the same element"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
w.push_str(" id=\"");
|
w.push_str(" id=\"");
|
||||||
let _ = write!(Escaper::new(w), "{}", id);
|
let _ = write!(Escaper::new(w), "{}", id);
|
||||||
w.push('"');
|
w.push('"');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some((first, rest)) = self.classes.split_first() {
|
if let Some((first, rest)) = self.classes.split_first() {
|
||||||
if exclude.contains(&"class") {
|
|
||||||
trace::debug!(
|
|
||||||
caller = %Location::caller(),
|
|
||||||
attribute = "class",
|
|
||||||
discarded = %self.classes.join(" "),
|
|
||||||
id = %self.id.as_deref().unwrap_or("<none>"),
|
|
||||||
"Ignoring Props attribute already set as a literal on the same element"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
w.push_str(" class=\"");
|
w.push_str(" class=\"");
|
||||||
let _ = write!(Escaper::new(w), "{}", first);
|
let _ = write!(Escaper::new(w), "{}", first);
|
||||||
for class in rest {
|
for class in rest {
|
||||||
|
|
@ -939,23 +845,7 @@ impl RenderAttrs for Props {
|
||||||
}
|
}
|
||||||
w.push('"');
|
w.push('"');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some((first, rest)) = self.styles.split_first() {
|
if let Some((first, rest)) = self.styles.split_first() {
|
||||||
if exclude.contains(&"style") {
|
|
||||||
let discarded = self
|
|
||||||
.styles
|
|
||||||
.iter()
|
|
||||||
.map(|(property, value)| format!("{property}: {value}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("; ");
|
|
||||||
trace::debug!(
|
|
||||||
caller = %Location::caller(),
|
|
||||||
attribute = "style",
|
|
||||||
discarded = %discarded,
|
|
||||||
id = %self.id.as_deref().unwrap_or("<none>"),
|
|
||||||
"Ignoring Props attribute already set as a literal on the same element"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
w.push_str(" style=\"");
|
w.push_str(" style=\"");
|
||||||
let _ = write!(Escaper::new(w), "{}: {}", first.0, first.1);
|
let _ = write!(Escaper::new(w), "{}: {}", first.0, first.1);
|
||||||
for (property, value) in rest {
|
for (property, value) in rest {
|
||||||
|
|
@ -964,18 +854,7 @@ impl RenderAttrs for Props {
|
||||||
}
|
}
|
||||||
w.push('"');
|
w.push('"');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for (name, value) in &self.attrs {
|
for (name, value) in &self.attrs {
|
||||||
if exclude.contains(&name.as_ref()) {
|
|
||||||
trace::debug!(
|
|
||||||
caller = %Location::caller(),
|
|
||||||
attribute = %name,
|
|
||||||
discarded = %value,
|
|
||||||
id = %self.id.as_deref().unwrap_or("<none>"),
|
|
||||||
"Ignoring Props attribute already set as a literal on the same element"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
w.push(' ');
|
w.push(' ');
|
||||||
let _ = write!(Escaper::new(w), "{}", name);
|
let _ = write!(Escaper::new(w), "{}", name);
|
||||||
w.push_str("=\"");
|
w.push_str("=\"");
|
||||||
|
|
|
||||||
|
|
@ -157,13 +157,11 @@ async fn props_alongside_named_attr_renders_after_it() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_combined_via_chaining_instead_of_multiple_splices() {
|
async fn props_multiple_splices_in_same_element() {
|
||||||
// An element accepts only a single attribute splice (a second `(props)` on the same element
|
let p1 = Props::new("hx-get", "/api");
|
||||||
// is a compile error); values from separate sources are combined by chaining `with_prop()`
|
let p2 = Props::new("hx-swap", "outerHTML");
|
||||||
// on one `Props`, not by splicing two of them.
|
|
||||||
let p = Props::new("hx-get", "/api").with_prop(PropsOp::set("hx-swap", "outerHTML"));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p) {} }.into_string(),
|
html! { button (p1) (p2) {} }.into_string(),
|
||||||
r#"<button hx-get="/api" hx-swap="outerHTML"></button>"#
|
r#"<button hx-get="/api" hx-swap="outerHTML"></button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -195,48 +193,6 @@ async fn props_splice_empty_string_emits_nothing() {
|
||||||
assert_eq!(html! { span ("") { "x" } }.into_string(), "<span>x</span>");
|
assert_eq!(html! { span ("") { "x" } }.into_string(), "<span>x</span>");
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< RenderAttrs: literal attribute collisions >**************************************************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn props_id_collision_with_literal_omits_props_id() {
|
|
||||||
// A literal `#id` on the element takes precedence; `Props`'s own id is silently omitted instead
|
|
||||||
// of producing a duplicate `id` attribute.
|
|
||||||
let p = Props::default().with_id("from-props");
|
|
||||||
assert_eq!(
|
|
||||||
html! { div #fixed (p) {} }.into_string(),
|
|
||||||
r#"<div id="fixed"></div>"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn props_class_collision_with_literal_omits_props_classes() {
|
|
||||||
let p = Props::classes("from-props-a from-props-b");
|
|
||||||
assert_eq!(
|
|
||||||
html! { div.fixed (p) {} }.into_string(),
|
|
||||||
r#"<div class="fixed"></div>"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn props_style_collision_with_literal_omits_props_styles() {
|
|
||||||
let p = Props::default()
|
|
||||||
.with_prop(PropsOp::add_style("color", "red"))
|
|
||||||
.with_prop(PropsOp::add_style("font-weight", "bold"));
|
|
||||||
assert_eq!(
|
|
||||||
html! { div style="color: blue" (p) {} }.into_string(),
|
|
||||||
r#"<div style="color: blue"></div>"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn props_named_attr_collision_with_literal_omits_props_value() {
|
|
||||||
let p = Props::default().with_prop(PropsOp::set("title", "from-props"));
|
|
||||||
assert_eq!(
|
|
||||||
html! { span title="literal" (p) {} }.into_string(),
|
|
||||||
r#"<span title="literal"></span>"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< is_attrs_empty / is_empty >******************************************************************
|
// **< is_attrs_empty / is_empty >******************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ async fn classes_new_empty_and_whitespace_is_empty() {
|
||||||
async fn classes_new_normalizes_and_dedups_and_preserves_first_occurrence_order() {
|
async fn classes_new_normalizes_and_dedups_and_preserves_first_occurrence_order() {
|
||||||
let p = Props::classes("Btn btn BTN btn-primary BTN-PRIMARY");
|
let p = Props::classes("Btn btn BTN btn-primary BTN-PRIMARY");
|
||||||
assert_classes(&p, Some("btn btn-primary"));
|
assert_classes(&p, Some("btn btn-primary"));
|
||||||
assert!(p.has_all_classes("BTN"));
|
assert!(p.has_class("BTN"));
|
||||||
assert!(p.has_all_classes("btn-primary"));
|
assert!(p.has_class("btn-primary"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< PropsOp::add_classes >***********************************************************************
|
// **< PropsOp::add_classes >***********************************************************************
|
||||||
|
|
@ -119,57 +119,6 @@ async fn classes_replace_rejects_non_ascii_targets_is_noop() {
|
||||||
assert_classes(&p, Some("a b c"));
|
assert_classes(&p, Some("a b c"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< PropsOp::replace_all_classes >***************************************************************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn classes_replace_all_removes_targets_and_inserts_new_at_min_position() {
|
|
||||||
let p = Props::classes("a b c d").with_prop(PropsOp::replace_all_classes("c a", "x y"));
|
|
||||||
assert_classes(&p, Some("x y b d"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn classes_replace_all_when_missing_one_does_nothing_even_to_existing_one() {
|
|
||||||
let p = Props::classes("a b").with_prop(PropsOp::replace_all_classes("a x", "c d"));
|
|
||||||
assert_classes(&p, Some("a b"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn classes_replace_all_when_none_found_does_nothing() {
|
|
||||||
let p = Props::classes("a b").with_prop(PropsOp::replace_all_classes("x y", "c d"));
|
|
||||||
assert_classes(&p, Some("a b"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn classes_replace_all_is_case_insensitive_on_targets_and_new_values_are_normalized() {
|
|
||||||
let p = Props::classes("btn btn-primary active")
|
|
||||||
.with_prop(PropsOp::replace_all_classes("BTN ACTIVE", "Btn-Secondary"));
|
|
||||||
assert_classes(&p, Some("btn-secondary btn-primary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn classes_replace_all_with_empty_new_removes_only() {
|
|
||||||
let p = Props::classes("a b c").with_prop(PropsOp::replace_all_classes("a b", " "));
|
|
||||||
assert_classes(&p, Some("c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn classes_replace_all_dedups_against_existing_items() {
|
|
||||||
let p = Props::classes("a b c").with_prop(PropsOp::replace_all_classes("a b", "c d"));
|
|
||||||
assert_classes(&p, Some("d c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn classes_replace_all_ignores_target_whitespace_and_repetition() {
|
|
||||||
let p = Props::classes("a b c").with_prop(PropsOp::replace_all_classes(" b b ", "x y"));
|
|
||||||
assert_classes(&p, Some("a x y c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn classes_replace_all_rejects_non_ascii_targets_is_noop() {
|
|
||||||
let p = Props::classes("a b c").with_prop(PropsOp::replace_all_classes("b ñ", "x"));
|
|
||||||
assert_classes(&p, Some("a b c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< PropsOp::set / remove ("class") >************************************************************
|
// **< PropsOp::set / remove ("class") >************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -216,41 +165,41 @@ async fn classes_remove_with_extra_whitespace() {
|
||||||
assert_classes(&p, Some("a c"));
|
assert_classes(&p, Some("a c"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< has_classes / has_all_classes >**************************************************************
|
// **< has_class / has_any_class >******************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_contains_single() {
|
async fn classes_contains_single() {
|
||||||
let p = Props::classes("btn btn-primary");
|
let p = Props::classes("btn btn-primary");
|
||||||
assert!(p.has_all_classes("btn"));
|
assert!(p.has_class("btn"));
|
||||||
assert!(p.has_all_classes("BTN"));
|
assert!(p.has_class("BTN"));
|
||||||
assert!(!p.has_all_classes("missing"));
|
assert!(!p.has_class("missing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_contains_all_and_any() {
|
async fn classes_contains_all_and_any() {
|
||||||
let p = Props::classes("btn btn-primary active");
|
let p = Props::classes("btn btn-primary active");
|
||||||
assert!(p.has_classes("missing active"));
|
assert!(p.has_class("btn active"));
|
||||||
assert!(p.has_classes("BTN-PRIMARY missing"));
|
assert!(p.has_class("BTN BTN-PRIMARY"));
|
||||||
assert!(!p.has_classes("missing other"));
|
assert!(!p.has_class("btn missing"));
|
||||||
assert!(p.has_all_classes("btn active"));
|
assert!(p.has_any_class("missing active"));
|
||||||
assert!(p.has_all_classes("BTN BTN-PRIMARY"));
|
assert!(p.has_any_class("BTN-PRIMARY missing"));
|
||||||
assert!(!p.has_all_classes("btn missing"));
|
assert!(!p.has_any_class("missing other"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_contains_empty_and_whitespace_is_false() {
|
async fn classes_contains_empty_and_whitespace_is_false() {
|
||||||
let p = Props::classes("a b");
|
let p = Props::classes("a b");
|
||||||
assert!(!p.has_classes(""));
|
assert!(!p.has_class(""));
|
||||||
assert!(!p.has_classes(" \n "));
|
assert!(!p.has_class(" \t"));
|
||||||
assert!(!p.has_all_classes(""));
|
assert!(!p.has_any_class(""));
|
||||||
assert!(!p.has_all_classes(" \t"));
|
assert!(!p.has_any_class(" \n "));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_contains_non_ascii_is_false() {
|
async fn classes_contains_non_ascii_is_false() {
|
||||||
let p = Props::classes("a b");
|
let p = Props::classes("a b");
|
||||||
assert!(!p.has_classes("a ñ"));
|
assert!(!p.has_class("ñ"));
|
||||||
assert!(!p.has_all_classes("ñ"));
|
assert!(!p.has_any_class("a ñ"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< is_classes_empty >***************************************************************************
|
// **< is_classes_empty >***************************************************************************
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue