✨ (html): Evita duplicar atributos de Props
`html!` recopila los atributos literales de cada elemento y evita que `Props` los duplique al renderizar.
This commit is contained in:
parent
05187c9343
commit
745d492d5b
5 changed files with 274 additions and 42 deletions
|
|
@ -1,7 +1,9 @@
|
|||
//! HTML en código.
|
||||
|
||||
pub(crate) mod maud;
|
||||
pub use maud::{DOCTYPE, Escaper, Markup, PreEscaped, Render, display, html, html_private};
|
||||
pub use maud::DOCTYPE;
|
||||
pub use maud::{Escaper, Markup, PreEscaped, Render, RenderAttrs};
|
||||
pub use maud::{display, html, html_private};
|
||||
|
||||
mod route_path;
|
||||
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.
|
||||
//!
|
||||
//! This documentation only describes the runtime API. For a general
|
||||
//! guide, check out the [book] instead.
|
||||
//! This documentation only describes the runtime API. For a general guide, check out the [book]
|
||||
//! instead.
|
||||
//!
|
||||
//! [book]: https://maud.lambda.xyz/
|
||||
|
||||
|
|
@ -29,8 +29,7 @@ mod escape;
|
|||
///
|
||||
/// 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
|
||||
///
|
||||
|
|
@ -59,15 +58,14 @@ impl fmt::Write for Escaper<'_> {
|
|||
|
||||
/// Representa un tipo que puede renderizarse como HTML.
|
||||
///
|
||||
/// To implement this for your own type, override either the `.render()`
|
||||
/// or `.render_to()` methods; since each is defined in terms of the
|
||||
/// other, you only need to implement one of them. See the example below.
|
||||
/// To implement this for your own type, override either the `.render()` or `.render_to()` methods;
|
||||
/// since each is defined in terms of the other, you only need to implement one of them. See the
|
||||
/// example below.
|
||||
///
|
||||
/// # Minimal implementation
|
||||
///
|
||||
/// An implementation of this trait must override at least one of
|
||||
/// `.render()` or `.render_to()`. Since the default definitions of
|
||||
/// these methods call each other, not doing this will result in
|
||||
/// An implementation of this trait must override at least one of `.render()` or `.render_to()`.
|
||||
/// Since the default definitions of these methods call each other, not doing this will result in
|
||||
/// infinite recursion.
|
||||
pub trait Render {
|
||||
/// Renders `self` as a block of `Markup`.
|
||||
|
|
@ -79,13 +77,12 @@ pub trait Render {
|
|||
|
||||
/// Appends a representation of `self` to the given buffer.
|
||||
///
|
||||
/// Its default implementation just calls `.render()`, but you may
|
||||
/// override it with something more efficient.
|
||||
/// Its default implementation just calls `.render()`, but you may override it with something
|
||||
/// more efficient.
|
||||
///
|
||||
/// Note that no further escaping is performed on data written to
|
||||
/// the buffer. If you override this method, you must make sure that
|
||||
/// any data written is properly escaped, whether by hand or using
|
||||
/// the [`Escaper`](struct.Escaper.html) wrapper struct.
|
||||
/// Note that no further escaping is performed on data written to the buffer. If you override
|
||||
/// this method, you must make sure that 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) {
|
||||
buffer.push_str(&self.render().into_string());
|
||||
}
|
||||
|
|
@ -139,6 +136,27 @@ 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 {
|
||||
($($ty:ty)*) => {
|
||||
$(
|
||||
|
|
@ -286,7 +304,7 @@ mod axum_support {
|
|||
pub mod html_private {
|
||||
extern crate alloc;
|
||||
|
||||
use super::{Render, display};
|
||||
use super::{Render, RenderAttrs, display};
|
||||
use alloc::string::String;
|
||||
use core::fmt::Display;
|
||||
|
||||
|
|
@ -333,4 +351,77 @@ pub mod html_private {
|
|||
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::html::maud::{Escaper, Render};
|
||||
use crate::html::maud::{Escaper, RenderAttrs};
|
||||
use crate::{AutoDefault, CowStr, builder_fn, trace, util};
|
||||
|
||||
use thiserror::Error;
|
||||
|
|
@ -7,6 +7,7 @@ use thiserror::Error;
|
|||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Write};
|
||||
use std::panic::Location;
|
||||
use std::sync::Arc;
|
||||
|
||||
// **< PropsExtra >*********************************************************************************
|
||||
|
|
@ -350,6 +351,24 @@ impl PropsOp {
|
|||
/// 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
|
||||
///
|
||||
/// Las variantes [`SetExtra`](PropsOp::SetExtra) y [`RemoveExtra`](PropsOp::RemoveExtra), usando
|
||||
|
|
@ -881,32 +900,82 @@ impl Props {
|
|||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl Render for Props {
|
||||
fn render_to(&self, w: &mut String) {
|
||||
impl RenderAttrs for Props {
|
||||
// Omite cualquier atributo que esté en `exclude` (recopilados por `html!` a partir de los
|
||||
// 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() {
|
||||
w.push_str(" id=\"");
|
||||
let _ = write!(Escaper::new(w), "{}", id);
|
||||
w.push('"');
|
||||
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=\"");
|
||||
let _ = write!(Escaper::new(w), "{}", id);
|
||||
w.push('"');
|
||||
}
|
||||
}
|
||||
if let Some((first, rest)) = self.classes.split_first() {
|
||||
w.push_str(" class=\"");
|
||||
let _ = write!(Escaper::new(w), "{}", first);
|
||||
for class in rest {
|
||||
w.push(' ');
|
||||
let _ = write!(Escaper::new(w), "{}", class);
|
||||
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=\"");
|
||||
let _ = write!(Escaper::new(w), "{}", first);
|
||||
for class in rest {
|
||||
w.push(' ');
|
||||
let _ = write!(Escaper::new(w), "{}", class);
|
||||
}
|
||||
w.push('"');
|
||||
}
|
||||
w.push('"');
|
||||
}
|
||||
if let Some((first, rest)) = self.styles.split_first() {
|
||||
w.push_str(" style=\"");
|
||||
let _ = write!(Escaper::new(w), "{}: {}", first.0, first.1);
|
||||
for (property, value) in rest {
|
||||
w.push_str("; ");
|
||||
let _ = write!(Escaper::new(w), "{}: {}", property, value);
|
||||
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=\"");
|
||||
let _ = write!(Escaper::new(w), "{}: {}", first.0, first.1);
|
||||
for (property, value) in rest {
|
||||
w.push_str("; ");
|
||||
let _ = write!(Escaper::new(w), "{}: {}", property, value);
|
||||
}
|
||||
w.push('"');
|
||||
}
|
||||
w.push('"');
|
||||
}
|
||||
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(' ');
|
||||
let _ = write!(Escaper::new(w), "{}", name);
|
||||
w.push_str("=\"");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue