use crate::core::TypeInfo;
use crate::html::maud::{Escaper, Render};
use crate::{AutoDefault, CowStr, builder_fn, util};
use std::any::Any;
use std::collections::HashMap;
use std::fmt::{self, Write};
use std::sync::Arc;
// **< PropsExtra >*********************************************************************************
/// Encapsula un valor tipado extra para almacenar en [`Props`].
///
/// Internamente usa [`Arc`] para que [`Props`] pueda implementar [`Clone`] sin requerir que los
/// valores almacenados sean clonables. El nombre del tipo almacenado permite generar mensajes de
/// error precisos.
pub struct PropsExtra {
value: Arc,
type_name: &'static str,
}
impl Clone for PropsExtra {
fn clone(&self) -> Self {
Self {
value: Arc::clone(&self.value),
type_name: self.type_name,
}
}
}
impl fmt::Debug for PropsExtra {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<{}>", self.type_name)
}
}
// **< PropsError >*********************************************************************************
/// Errores de acceso a valores extra de [`Props`].
///
/// - [`PropsError::ExtraNotFound`]: la clave no existe. Incluye la clave (`key`).
/// - [`PropsError::ExtraTypeMismatch`]: la clave existe pero el tipo solicitado no coincide con el
/// almacenado. Incluye la clave (`key`), tipo esperado (`expected`) y tipo realmente encontrado
/// (`found`) para facilitar el diagnóstico.
#[derive(Debug, PartialEq, Eq)]
pub enum PropsError {
ExtraNotFound {
key: &'static str,
},
ExtraTypeMismatch {
key: &'static str,
expected: &'static str,
found: &'static str,
},
}
impl fmt::Display for PropsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PropsError::ExtraNotFound { key } => write!(f, "extra \"{key}\" not found"),
PropsError::ExtraTypeMismatch {
key,
expected,
found,
} => write!(
f,
"type mismatch for extra \"{key}\": expected \"{expected}\", found \"{found}\""
),
}
}
}
impl std::error::Error for PropsError {}
// **< PropsOp >************************************************************************************
/// Operaciones sobre el identificador, clases CSS, atributos HTML y valores extra en [`Props`].
///
/// Cada variante lleva los datos necesarios para ejecutarse. El método recomendado para usarlas es
/// recurrir a los constructores asociados como [`set_id()`](Self::set_id),
/// [`add_classes()`](Self::add_classes), [`set()`](Self::set), etc.
///
/// Las variantes `*Id` operan sobre el atributo `id` del componente. Cuando se usa `"id"` como
/// nombre de atributo en `Set`, el valor se normaliza igual que [`SetId`](Self::SetId).
///
/// Las variantes `*Classes` gestionan la lista de clases CSS. Además, `Set("class", ...)`
/// reemplaza la lista completa y `Remove("class")` la vacía.
///
/// Las variantes [`Set`](Self::Set) y [`Remove`](Self::Remove) son operaciones de propósito
/// general: `Set` añade o reemplaza cualquier atributo HTML por nombre y valor, y `Remove` lo
/// elimina. Los atributos `id` y `class` tienen semántica especial documentada en cada variante.
///
/// Las variantes `*Extra` permiten añadir valores tipados usando una clave. Están pensadas para que
/// temas y extensiones amplíen el comportamiento de componentes ya existentes. Como no es posible
/// añadir campos a la estructura de un componente ya definido, temas y extensiones pueden definir
/// un *trait* con nuevos métodos que leen y escriben valores extra en [`Props`]. Esos valores se
/// interpretan como si fueran valores internos del componente para tomar decisiones durante el
/// renderizado.
#[derive(Clone, Debug)]
pub enum PropsOp {
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
/// a minúsculas y sustituye los espacios intermedios por `_`. Si el resultado es vacío, elimina
/// el identificador.
SetId(CowStr),
/// Establece el identificador del componente **sólo si aún no hay ninguno definido**. Aplica la
/// misma normalización que [`SetId`](Self::SetId); si el resultado es vacío, la operación no
/// tiene efecto.
EnsureId(CowStr),
/// Añade la clase o clases que no existan al final de la lista. La operación se ignora si el
/// valor contiene caracteres no ASCII.
AddClasses(CowStr),
/// 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.
PrependClasses(CowStr),
/// 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 sustituida encontrada. Si
/// ninguna de las clases a sustituir existe, la operación no tiene efecto. Se ignora si alguno
/// de los dos valores contiene caracteres no ASCII.
ReplaceClasses(CowStr, CowStr),
/// Elimina la clase o clases indicadas de la lista. La operación se ignora si el valor contiene
/// caracteres no ASCII.
RemoveClasses(CowStr),
/// Añade un atributo o sustituye su valor si ya existe. Usar `"id"` como nombre de atributo
/// aplica al valor la misma normalización que [`SetId`](Self::SetId). Usar `"class"` como
/// nombre de atributo reemplaza la lista completa de clases por las nuevas indicadas; la
/// operación se ignora si el valor contiene caracteres no ASCII.
Set(CowStr, CowStr),
/// Elimina el atributo indicado. Usar `"id"` elimina el identificador; usar `"class"` vacía la
/// lista de clases.
Remove(CowStr),
/// Almacena un valor extra tipado asociado a la clave indicada. Si ya existe uno con esa clave,
/// lo reemplaza.
SetExtra(&'static str, PropsExtra),
/// Elimina el valor extra asociado a la clave indicada, si existe.
RemoveExtra(&'static str),
}
impl PropsOp {
/// Crea la variante [`SetId`](Self::SetId) con el identificador indicado.
pub fn set_id(id: impl Into) -> Self {
Self::SetId(id.into())
}
/// Crea la variante [`EnsureId`](Self::EnsureId) con el identificador indicado.
pub fn ensure_id(id: impl Into) -> Self {
Self::EnsureId(id.into())
}
/// Crea la variante [`AddClasses`](Self::AddClasses) con la clase o clases indicadas.
pub fn add_classes(classes: impl Into) -> Self {
Self::AddClasses(classes.into())
}
/// Crea la variante [`PrependClasses`](Self::PrependClasses) con la clase o clases indicadas.
pub fn prepend_classes(classes: impl Into) -> Self {
Self::PrependClasses(classes.into())
}
/// Crea la variante [`ReplaceClasses`](Self::ReplaceClasses) con las clases a sustituir (`old`)
/// y las nuevas clases (`new`).
///
/// ```rust
/// # use pagetop::prelude::*;
/// let props = Props::classes("button primary")
/// .with_prop(PropsOp::replace_classes("button", "btn"));
/// assert_eq!(props.get_classes(), Some("btn primary".to_string()));
/// ```
pub fn replace_classes(old: impl Into, new: impl Into) -> Self {
Self::ReplaceClasses(old.into(), new.into())
}
/// Crea la variante [`RemoveClasses`](Self::RemoveClasses) con la clase o clases indicadas.
pub fn remove_classes(classes: impl Into) -> Self {
Self::RemoveClasses(classes.into())
}
/// Crea la variante [`Set`](Self::Set) con nombre y valor del atributo.
pub fn set(name: impl Into, value: impl Into) -> Self {
Self::Set(name.into(), value.into())
}
/// Crea la variante [`Remove`](Self::Remove) para el atributo indicado.
pub fn remove(name: impl Into) -> Self {
Self::Remove(name.into())
}
/// Crea la variante [`SetExtra`](Self::SetExtra) con la clave y el valor indicados.
///
/// ```rust
/// # use pagetop::prelude::*;
/// const EXT_SIZE: &str = "myext.size";
/// let props = Props::default().with_prop(PropsOp::set_extra(EXT_SIZE, 42_u32));
/// assert_eq!(props.extra_or(EXT_SIZE, 0_u32), 42);
/// ```
pub fn set_extra(key: &'static str, value: T) -> Self {
Self::SetExtra(
key,
PropsExtra {
value: Arc::new(value),
type_name: TypeInfo::FullName.of::(),
},
)
}
/// Crea la variante [`RemoveExtra`](Self::RemoveExtra) para la clave indicada.
pub fn remove_extra(key: &'static str) -> Self {
Self::RemoveExtra(key)
}
}
// **< Props >**************************************************************************************
/// Recoge el identificador, clases CSS, atributos HTML y valores extra de un componente.
///
/// Al renderizar con [`html!`](crate::html::html) se emite primero el identificador `id` (si
/// existe), luego `class` (si hay clases) y después el resto de atributos, normalmente aplicados al
/// elemento raíz del componente.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let props = Props::new("hx-get", "/api/items")
/// .with_prop(PropsOp::set("hx-target", "#lista"))
/// .with_prop(PropsOp::set("hx-swap", "outerHTML"));
///
/// let markup = html! {
/// button (props) { "Cargar" }
/// };
///
/// assert_eq!(
/// markup.into_string(),
/// r##""##
/// );
/// ```
///
/// # Identificadores
///
/// [`SetId`](PropsOp::SetId) (usando [`PropsOp::set_id()`]) normaliza el valor asignado al
/// identificador del componente: recorta espacios, convierte a minúsculas y sustituye los espacios
/// intermedios por `_`.
///
/// ```rust
/// # use pagetop::prelude::*;
/// let props = Props::default().with_id("My Button");
/// let markup = html! { button (props) { "OK" } };
/// assert_eq!(markup.into_string(), r#""#);
/// ```
///
/// [`EnsureId`](PropsOp::EnsureId) (usando [`PropsOp::ensure_id()`]) sólo asigna si no hay
/// identificador previo:
///
/// ```rust
/// # use pagetop::prelude::*;
/// // Con `id` previo: `EnsureId` no tiene efecto.
/// let props = Props::default()
/// .with_id("explicit")
/// .with_prop(PropsOp::ensure_id("default"));
/// assert_eq!(props.get_id(), Some("explicit".to_string()));
///
/// // Sin `id` previo: `EnsureId` asigna el valor.
/// let props = Props::default().with_prop(PropsOp::ensure_id("default"));
/// assert_eq!(props.get_id(), Some("default".to_string()));
/// ```
///
/// # Clases CSS
///
/// ```rust
/// # use pagetop::prelude::*;
/// let props = Props::default()
/// .with_prop(PropsOp::add_classes("btn btn-primary"))
/// .with_prop(PropsOp::add_classes("active"))
/// .with_prop(PropsOp::replace_classes("btn-primary", "btn-secondary"));
///
/// let markup = html! { button (props) { "OK" } };
/// assert_eq!(markup.into_string(), r#""#);
/// ```
///
/// # Valores extra
///
/// Las variantes [`SetExtra`](PropsOp::SetExtra) y [`RemoveExtra`](PropsOp::RemoveExtra), usando
/// [`PropsOp::set_extra()`] y [`PropsOp::remove_extra()`] respectivamente, permiten adjuntar
/// valores tipados a un `Props`. Son útiles para que temas y extensiones amplíen el comportamiento
/// de componentes ya existentes mediante *traits* con nuevos métodos que lean y escriban esos
/// valores.
///
/// ```rust
/// # use pagetop::prelude::*;
/// const EXT_ENABLED: &str = "myext.enabled";
/// const EXT_LABEL: &str = "myext.label";
///
/// let props = Props::default()
/// .with_prop(PropsOp::set_extra(EXT_ENABLED, true))
/// .with_prop(PropsOp::set_extra(EXT_LABEL, "flotante".to_string()));
///
/// assert!(props.extra_or(EXT_ENABLED, false));
/// assert_eq!(props.extra_or(EXT_LABEL, String::new()), "flotante");
///
/// // Tipo incorrecto devuelve el valor por defecto indicado:
/// assert_eq!(props.extra_or(EXT_ENABLED, 0_u8), 0);
/// ```
///
/// Los valores extra no se emiten en el HTML al renderizar; son exclusivamente para uso interno de
/// temas y extensiones.
///
/// # Integración en componentes
///
/// El patrón recomendado es añadir un campo `props: Props` con su método *builder* delegado:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// #[derive(AutoDefault, Clone, Getters)]
/// pub struct MyButton {
/// label: L10n,
/// props: Props,
/// }
///
/// #[async_trait]
/// impl Component for MyButton {
/// fn new() -> Self { Self::default() }
///
/// async fn prepare(&self, cx: &mut Context) -> Result {
/// Ok(html! {
/// button (self.props()) {
/// (self.label().using(cx))
/// }
/// })
/// }
/// }
///
/// impl MyButton {
/// /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
/// #[builder_fn]
/// pub fn with_prop(mut self, op: PropsOp) -> Self {
/// self.props.alter_prop(op);
/// self
/// }
/// }
/// ```
#[derive(AutoDefault, Clone, Debug)]
pub struct Props {
id: Option,
classes: Vec,
attrs: Vec<(CowStr, CowStr)>,
extras: HashMap<&'static str, PropsExtra>,
}
impl Props {
/// Crea una colección con un primer atributo ya establecido.
pub fn new(name: impl Into, value: impl Into) -> Self {
Self::default().with_prop(PropsOp::set(name, value))
}
/// Crea una colección con las clases CSS iniciales indicadas.
pub fn classes(classes: impl Into) -> Self {
Self::default().with_prop(PropsOp::add_classes(classes))
}
// **< Props BUILDER >**************************************************************************
/// Establece el identificador del componente; equivale a `with_prop(PropsOp::set_id(id))`.
#[builder_fn]
pub fn with_id(mut self, id: impl Into) -> Self {
self.apply_id(id.into().as_ref());
self
}
/// Modifica el identificador, las clases, los atributos o los valores extra según la operación
/// indicada. El método recomendado para construir cada operación es usar los constructores de
/// [`PropsOp`]:
///
/// - [`PropsOp::set_id()`] - establece el identificador normalizando el valor.
/// - [`PropsOp::ensure_id()`] - establece el identificador sólo si no hay ninguno definido.
/// - [`PropsOp::add_classes()`] - añade clases al final (sin duplicados).
/// - [`PropsOp::prepend_classes()`] - añade clases al principio (sin duplicados).
/// - [`PropsOp::replace_classes()`] - sustituye una o varias clases existentes por otras
/// nuevas, preservando su posición.
/// - [`PropsOp::remove_classes()`] - elimina las clases indicadas.
/// - [`PropsOp::set()`] - añade el atributo o reemplaza su valor. `set("id", ...)` aplica la
/// misma normalización que `set_id()`. `set("class", ...)` reemplaza la lista de clases.
/// - [`PropsOp::remove()`] - elimina el atributo. `remove("id")` elimina el identificador;
/// `remove("class")` vacía la lista de clases.
/// - [`PropsOp::set_extra()`] - almacena un valor extra tipado.
/// - [`PropsOp::remove_extra()`] - elimina el valor extra asociado a la clave.
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
match op {
PropsOp::SetId(value) => {
self.apply_id(value.as_ref());
}
PropsOp::EnsureId(value) => {
if self.id.is_none() {
self.apply_id(value.as_ref());
}
}
PropsOp::AddClasses(classes) => {
let Some(normalized) =
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
else {
return self;
};
let pos = self.classes.len();
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), pos);
}
PropsOp::PrependClasses(classes) => {
let Some(normalized) =
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
else {
return self;
};
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
}
PropsOp::ReplaceClasses(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;
};
let mut pos = self.classes.len();
let mut replaced = false;
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);
replaced = true;
}
}
if replaced {
self.insert_classes(new.as_ref().split_ascii_whitespace(), pos);
}
}
PropsOp::RemoveClasses(classes) => {
let Some(normalized) =
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
else {
return self;
};
self.classes.retain(|c| {
!normalized
.as_ref()
.split_ascii_whitespace()
.any(|r| r == c.as_str())
});
}
PropsOp::Set(name, value) => {
if name.as_ref() == "id" {
self.apply_id(value.as_ref());
} else if name.as_ref() == "class" {
if let Some(normalized) =
util::normalize_ascii_or_empty(value.as_ref(), "Props::with_prop")
{
self.classes.clear();
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
}
} else if let Some(pos) = self.attrs.iter().position(|(k, _)| k == &name) {
self.attrs[pos].1 = value;
} else {
self.attrs.push((name, value));
}
}
PropsOp::Remove(name) => {
if name.as_ref() == "id" {
self.id = None;
} else if name.as_ref() == "class" {
self.classes.clear();
} else {
self.attrs.retain(|(k, _)| k != &name);
}
}
PropsOp::SetExtra(key, extra) => {
self.extras.insert(key, extra);
}
PropsOp::RemoveExtra(key) => {
self.extras.remove(key);
}
}
self
}
// **< Props GETTERS >**************************************************************************
/// Devuelve el identificador normalizado del componente, si existe.
#[inline]
pub fn get_id(&self) -> Option {
self.id.clone()
}
/// Devuelve la lista de clases como cadena de texto, si hay clases definidas.
pub fn get_classes(&self) -> Option {
if self.classes.is_empty() {
None
} else {
Some(self.classes.join(" "))
}
}
/// Devuelve el valor del atributo indicado, si existe.
///
/// Los nombres `"id"` y `"class"` son equivalentes a llamar a [`get_id()`](Self::get_id) y
/// [`get_classes()`](Self::get_classes) respectivamente.
pub fn get_prop(&self, name: impl AsRef) -> Option {
match name.as_ref() {
"id" => self.id.clone(),
"class" => self.get_classes(),
name => self
.attrs
.iter()
.find(|(k, _)| k.as_ref() == name)
.map(|(_, v)| v.to_string()),
}
}
/// Devuelve `true` si no hay ningún identificador definido.
#[inline]
pub fn is_id_empty(&self) -> bool {
self.id.is_none()
}
/// Devuelve `true` si no hay ninguna clase definida.
#[inline]
pub fn is_classes_empty(&self) -> bool {
self.classes.is_empty()
}
/// Devuelve `true` si no hay ningún atributo adicional definido, sin tener en cuenta el
/// identificador ni las clases.
#[inline]
pub fn is_attrs_empty(&self) -> bool {
self.attrs.is_empty()
}
/// Devuelve `true` si no hay ningún identificador, clases ni atributos adicionales definidos,
/// sin tener en cuenta los valores extra.
#[inline]
pub fn is_empty(&self) -> bool {
self.id.is_none() && self.attrs.is_empty() && self.classes.is_empty()
}
/// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
pub fn has_class(&self, classes: impl AsRef) -> bool {
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
return false;
};
normalized
.as_ref()
.split_ascii_whitespace()
.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) -> 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`.
///
/// Devuelve un [`Result`] que indica si la clave existe y si el tipo coincide:
///
/// - `Ok(&T)` si la clave existe y el tipo coincide. El tipo `T` debe ser el mismo que se usó
/// al almacenar el valor con [`PropsOp::set_extra()`].
/// - `Err(PropsError::ExtraNotFound)` si la clave no existe.
/// - `Err(PropsError::ExtraTypeMismatch)` si el tipo no coincide.
///
/// ```rust
/// # use pagetop::prelude::*;
/// const EXT_COUNT: &str = "myext.count";
/// const EXT_OTHER: &str = "myext.other";
///
/// let props = Props::default().with_prop(PropsOp::set_extra(EXT_COUNT, 7_i32));
///
/// assert_eq!(*props.extra::(EXT_COUNT).unwrap(), 7);
/// assert_eq!(props.extra::(EXT_OTHER), Err(PropsError::ExtraNotFound { key: EXT_OTHER }));
/// assert!(matches!(
/// props.extra::(EXT_COUNT),
/// Err(PropsError::ExtraTypeMismatch { .. })
/// ));
/// ```
pub fn extra(&self, key: &'static str) -> Result<&T, PropsError> {
let ev = self
.extras
.get(key)
.ok_or(PropsError::ExtraNotFound { key })?;
ev.value
.downcast_ref::()
.ok_or_else(|| PropsError::ExtraTypeMismatch {
key,
expected: TypeInfo::FullName.of::(),
found: ev.type_name,
})
}
/// Devuelve el valor extra clonado o el **valor `default`** si no existe o el tipo no coincide.
///
/// ```rust
/// # use pagetop::prelude::*;
/// const EXT_FLAG: &str = "myext.flag";
/// const EXT_OTHER: &str = "myext.other";
///
/// let props = Props::default().with_prop(PropsOp::set_extra(EXT_FLAG, true));
///
/// assert!(props.extra_or(EXT_FLAG, false));
/// assert!(!props.extra_or(EXT_OTHER, false));
/// ```
pub fn extra_or(&self, key: &'static str, default: T) -> T {
self.extra::(key).ok().cloned().unwrap_or(default)
}
/// Devuelve el valor extra clonado o el **valor por defecto del tipo** si no existe o el tipo
/// no coincide.
///
/// ```rust
/// # use pagetop::prelude::*;
/// const EXT_FLAG: &str = "myext.flag";
/// const EXT_COUNT: &str = "myext.count";
///
/// let props = Props::default();
///
/// assert_eq!(props.extra_or_default::(EXT_FLAG), false);
/// assert_eq!(props.extra_or_default::(EXT_COUNT), 0);
/// ```
pub fn extra_or_default(&self, key: &'static str) -> T {
self.extra::(key).ok().cloned().unwrap_or_default()
}
/// Devuelve el valor extra clonado o el **valor evaluado por la función `f`** si no existe o el
/// tipo no coincide.
///
/// ```rust
/// # use pagetop::prelude::*;
/// const EXT_LABEL: &str = "myext.label";
///
/// let props = Props::default();
///
/// let result = props.extra_or_else(EXT_LABEL, || "default".to_string());
/// assert_eq!(result, "default");
/// ```
pub fn extra_or_else T>(
&self,
key: &'static str,
f: F,
) -> T {
self.extra::(key).ok().cloned().unwrap_or_else(f)
}
// **< Props PRIVATE >**************************************************************************
fn apply_id(&mut self, id: &str) {
let id = id.trim();
self.id = if id.is_empty() {
None
} else {
Some(id.to_ascii_lowercase().replace(' ', "_"))
};
}
fn insert_classes<'a, I>(&mut self, classes: I, mut pos: usize)
where
I: IntoIterator,
{
for class in classes {
if !self.classes.iter().any(|c| c == class) {
let class = class.to_string();
if pos >= self.classes.len() {
self.classes.push(class);
} else {
self.classes.insert(pos, class);
}
pos += 1;
}
}
}
}
#[doc(hidden)]
impl Render for Props {
fn render_to(&self, w: &mut String) {
if let Some(id) = self.id.as_deref() {
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);
}
w.push('"');
}
for (name, value) in &self.attrs {
w.push(' ');
let _ = write!(Escaper::new(w), "{}", name);
w.push_str("=\"");
let _ = write!(Escaper::new(w), "{}", value);
w.push('"');
}
}
}