Compare commits
2 commits
28f1eee391
...
8577ca8a59
| Author | SHA1 | Date | |
|---|---|---|---|
| 8577ca8a59 | |||
| ee1b48ff3f |
18 changed files with 1354 additions and 1248 deletions
|
|
@ -25,13 +25,13 @@
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! # use pagetop::prelude::*;
|
//! # use pagetop::prelude::*;
|
||||||
//! # use pagetop_htmx::prelude::*;
|
//! # use pagetop_htmx::prelude::*;
|
||||||
//! # let cx = Context::default();
|
//! # let mut cx = Context::default();
|
||||||
//! let props = Props::new(hx::GET, "/api/items")
|
//! let props = Props::new(hx::GET, "/api/items")
|
||||||
//! .with_prop(PropsOp::set(hx::TARGET, "#list"))
|
//! .with_prop(PropsOp::set(hx::TARGET, "#list"))
|
||||||
//! .with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML));
|
//! .with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML));
|
||||||
//!
|
//!
|
||||||
//! let markup = html! {
|
//! let markup = html! {
|
||||||
//! button (props.unpack(&cx)) { "Load" }
|
//! button (props.unpack(&mut cx)) { "Load" }
|
||||||
//! };
|
//! };
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ impl Crumb {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderiza con enlace si tiene ruta, o texto plano en otro caso. Sólo lo usa `Breadcrumb`.
|
// Renderiza con enlace si tiene ruta, o texto plano en otro caso. Sólo lo usa `Breadcrumb`.
|
||||||
pub(super) fn render_crumb(&self, cx: &Context) -> Markup {
|
pub(super) fn render_crumb(&self, cx: &mut Context) -> Markup {
|
||||||
let label = self.label().using(cx);
|
let label = self.label().using(cx);
|
||||||
match self.route() {
|
match self.route() {
|
||||||
Some(route) => html! {
|
Some(route) => html! {
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ impl Column {
|
||||||
// Traduce la etiqueta y, si la columna es ordenable, la envuelve en su enlace con `aria-sort`
|
// Traduce la etiqueta y, si la columna es ordenable, la envuelve en su enlace con `aria-sort`
|
||||||
// y las clases `table-sort*` ya resueltas por `table::SortLink`. Sólo lo usa `Table` al
|
// y las clases `table-sort*` ya resueltas por `table::SortLink`. Sólo lo usa `Table` al
|
||||||
// renderizar.
|
// renderizar.
|
||||||
pub(super) fn render_header(&self, cx: &Context) -> Markup {
|
pub(super) fn render_header(&self, cx: &mut Context) -> Markup {
|
||||||
let label = self.label().using(cx);
|
let label = self.label().using(cx);
|
||||||
|
|
||||||
let Some(sort) = self.sort() else {
|
let Some(sort) = self.sort() else {
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ impl PageTopSvg {
|
||||||
/// [`Lc::none()`]: crate::locale::Lc::none
|
/// [`Lc::none()`]: crate::locale::Lc::none
|
||||||
/// [`Image`]: crate::base::component::Image
|
/// [`Image`]: crate::base::component::Image
|
||||||
/// [`image::Source::Logo`]: crate::base::component::image::Source::Logo
|
/// [`image::Source::Logo`]: crate::base::component::image::Source::Logo
|
||||||
pub fn markup_with(&self, cx: &Context, props: &Props, label: Lc) -> Markup {
|
pub fn markup_with(&self, cx: &mut Context, props: &Props, label: Lc) -> Markup {
|
||||||
let label = label.lookup(cx);
|
let label = label.lookup(cx);
|
||||||
html! {
|
html! {
|
||||||
svg
|
svg
|
||||||
|
|
|
||||||
1091
src/html/props.rs
1091
src/html/props.rs
File diff suppressed because it is too large
Load diff
809
src/html/props/definition.rs
Normal file
809
src/html/props/definition.rs
Normal file
|
|
@ -0,0 +1,809 @@
|
||||||
|
use crate::core::TypeInfo;
|
||||||
|
use crate::core::component::Context;
|
||||||
|
use crate::html::maud::{Escaper, RenderAttrs};
|
||||||
|
use crate::html::props::{PropsError, PropsExtra, PropsOp};
|
||||||
|
use crate::{AutoDefault, CowStr, builder_impl, trace, util};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::panic::Location;
|
||||||
|
|
||||||
|
// **< Props >**************************************************************************************
|
||||||
|
|
||||||
|
/// Recoge el identificador, clases CSS, atributos HTML y valores extra de un componente.
|
||||||
|
///
|
||||||
|
/// Guarda estos valores con operaciones [`PropsOp`]. Cuando se renderiza usando
|
||||||
|
/// [`html!`](crate::html::html) se emite primero el identificador `id` (si existe), luego `class`
|
||||||
|
/// (si hay clases), después `style` (si hay declaraciones de estilo) y por último el resto de
|
||||||
|
/// atributos; normalmente se asignan al elemento raíz del componente.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// Se omite la construcción explícita de `Context` (`let mut cx = Context::default();`) para no
|
||||||
|
/// distraer del resto del ejemplo.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # let mut cx = Context::default();
|
||||||
|
/// 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.unpack(&mut cx)) { "Cargar" }
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// markup.into_string(),
|
||||||
|
/// r##"<button hx-get="/api/items" hx-target="#lista" hx-swap="outerHTML">Cargar</button>"##
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # 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 mut cx = Context::default();
|
||||||
|
/// let props = Props::default().with_id("My Button");
|
||||||
|
/// let markup = html! { button (props.unpack(&mut cx)) { "OK" } };
|
||||||
|
/// assert_eq!(markup.into_string(), r#"<button id="my_button">OK</button>"#);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`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 mut cx = Context::default();
|
||||||
|
/// 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.unpack(&mut cx)) { "OK" } };
|
||||||
|
/// assert_eq!(markup.into_string(), r#"<button class="btn btn-secondary active">OK</button>"#);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Estilos CSS
|
||||||
|
///
|
||||||
|
/// Cada declaración se añade indicando una propiedad y su valor. Si la propiedad ya existe,
|
||||||
|
/// [`AddStyle`](PropsOp::AddStyle) sustituye su valor conservando la posición, sin duplicarla.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # let mut cx = Context::default();
|
||||||
|
/// let props = Props::default()
|
||||||
|
/// .with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
/// .with_prop(PropsOp::add_style("font-weight", "bold"))
|
||||||
|
/// .with_prop(PropsOp::add_style("color", "blue"))
|
||||||
|
/// .with_prop(PropsOp::remove_style("font-weight"));
|
||||||
|
///
|
||||||
|
/// let markup = html! { button (props.unpack(&mut cx)) { "OK" } };
|
||||||
|
/// assert_eq!(markup.into_string(), r#"<button style="color: blue">OK</button>"#);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Atributos duplicados junto a `Props`
|
||||||
|
///
|
||||||
|
/// Cuando el componente combina `(self.props().unpack(cx))` con un atributo 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 mut cx = Context::default();
|
||||||
|
/// let props = Props::default().with_prop(PropsOp::set("title", "de Props"));
|
||||||
|
///
|
||||||
|
/// let markup = html! { span title="literal" (props.unpack(&mut cx)) { "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
|
||||||
|
/// [`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: Lc,
|
||||||
|
/// props: Props,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[async_trait]
|
||||||
|
/// impl Component for MyButton {
|
||||||
|
/// fn new() -> Self { Self::default() }
|
||||||
|
///
|
||||||
|
/// async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
/// Ok(html! {
|
||||||
|
/// button (self.props().unpack(cx)) {
|
||||||
|
/// (self.label().using(cx))
|
||||||
|
/// }
|
||||||
|
/// })
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[builder_impl]
|
||||||
|
/// impl MyButton {
|
||||||
|
/// /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||||
|
/// pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||||
|
/// self.props.alter_prop(op);
|
||||||
|
/// self
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
|
pub struct Props {
|
||||||
|
id: Option<String>,
|
||||||
|
classes: Vec<String>,
|
||||||
|
styles: Vec<(CowStr, CowStr)>,
|
||||||
|
attrs: Vec<(CowStr, CowStr)>,
|
||||||
|
extras: HashMap<&'static str, PropsExtra>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_impl]
|
||||||
|
impl Props {
|
||||||
|
/// Crea una colección con un primer atributo ya establecido.
|
||||||
|
pub fn new(name: impl Into<CowStr>, value: impl Into<CowStr>) -> 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<CowStr>) -> Self {
|
||||||
|
Self::default().with_prop(PropsOp::add_classes(classes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Props BUILDER >**************************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador del componente; equivale a `with_prop(PropsOp::set_id(id))`.
|
||||||
|
pub fn with_id(mut self, id: impl Into<CowStr>) -> 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`].
|
||||||
|
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(classes.as_ref()) 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(classes.as_ref()) else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
|
||||||
|
}
|
||||||
|
PropsOp::ReplaceClasses(old, new) => {
|
||||||
|
let Some(old) = util::normalize_ascii(old.as_ref()) else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
let Some(new) = util::normalize_ascii(new.as_ref()) 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::ReplaceAllClasses(old, new) => {
|
||||||
|
let Some(old) = util::normalize_ascii(old.as_ref()) else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
let Some(new) = util::normalize_ascii(new.as_ref()) 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) => {
|
||||||
|
let Some(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
self.classes.retain(|c| {
|
||||||
|
!normalized
|
||||||
|
.as_ref()
|
||||||
|
.split_ascii_whitespace()
|
||||||
|
.any(|r| r == c.as_str())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
PropsOp::AddStyle(property, value) => {
|
||||||
|
self.set_style(property.as_ref(), value.as_ref());
|
||||||
|
}
|
||||||
|
PropsOp::RemoveStyle(property) => {
|
||||||
|
self.remove_style(property.as_ref());
|
||||||
|
}
|
||||||
|
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(value.as_ref()) {
|
||||||
|
self.classes.clear();
|
||||||
|
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
|
||||||
|
}
|
||||||
|
} else if name.as_ref() == "style" {
|
||||||
|
self.styles.clear();
|
||||||
|
self.parse_styles(value.as_ref());
|
||||||
|
} else if let Some(pos) = self.attrs.iter().position(|(k, _)| k == &name) {
|
||||||
|
self.attrs[pos].1 = value;
|
||||||
|
} else {
|
||||||
|
self.attrs.push((name, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PropsOp::Rename(from, to) => {
|
||||||
|
if let Some(pos) = self.attrs.iter().position(|(k, _)| k == &from) {
|
||||||
|
let (_, value) = self.attrs.remove(pos);
|
||||||
|
if !self.attrs.iter().any(|(k, _)| k == &to) {
|
||||||
|
self.attrs.push((to, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PropsOp::Remove(name) => {
|
||||||
|
if name.as_ref() == "id" {
|
||||||
|
self.id = None;
|
||||||
|
} else if name.as_ref() == "class" {
|
||||||
|
self.classes.clear();
|
||||||
|
} else if name.as_ref() == "style" {
|
||||||
|
self.styles.clear();
|
||||||
|
} else {
|
||||||
|
self.attrs.retain(|(k, _)| k != &name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PropsOp::SetExtra(key, extra) => {
|
||||||
|
self.extras.insert(key, extra);
|
||||||
|
}
|
||||||
|
PropsOp::RemoveExtra(key) => {
|
||||||
|
self.extras.remove(key);
|
||||||
|
}
|
||||||
|
PropsOp::FlexItem(placement) => {
|
||||||
|
placement.apply_to(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Props GETTERS >**************************************************************************
|
||||||
|
|
||||||
|
/// Devuelve el identificador normalizado del componente, si existe.
|
||||||
|
#[inline]
|
||||||
|
pub fn get_id(&self) -> Option<String> {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve la lista de clases como cadena de texto, si hay clases definidas.
|
||||||
|
pub fn get_classes(&self) -> Option<String> {
|
||||||
|
if self.classes.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.classes.join(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve las declaraciones de estilo como cadena de texto (separadas por `"; "`), si hay
|
||||||
|
/// estilos definidos.
|
||||||
|
pub fn get_styles(&self) -> Option<String> {
|
||||||
|
if self.styles.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
self.styles
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{k}: {v}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("; "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve el valor de la propiedad de estilo indicada, si existe.
|
||||||
|
pub fn get_style(&self, property: impl AsRef<str>) -> Option<String> {
|
||||||
|
let property = property.as_ref().trim().to_ascii_lowercase();
|
||||||
|
self.styles
|
||||||
|
.iter()
|
||||||
|
.find(|(k, _)| k.as_ref() == property)
|
||||||
|
.map(|(_, v)| v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve el valor del atributo indicado, si existe.
|
||||||
|
///
|
||||||
|
/// Los nombres `"id"`, `"class"` y `"style"` son equivalentes a llamar a
|
||||||
|
/// [`get_id()`](Self::get_id), [`get_classes()`](Self::get_classes) y
|
||||||
|
/// [`get_styles()`](Self::get_styles) respectivamente.
|
||||||
|
pub fn get_prop(&self, name: impl AsRef<str>) -> Option<String> {
|
||||||
|
match name.as_ref() {
|
||||||
|
"id" => self.id.clone(),
|
||||||
|
"class" => self.get_classes(),
|
||||||
|
"style" => self.get_styles(),
|
||||||
|
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 estilo definido.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_styles_empty(&self) -> bool {
|
||||||
|
self.styles.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve `true` si no hay ningún atributo adicional definido, sin tener en cuenta el
|
||||||
|
/// identificador, las clases ni los estilos.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_attrs_empty(&self) -> bool {
|
||||||
|
self.attrs.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve `true` si no hay ningún identificador, clases, estilos o atributos adicionales
|
||||||
|
/// definidos, sin tener en cuenta los valores extra.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.id.is_none()
|
||||||
|
&& self.classes.is_empty()
|
||||||
|
&& self.styles.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_non_blank(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.
|
||||||
|
pub fn has_all_classes(&self, classes: impl AsRef<str>) -> bool {
|
||||||
|
let Ok(normalized) = util::normalize_ascii_non_blank(classes.as_ref()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
normalized
|
||||||
|
.as_ref()
|
||||||
|
.split_ascii_whitespace()
|
||||||
|
.all(|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::<i32>(EXT_COUNT).unwrap(), 7);
|
||||||
|
/// assert_eq!(
|
||||||
|
/// props.extra::<i32>(EXT_OTHER),
|
||||||
|
/// Err(PropsError::ExtraNotFound { key: EXT_OTHER })
|
||||||
|
/// );
|
||||||
|
/// assert!(matches!(
|
||||||
|
/// props.extra::<u32>(EXT_COUNT),
|
||||||
|
/// Err(PropsError::ExtraTypeMismatch { .. })
|
||||||
|
/// ));
|
||||||
|
/// ```
|
||||||
|
pub fn extra<T: 'static>(&self, key: &'static str) -> Result<&T, PropsError> {
|
||||||
|
let ev = self
|
||||||
|
.extras
|
||||||
|
.get(key)
|
||||||
|
.ok_or(PropsError::ExtraNotFound { key })?;
|
||||||
|
ev.value
|
||||||
|
.downcast_ref::<T>()
|
||||||
|
.ok_or_else(|| PropsError::ExtraTypeMismatch {
|
||||||
|
key,
|
||||||
|
expected: TypeInfo::FullName.of::<T>(),
|
||||||
|
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<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
|
||||||
|
self.extra::<T>(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::<bool>(EXT_FLAG), false);
|
||||||
|
/// assert_eq!(props.extra_or_default::<i32>(EXT_COUNT), 0);
|
||||||
|
/// ```
|
||||||
|
pub fn extra_or_default<T: Clone + Default + 'static>(&self, key: &'static str) -> T {
|
||||||
|
self.extra::<T>(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: Clone + 'static, F: FnOnce() -> T>(
|
||||||
|
&self,
|
||||||
|
key: &'static str,
|
||||||
|
f: F,
|
||||||
|
) -> T {
|
||||||
|
self.extra::<T>(key).ok().cloned().unwrap_or_else(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Props RENDER >***************************************************************************
|
||||||
|
|
||||||
|
/// Extrae `Props` en la posición de atributos de [`html!`] usando el `Context` activo:
|
||||||
|
/// `button (self.props().unpack(cx)) { ... }`.
|
||||||
|
///
|
||||||
|
/// `Props` no implementa [`RenderAttrs`] directamente. Obliga a pasar siempre el `Context`
|
||||||
|
/// vigente en el punto donde se renderiza, aunque no lo necesite ningún atributo propio. Recibe
|
||||||
|
/// `&mut Context` porque aquí, en el momento de extraer los atributos, es donde se resuelve
|
||||||
|
/// [`FlexItem`]: las clases que devuelve se añaden a las del propio componente al escribir el
|
||||||
|
/// atributo `class`.
|
||||||
|
///
|
||||||
|
/// Si el propio elemento actúa además como contenedor [`Flex`], utiliza [`unpack_with_flex()`]
|
||||||
|
/// en su lugar.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # let mut cx = Context::default();
|
||||||
|
/// let props = Props::default().with_id("example");
|
||||||
|
/// let markup = html! { button (props.unpack(&mut cx)) { "OK" } };
|
||||||
|
/// assert_eq!(markup.into_string(), r#"<button id="example">OK</button>"#);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`html!`]: crate::html::html
|
||||||
|
/// [`Flex`]: crate::html::flex::Flex
|
||||||
|
/// [`FlexItem`]: crate::html::flex::FlexItem
|
||||||
|
/// [`unpack_with_flex()`]: Self::unpack_with_flex
|
||||||
|
pub fn unpack<'a>(&'a self, cx: &mut Context) -> impl RenderAttrs + 'a {
|
||||||
|
PropsUnpack {
|
||||||
|
props: self,
|
||||||
|
classes: self.flex_item.apply(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Igual que [`unpack()`], pero además resuelve `flex` con el posicionamiento [`Flex`].
|
||||||
|
///
|
||||||
|
/// A diferencia de [`FlexItem`] (que se acumula con [`PropsOp::FlexItem`] porque cualquier
|
||||||
|
/// componente ajeno puede necesitarlo sin tener un campo propio para ello), `Flex` sólo tiene
|
||||||
|
/// sentido en los contenedores que ya declaran su propio campo `flex: Flex` (`Container`,
|
||||||
|
/// `Navbar`...): se les pasa aquí directamente, ya resuelto (`self.flex()`), sin pasar por
|
||||||
|
/// `PropsOp`.
|
||||||
|
///
|
||||||
|
/// [`unpack()`]: Self::unpack
|
||||||
|
/// [`Flex`]: crate::html::flex::Flex
|
||||||
|
/// [`FlexItem`]: crate::html::flex::FlexItem
|
||||||
|
/// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem
|
||||||
|
pub fn unpack_with_flex<'a>(&'a self, cx: &mut Context, flex: Flex) -> impl RenderAttrs + 'a {
|
||||||
|
PropsUnpack {
|
||||||
|
props: self,
|
||||||
|
classes: util::join_pair!(flex.apply(cx), " ", self.flex_item.apply(cx)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Props PRIVATE >**************************************************************************
|
||||||
|
|
||||||
|
fn apply_id(&mut self, id: &str) {
|
||||||
|
self.id = util::normalize_token(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_classes<'a, I>(&mut self, classes: I, mut pos: usize)
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = &'a str>,
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añade o sustituye una declaración "propiedad: valor". Si la propiedad ya existe, sustituye
|
||||||
|
// su valor conservando la posición; si no, la añade al final. Ignora la declaración si la
|
||||||
|
// propiedad o el valor quedan vacíos tras recortar espacios. No aplica `normalize_ascii`: ver
|
||||||
|
// la documentación de `PropsOp::AddStyle` sobre por qué los valores de estilo no se restringen
|
||||||
|
// a ASCII.
|
||||||
|
fn set_style(&mut self, property: &str, value: &str) {
|
||||||
|
let Some(property) = util::non_blank(property) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let property = property.to_ascii_lowercase();
|
||||||
|
let Some(value) = util::non_blank(value) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(pos) = self.styles.iter().position(|(k, _)| k.as_ref() == property) {
|
||||||
|
self.styles[pos].1 = value.to_string().into();
|
||||||
|
} else {
|
||||||
|
self.styles
|
||||||
|
.push((property.into(), value.to_string().into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpreta una cadena "propiedad: valor" separadas por ";" (igual que el atributo HTML
|
||||||
|
// `style`) y aplica cada declaración con `set_style`. Ignora las declaraciones sin ":".
|
||||||
|
fn parse_styles(&mut self, styles: &str) {
|
||||||
|
for style in Self::split_style_declarations(styles) {
|
||||||
|
let Some(style) = util::non_blank(style) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some((property, value)) = style.split_once(':') else {
|
||||||
|
trace::debug!(
|
||||||
|
target = "Props::with_prop",
|
||||||
|
declaration = %style,
|
||||||
|
"Ignoring malformed style declaration (missing \":\")"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
self.set_style(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divide una cadena de declaraciones de estilo por ";", igual que `str::split(';')`, pero sin
|
||||||
|
// cortar dentro de paréntesis (`url(...)`) ni de cadenas entre comillas simples o dobles
|
||||||
|
// (`content: "a;b"`). No es un análisis CSS completo: no reconoce comentarios `/* ... */` ni
|
||||||
|
// comillas escapadas, y unos paréntesis o comillas sin cerrar arrastran el resto de la cadena
|
||||||
|
// a la última declaración.
|
||||||
|
fn split_style_declarations(styles: &str) -> Vec<&str> {
|
||||||
|
let mut depth = 0i32;
|
||||||
|
let mut quote = None;
|
||||||
|
let mut start = 0;
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
for (i, c) in styles.char_indices() {
|
||||||
|
if quote.is_none() && (c == '\'' || c == '"') {
|
||||||
|
quote = Some(c);
|
||||||
|
} else if quote == Some(c) {
|
||||||
|
quote = None;
|
||||||
|
} else if quote.is_none() && c == '(' {
|
||||||
|
depth += 1;
|
||||||
|
} else if quote.is_none() && c == ')' {
|
||||||
|
depth = (depth - 1).max(0);
|
||||||
|
} else if quote.is_none() && depth == 0 && c == ';' {
|
||||||
|
parts.push(&styles[start..i]);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.push(&styles[start..]);
|
||||||
|
parts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elimina la propiedad de estilo indicada, si existe.
|
||||||
|
fn remove_style(&mut self, property: &str) {
|
||||||
|
let property = property.trim().to_ascii_lowercase();
|
||||||
|
self.styles.retain(|(k, _)| k.as_ref() != property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Props {
|
||||||
|
// Escribe los atributos, omitiendo cualquiera 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]`, heredado desde `PropsUnpack::render_attrs_to()`) para facilitar la
|
||||||
|
// localización del problema.
|
||||||
|
#[track_caller]
|
||||||
|
fn write_attrs(&self, classes: &str, w: &mut String, exclude: &[&str]) {
|
||||||
|
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=\"");
|
||||||
|
let _ = write!(Escaper::new(w), "{}", id);
|
||||||
|
w.push('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clases propias del componente más las que aplica `Props::unpack()`/`unpack_with_flex()`.
|
||||||
|
let mut all_classes: Vec<&str> = self.classes.iter().map(String::as_str).collect();
|
||||||
|
all_classes.extend(classes.split_ascii_whitespace());
|
||||||
|
if let Some((first, rest)) = all_classes.split_first() {
|
||||||
|
if exclude.contains(&"class") {
|
||||||
|
trace::debug!(
|
||||||
|
caller = %Location::caller(),
|
||||||
|
attribute = "class",
|
||||||
|
discarded = %all_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('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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=\"");
|
||||||
|
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('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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("=\"");
|
||||||
|
let _ = write!(Escaper::new(w), "{}", value);
|
||||||
|
w.push('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< PropsUnpack >********************************************************************************
|
||||||
|
|
||||||
|
// Devuelto por `Props::unpack()`/`Props::unpack_with_flex()`. `classes` son las clases resueltas
|
||||||
|
// por `FlexItem::apply()`/`Flex::apply()` (desde el propio `unpack*()` usando el `&mut Context`),
|
||||||
|
// pendientes sólo de añadir a las del componente (ver `Props::write_attrs()`).
|
||||||
|
struct PropsUnpack<'a> {
|
||||||
|
props: &'a Props,
|
||||||
|
classes: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
impl RenderAttrs for PropsUnpack<'_> {
|
||||||
|
#[track_caller]
|
||||||
|
fn render_attrs_to(&self, w: &mut String, exclude: &[&str]) {
|
||||||
|
self.props.write_attrs(&self.classes, w, exclude);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/html/props/error.rs
Normal file
18
src/html/props/error.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Errores de acceso a valores extra de [`Props`](crate::html::props::Props).
|
||||||
|
#[derive(Debug, PartialEq, Eq, Error)]
|
||||||
|
pub enum PropsError {
|
||||||
|
/// La clave no existe. Incluye la clave (`key`).
|
||||||
|
#[error("extra \"{key}\" not found")]
|
||||||
|
ExtraNotFound { key: &'static str },
|
||||||
|
/// 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.
|
||||||
|
#[error("type mismatch for extra \"{key}\": expected \"{expected}\", found \"{found}\"")]
|
||||||
|
ExtraTypeMismatch {
|
||||||
|
key: &'static str,
|
||||||
|
expected: &'static str,
|
||||||
|
found: &'static str,
|
||||||
|
},
|
||||||
|
}
|
||||||
30
src/html/props/extra.rs
Normal file
30
src/html/props/extra.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
use std::any::Any;
|
||||||
|
use std::fmt;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Encapsula un valor tipado extra para almacenar en [`Props`](crate::html::props::Props).
|
||||||
|
///
|
||||||
|
/// Internamente usa [`Arc`] para que [`Props`](crate::html::props::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 {
|
||||||
|
// `pub(super)`: `PropsOp::set_extra()` (en el módulo hermano `op`) construye este valor
|
||||||
|
// directamente con un literal de struct.
|
||||||
|
pub(super) value: Arc<dyn Any + Send + Sync>,
|
||||||
|
pub(super) 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
272
src/html/props/op.rs
Normal file
272
src/html/props/op.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
use crate::CowStr;
|
||||||
|
use crate::core::TypeInfo;
|
||||||
|
use crate::html::flex::FlexItem;
|
||||||
|
use crate::html::props::extra::PropsExtra;
|
||||||
|
|
||||||
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Operaciones sobre el identificador, clases CSS, atributos HTML y valores extra en
|
||||||
|
/// [`Props`](crate::html::props::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 de clases y `Remove("class")` la vacía.
|
||||||
|
///
|
||||||
|
/// Las variantes `*Style` gestionan las declaraciones de estilos para el atributo `style`, con una
|
||||||
|
/// propiedad cada vez. Además, `Set("style", ...)` reemplaza la lista completa de estilos y
|
||||||
|
/// `Remove("style")` 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`, `class` y `style` tienen semántica especial documentada en estas
|
||||||
|
/// variantes.
|
||||||
|
///
|
||||||
|
/// [`Rename`](Self::Rename) cambia el nombre de un atributo genérico conservando su valor; no
|
||||||
|
/// reconoce `"id"`, `"class"` ni `"style"` como origen ni destino. Está pensada para que un tema
|
||||||
|
/// traduzca a su propio vocabulario los atributos que un componente ya expone, sin que éste tenga
|
||||||
|
/// que conocer ningún tema en concreto.
|
||||||
|
///
|
||||||
|
/// Las variantes `*Extra` permiten añadir valores tipados usando una clave. Están pensadas para
|
||||||
|
/// ampliar 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`](crate::html::props::Props). Esos valores
|
||||||
|
/// se interpretan como si
|
||||||
|
/// fueran valores internos del componente para tomar decisiones durante el renderizado.
|
||||||
|
///
|
||||||
|
/// Finalmente, [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem sobre
|
||||||
|
/// cualquier componente.
|
||||||
|
#[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 más** clases del primer valor por las clases indicadas en el segundo
|
||||||
|
/// valor, insertando las nuevas en la posición de la primera clase a sustituir encontrada, con
|
||||||
|
/// independencia del orden en que aparecen en el primer valor. Las que no existan se ignoran.
|
||||||
|
/// Si **ninguna** de las clases a sustituir existe, la operación no tiene efecto y no se
|
||||||
|
/// inserta nada. Se ignora si alguno de los dos valores contiene caracteres no ASCII.
|
||||||
|
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
|
||||||
|
/// caracteres no ASCII.
|
||||||
|
RemoveClasses(CowStr),
|
||||||
|
/// Añade una declaración de estilo (propiedad, valor) o sustituye su valor si la propiedad ya
|
||||||
|
/// existe, conservando su posición; si no, se añade al final. A diferencia de las clases, el
|
||||||
|
/// valor admite caracteres no ASCII (p. ej. `content`, `font-family`) y distingue mayúsculas y
|
||||||
|
/// minúsculas. El nombre de la propiedad se normaliza a minúsculas. Si la propiedad o el valor
|
||||||
|
/// quedan vacíos tras recortar espacios, la operación se ignora.
|
||||||
|
AddStyle(CowStr, CowStr),
|
||||||
|
/// Elimina la propiedad de estilo indicada, si existe.
|
||||||
|
RemoveStyle(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.
|
||||||
|
///
|
||||||
|
/// Usar `"style"` como nombre de atributo reemplaza la lista completa de estilos por los nuevos
|
||||||
|
/// indicados, interpretando el valor como declaraciones `"propiedad: valor"` separadas por `;`
|
||||||
|
/// (igual que el propio atributo `style` HTML). El separador `;` respeta paréntesis y comillas,
|
||||||
|
/// tal que valores como una *data URI* (`background: url(data:image/png;base64,...)`) o una
|
||||||
|
/// cadena con `;` (`content: "a;b"`) se interpretan correctamente. En cualquier caso, se
|
||||||
|
/// recomienda usar [`PropsOp::add_style()`](Self::add_style) para declarar estilos.
|
||||||
|
Set(CowStr, CowStr),
|
||||||
|
/// Si el primer atributo (origen) existe, lo renombra al segundo (destino), conservando su
|
||||||
|
/// valor; si el destino ya tiene su propio valor, se respeta sin sobrescribir y sólo se elimina
|
||||||
|
/// el origen. Si el origen no existe, la operación no tiene efecto.
|
||||||
|
///
|
||||||
|
/// Sólo actúa sobre atributos genéricos, por lo que los nombres `"id"`, `"class"` y `"style"`
|
||||||
|
/// no se reconocen como origen ni destino.
|
||||||
|
Rename(CowStr, CowStr),
|
||||||
|
/// Elimina el atributo indicado. Usar `"id"` elimina el identificador; usar `"class"` vacía la
|
||||||
|
/// lista de clases; y usar `"style"` vacía la lista de estilos.
|
||||||
|
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),
|
||||||
|
/// Aplica un posicionamiento [`FlexItem`] a un componente particular en un contenedor [`Flex`].
|
||||||
|
/// Añade directamente sus estilos Flexbox al propio componente, sin usar clases CSS.
|
||||||
|
///
|
||||||
|
/// Existe como variante de `PropsOp`, y no como método builder de un componente, porque las
|
||||||
|
/// propiedades de un ítem Flexbox tienen sentido sobre **cualquier** componente que pueda
|
||||||
|
/// añadirse como hijo de un contenedor Flex (por ejemplo `Button`, `Nav`, un componente de
|
||||||
|
/// terceros, incluso otro componente que sea, a su vez, un contenedor Flex para sus propios
|
||||||
|
/// hijos).
|
||||||
|
///
|
||||||
|
/// No existe una variante equivalente `PropsOp::Flex` para el componente contenedor. No hace
|
||||||
|
/// falta porque los componentes contenedores, como `Container` o `Navbar`, ofrecen su propio
|
||||||
|
/// `with_flex()` tipado y con introspección (p. ej. [`Container::flex()`]).
|
||||||
|
///
|
||||||
|
/// [`Flex`]: crate::html::flex::Flex
|
||||||
|
/// [`Container::flex()`]: crate::base::component::Container::flex
|
||||||
|
FlexItem(FlexItem),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropsOp {
|
||||||
|
/// Crea la variante [`SetId`](Self::SetId) con el identificador indicado.
|
||||||
|
pub fn set_id(id: impl Into<CowStr>) -> Self {
|
||||||
|
Self::SetId(id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`EnsureId`](Self::EnsureId) con el identificador indicado.
|
||||||
|
pub fn ensure_id(id: impl Into<CowStr>) -> Self {
|
||||||
|
Self::EnsureId(id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`AddClasses`](Self::AddClasses) con la clase o clases indicadas.
|
||||||
|
pub fn add_classes(classes: impl Into<CowStr>) -> Self {
|
||||||
|
Self::AddClasses(classes.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`PrependClasses`](Self::PrependClasses) con la clase o clases indicadas.
|
||||||
|
pub fn prepend_classes(classes: impl Into<CowStr>) -> 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()));
|
||||||
|
///
|
||||||
|
/// // 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 {
|
||||||
|
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.
|
||||||
|
pub fn remove_classes(classes: impl Into<CowStr>) -> Self {
|
||||||
|
Self::RemoveClasses(classes.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`AddStyle`](Self::AddStyle) con la propiedad y el valor de estilo
|
||||||
|
/// indicados.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// let props = Props::default()
|
||||||
|
/// .with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
/// .with_prop(PropsOp::add_style("font-weight", "bold"))
|
||||||
|
/// .with_prop(PropsOp::add_style("color", "blue"));
|
||||||
|
/// assert_eq!(props.get_styles(), Some("color: blue; font-weight: bold".to_string()));
|
||||||
|
/// ```
|
||||||
|
pub fn add_style(property: impl Into<CowStr>, value: impl Into<CowStr>) -> Self {
|
||||||
|
Self::AddStyle(property.into(), value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`RemoveStyle`](Self::RemoveStyle) para la propiedad de estilo indicada.
|
||||||
|
pub fn remove_style(property: impl Into<CowStr>) -> Self {
|
||||||
|
Self::RemoveStyle(property.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`Set`](Self::Set) con nombre y valor del atributo.
|
||||||
|
pub fn set(name: impl Into<CowStr>, value: impl Into<CowStr>) -> Self {
|
||||||
|
Self::Set(name.into(), value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`Rename`](Self::Rename) para renombrar `from` a `to`.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// let props = Props::new("data-dialog-toggle", "modal")
|
||||||
|
/// .with_prop(PropsOp::rename("data-dialog-toggle", "data-bs-toggle"));
|
||||||
|
/// assert_eq!(props.get_prop("data-bs-toggle"), Some("modal".to_string()));
|
||||||
|
/// assert_eq!(props.get_prop("data-dialog-toggle"), None);
|
||||||
|
///
|
||||||
|
/// // Si el destino tiene su propio valor no se sobrescribe, sólo se elimina el origen.
|
||||||
|
/// let props = Props::new("data-bs-toggle", "collapse")
|
||||||
|
/// .with_prop(PropsOp::set("data-dialog-toggle", "modal"))
|
||||||
|
/// .with_prop(PropsOp::rename("data-dialog-toggle", "data-bs-toggle"));
|
||||||
|
/// assert_eq!(props.get_prop("data-bs-toggle"), Some("collapse".to_string()));
|
||||||
|
/// assert_eq!(props.get_prop("data-dialog-toggle"), None);
|
||||||
|
/// ```
|
||||||
|
pub fn rename(from: impl Into<CowStr>, to: impl Into<CowStr>) -> Self {
|
||||||
|
Self::Rename(from.into(), to.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`Remove`](Self::Remove) para el atributo indicado.
|
||||||
|
pub fn remove(name: impl Into<CowStr>) -> 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<T: Any + Send + Sync + 'static>(key: &'static str, value: T) -> Self {
|
||||||
|
Self::SetExtra(
|
||||||
|
key,
|
||||||
|
PropsExtra {
|
||||||
|
value: Arc::new(value),
|
||||||
|
type_name: TypeInfo::FullName.of::<T>(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`RemoveExtra`](Self::RemoveExtra) para la clave indicada.
|
||||||
|
pub fn remove_extra(key: &'static str) -> Self {
|
||||||
|
Self::RemoveExtra(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`FlexItem`](Self::FlexItem) con el posicionamiento indicado.
|
||||||
|
pub fn flex_item(placement: FlexItem) -> Self {
|
||||||
|
Self::FlexItem(placement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -247,7 +247,7 @@ impl Page {
|
||||||
head {
|
head {
|
||||||
(head)
|
(head)
|
||||||
}
|
}
|
||||||
body (self.body_props().unpack(&self.context)) {
|
body (self.body_props().clone().unpack(&mut self.context)) {
|
||||||
(body)
|
(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,92 +49,97 @@ async fn without_flex_no_style_attribute_is_added() {
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn default_flex_adds_only_display_flex() {
|
async fn default_flex_adds_only_display_flex() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let mut container = Container::new()
|
let mut container = Container::new()
|
||||||
.with_flex(Flex::row())
|
.with_flex(Flex::new())
|
||||||
.with_child(Lc::n("x"));
|
.with_child(Lc::n("x"));
|
||||||
let html = container
|
let html = container.render(&mut cx).await.into_string();
|
||||||
.render(&mut Context::default())
|
let assets = cx.render_assets().into_string();
|
||||||
.await
|
|
||||||
.into_string();
|
|
||||||
|
|
||||||
assert!(html.contains(r#"style="display: flex""#));
|
assert!(html.contains(r#"class="_flex_""#));
|
||||||
|
assert!(assets.contains("_flex_{display:flex}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn column_direction_adds_flex_direction_style() {
|
async fn column_direction_adds_flex_direction_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let mut container = Container::new()
|
let mut container = Container::new()
|
||||||
.with_flex(Flex::column())
|
.with_flex(Flex::new().with_direction(flex::Direction::Column))
|
||||||
.with_child(Lc::n("x"));
|
.with_child(Lc::n("x"));
|
||||||
let html = container
|
let html = container.render(&mut cx).await.into_string();
|
||||||
.render(&mut Context::default())
|
let assets = cx.render_assets().into_string();
|
||||||
.await
|
|
||||||
.into_string();
|
|
||||||
|
|
||||||
assert!(html.contains("display: flex"));
|
assert!(html.contains("_flex_"));
|
||||||
assert!(html.contains("flex-direction: column"));
|
assert!(html.contains("_flex-direction_column_"));
|
||||||
|
assert!(assets.contains("_flex_{display:flex}"));
|
||||||
|
assert!(assets.contains("_flex-direction_column_{flex-direction:column}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn wrap_justify_and_align_add_their_matching_styles() {
|
async fn wrap_justify_and_align_add_their_matching_styles() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let mut container = Container::new()
|
let mut container = Container::new()
|
||||||
.with_flex(
|
.with_flex(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_wrap(flex::Behavior::Wrap)
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
.with_justify(flex::ContentJustify::Center)
|
.with_justify(flex::ContentJustify::Center)
|
||||||
.with_align(flex::Align::Center)
|
.with_align(flex::Align::Center)
|
||||||
.with_align_content(flex::AlignContent::SpaceBetween),
|
.with_align_content(flex::AlignContent::SpaceBetween),
|
||||||
)
|
)
|
||||||
.with_child(Lc::n("x"));
|
.with_child(Lc::n("x"));
|
||||||
let html = container
|
let html = container.render(&mut cx).await.into_string();
|
||||||
.render(&mut Context::default())
|
let assets = cx.render_assets().into_string();
|
||||||
.await
|
|
||||||
.into_string();
|
|
||||||
|
|
||||||
assert!(html.contains("flex-wrap: wrap"));
|
assert!(html.contains("_flex-wrap_wrap_"));
|
||||||
assert!(html.contains("justify-content: center"));
|
assert!(html.contains("_flex-justify_center_"));
|
||||||
assert!(html.contains("align-items: center"));
|
assert!(html.contains("_flex-align-items_center_"));
|
||||||
assert!(html.contains("align-content: space-between"));
|
assert!(html.contains("_flex-align-content_space-between_"));
|
||||||
|
assert!(assets.contains("_flex-wrap_wrap_{flex-wrap:wrap}"));
|
||||||
|
assert!(assets.contains("_flex-justify_center_{justify-content:center}"));
|
||||||
|
assert!(assets.contains("_flex-align-items_center_{align-items:center}"));
|
||||||
|
assert!(assets.contains("_flex-align-content_space-between_{align-content:space-between}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn gap_both_adds_a_single_gap_style() {
|
async fn gap_both_adds_a_single_gap_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let mut container = Container::new()
|
let mut container = Container::new()
|
||||||
.with_flex(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
.with_flex(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
.with_child(Lc::n("x"));
|
.with_child(Lc::n("x"));
|
||||||
let html = container
|
let html = container.render(&mut cx).await.into_string();
|
||||||
.render(&mut Context::default())
|
let assets = cx.render_assets().into_string();
|
||||||
.await
|
|
||||||
.into_string();
|
|
||||||
|
|
||||||
assert!(html.contains("gap: 0.5rem"));
|
assert!(html.contains("_flex-gap_0_5rem_"));
|
||||||
|
assert!(assets.contains("_flex-gap_0_5rem_{gap:0.5rem}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn gap_distinct_adds_row_and_column_gap_styles() {
|
async fn gap_distinct_adds_row_and_column_gap_styles() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let mut container = Container::new()
|
let mut container = Container::new()
|
||||||
.with_flex(Flex::row().with_gap(flex::Gap::Distinct {
|
.with_flex(Flex::new().with_gap(flex::Gap::Distinct {
|
||||||
row: UnitValue::Px(4),
|
row: UnitValue::Px(4),
|
||||||
column: UnitValue::Px(8),
|
column: UnitValue::Px(8),
|
||||||
}))
|
}))
|
||||||
.with_child(Lc::n("x"));
|
.with_child(Lc::n("x"));
|
||||||
let html = container
|
let html = container.render(&mut cx).await.into_string();
|
||||||
.render(&mut Context::default())
|
let assets = cx.render_assets().into_string();
|
||||||
.await
|
|
||||||
.into_string();
|
|
||||||
|
|
||||||
assert!(html.contains("row-gap: 4px"));
|
assert!(html.contains("_flex-row-gap_4px_"));
|
||||||
assert!(html.contains("column-gap: 8px"));
|
assert!(html.contains("_flex-column-gap_8px_"));
|
||||||
|
assert!(assets.contains("_flex-row-gap_4px_{row-gap:4px}"));
|
||||||
|
assert!(assets.contains("_flex-column-gap_8px_{column-gap:8px}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn gap_none_adds_no_gap_style() {
|
async fn gap_none_adds_no_gap_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let mut container = Container::new()
|
let mut container = Container::new()
|
||||||
.with_flex(Flex::row())
|
.with_flex(Flex::new())
|
||||||
.with_child(Lc::n("x"));
|
.with_child(Lc::n("x"));
|
||||||
let html = container
|
let html = container.render(&mut cx).await.into_string();
|
||||||
.render(&mut Context::default())
|
let assets = cx.render_assets().into_string();
|
||||||
.await
|
|
||||||
.into_string();
|
|
||||||
|
|
||||||
assert!(!html.contains("gap:"));
|
assert!(!html.contains("gap"));
|
||||||
|
assert!(!assets.contains("gap"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ async fn is_not_rendered_when_empty() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn nav_root_class_is_unaffected_by_content_flex() {
|
async fn nav_root_class_is_unaffected_by_content_flex() {
|
||||||
let mut navbar = Navbar::simple()
|
let mut navbar = Navbar::simple()
|
||||||
.with_flex(Flex::row().with_justify(flex::ContentJustify::End))
|
.with_flex(Flex::new().with_justify(flex::ContentJustify::End))
|
||||||
.with_item(navbar::Item::nav(one_link_nav()));
|
.with_item(navbar::Item::nav(one_link_nav()));
|
||||||
let html = navbar.render(&mut Context::default()).await.into_string();
|
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
|
|
@ -37,32 +37,42 @@ async fn without_flex_content_area_has_no_style_attribute() {
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn flex_adds_its_styles_to_the_content_area() {
|
async fn flex_adds_its_styles_to_the_content_area() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let mut navbar = Navbar::simple()
|
let mut navbar = Navbar::simple()
|
||||||
.with_flex(Flex::row().with_justify(flex::ContentJustify::End))
|
.with_flex(Flex::new().with_justify(flex::ContentJustify::End))
|
||||||
.with_item(navbar::Item::nav(one_link_nav()));
|
.with_item(navbar::Item::nav(one_link_nav()));
|
||||||
let html = navbar.render(&mut Context::default()).await.into_string();
|
let html = navbar.render(&mut cx).await.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert!(html.contains(r#"class="navbar-content""#));
|
assert!(html.contains("navbar-content"));
|
||||||
assert!(html.contains("display: flex"));
|
assert!(html.contains("_flex_"));
|
||||||
assert!(html.contains("justify-content: flex-end"));
|
assert!(html.contains("_flex-justify_flex-end_"));
|
||||||
|
assert!(assets.contains("_flex_{display:flex}"));
|
||||||
|
assert!(assets.contains("_flex-justify_flex-end_{justify-content:flex-end}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn flex_gap_adds_a_style_to_the_content_area() {
|
async fn flex_gap_adds_a_style_to_the_content_area() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let mut navbar = Navbar::simple()
|
let mut navbar = Navbar::simple()
|
||||||
.with_flex(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
.with_flex(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
.with_item(navbar::Item::nav(one_link_nav()));
|
.with_item(navbar::Item::nav(one_link_nav()));
|
||||||
let html = navbar.render(&mut Context::default()).await.into_string();
|
let html = navbar.render(&mut cx).await.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert!(html.contains("gap: 0.5rem"));
|
assert!(html.contains("_flex-gap_0_5rem_"));
|
||||||
|
assert!(assets.contains("_flex-gap_0_5rem_{gap:0.5rem}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Navbar + FlexItem::push_end >****************************************************************
|
// **< Navbar + FlexItem::push_end >****************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn push_end_adds_an_automatic_start_margin() {
|
async fn push_end_adds_an_automatic_start_margin() {
|
||||||
let mut nav = one_link_nav().with_prop(FlexItem::push_end());
|
let mut cx = Context::default();
|
||||||
let html = nav.render(&mut Context::default()).await.into_string();
|
let mut nav = one_link_nav().with_prop(FlexItem::push_end().into());
|
||||||
|
let html = nav.render(&mut cx).await.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert!(html.contains(r#"style="margin-inline-start: auto""#));
|
assert!(html.contains("_flex-item-offset_auto_"));
|
||||||
|
assert!(assets.contains("_flex-item-offset_auto_{margin-inline-start:auto}"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_default_renders_nothing() {
|
async fn props_default_renders_nothing() {
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (Props::default().unpack(&cx)) {} }.into_string(),
|
html! { span (Props::default().unpack(&mut cx)) {} }.into_string(),
|
||||||
"<span></span>"
|
"<span></span>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -45,9 +45,9 @@ async fn props_set_replaces_existing_value() {
|
||||||
async fn props_set_does_not_create_duplicate_key() {
|
async fn props_set_does_not_create_duplicate_key() {
|
||||||
// Reassigning the same key must replace the value, not add a duplicate entry.
|
// Reassigning the same key must replace the value, not add a duplicate entry.
|
||||||
let p = Props::new("key", "v1").with_prop(PropsOp::set("key", "v2"));
|
let p = Props::new("key", "v1").with_prop(PropsOp::set("key", "v2"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (p.unpack(&cx)) {} }.into_string(),
|
html! { span (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<span key="v2"></span>"#
|
r#"<span key="v2"></span>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -57,9 +57,9 @@ async fn props_set_preserves_insertion_order() {
|
||||||
let p = Props::new("a", "1")
|
let p = Props::new("a", "1")
|
||||||
.with_prop(PropsOp::set("b", "2"))
|
.with_prop(PropsOp::set("b", "2"))
|
||||||
.with_prop(PropsOp::set("c", "3"));
|
.with_prop(PropsOp::set("c", "3"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (p.unpack(&cx)) {} }.into_string(),
|
html! { span (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<span a="1" b="2" c="3"></span>"#
|
r#"<span a="1" b="2" c="3"></span>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -85,9 +85,9 @@ async fn props_remove_nonexistent_key_is_noop() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_renders_nothing_after_removing_last_attr() {
|
async fn props_renders_nothing_after_removing_last_attr() {
|
||||||
let p = Props::new("only", "one").with_prop(PropsOp::remove("only"));
|
let p = Props::new("only", "one").with_prop(PropsOp::remove("only"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (p.unpack(&cx)) {} }.into_string(),
|
html! { span (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
"<span></span>"
|
"<span></span>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -97,9 +97,9 @@ async fn props_renders_nothing_after_removing_last_attr() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_escapes_ampersand_and_angle_brackets_in_value() {
|
async fn props_escapes_ampersand_and_angle_brackets_in_value() {
|
||||||
let p = Props::new("data-info", "a&b<c>d");
|
let p = Props::new("data-info", "a&b<c>d");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (p.unpack(&cx)) {} }.into_string(),
|
html! { span (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<span data-info="a&b<c>d"></span>"#
|
r#"<span data-info="a&b<c>d"></span>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -107,9 +107,9 @@ async fn props_escapes_ampersand_and_angle_brackets_in_value() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_escapes_double_quotes_in_value() {
|
async fn props_escapes_double_quotes_in_value() {
|
||||||
let p = Props::new("data-label", r#"say "hello""#);
|
let p = Props::new("data-label", r#"say "hello""#);
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (p.unpack(&cx)) {} }.into_string(),
|
html! { span (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<span data-label="say "hello""></span>"#
|
r#"<span data-label="say "hello""></span>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -120,9 +120,9 @@ async fn props_escapes_double_quotes_in_value() {
|
||||||
async fn props_empty_in_html_macro_produces_no_attributes() {
|
async fn props_empty_in_html_macro_produces_no_attributes() {
|
||||||
// An empty Props must not emit even an extra blank space.
|
// An empty Props must not emit even an extra blank space.
|
||||||
let p = Props::default();
|
let p = Props::default();
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p.unpack(&cx)) { "x" } }.into_string(),
|
html! { button (p.unpack(&mut cx)) { "x" } }.into_string(),
|
||||||
"<button>x</button>"
|
"<button>x</button>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -130,9 +130,9 @@ async fn props_empty_in_html_macro_produces_no_attributes() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_single_attr_in_html_macro() {
|
async fn props_single_attr_in_html_macro() {
|
||||||
let p = Props::new("hx-get", "/api");
|
let p = Props::new("hx-get", "/api");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p.unpack(&cx)) { "Load" } }.into_string(),
|
html! { button (p.unpack(&mut cx)) { "Load" } }.into_string(),
|
||||||
r#"<button hx-get="/api">Load</button>"#
|
r#"<button hx-get="/api">Load</button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -142,9 +142,9 @@ async fn props_multiple_attrs_preserve_order_in_html_macro() {
|
||||||
let p = Props::new("hx-get", "/api")
|
let p = Props::new("hx-get", "/api")
|
||||||
.with_prop(PropsOp::set("hx-target", "#result"))
|
.with_prop(PropsOp::set("hx-target", "#result"))
|
||||||
.with_prop(PropsOp::set("hx-swap", "outerHTML"));
|
.with_prop(PropsOp::set("hx-swap", "outerHTML"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p.unpack(&cx)) {} }.into_string(),
|
html! { button (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r##"<button hx-get="/api" hx-target="#result" hx-swap="outerHTML"></button>"##
|
r##"<button hx-get="/api" hx-target="#result" hx-swap="outerHTML"></button>"##
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -153,9 +153,9 @@ async fn props_multiple_attrs_preserve_order_in_html_macro() {
|
||||||
async fn props_alongside_class_and_id_in_html_macro() {
|
async fn props_alongside_class_and_id_in_html_macro() {
|
||||||
// The unpack is always emitted after class and id, regardless of the order they are written in.
|
// The unpack is always emitted after class and id, regardless of the order they are written in.
|
||||||
let p = Props::new("hx-get", "/api");
|
let p = Props::new("hx-get", "/api");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button #mybtn .btn (p.unpack(&cx)) { "Go" } }.into_string(),
|
html! { button #mybtn .btn (p.unpack(&mut cx)) { "Go" } }.into_string(),
|
||||||
r#"<button class="btn" id="mybtn" hx-get="/api">Go</button>"#
|
r#"<button class="btn" id="mybtn" hx-get="/api">Go</button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -163,9 +163,9 @@ async fn props_alongside_class_and_id_in_html_macro() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_alongside_named_attr_renders_after_it() {
|
async fn props_alongside_named_attr_renders_after_it() {
|
||||||
let p = Props::new("hx-get", "/api");
|
let p = Props::new("hx-get", "/api");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button type="button" (p.unpack(&cx)) {} }.into_string(),
|
html! { button type="button" (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<button type="button" hx-get="/api"></button>"#
|
r#"<button type="button" hx-get="/api"></button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -176,31 +176,31 @@ async fn props_combined_via_chaining_instead_of_multiple_splices() {
|
||||||
// is a compile error); values from separate sources are combined by chaining `with_prop()`
|
// is a compile error); values from separate sources are combined by chaining `with_prop()`
|
||||||
// on one `Props`, not by splicing two of them.
|
// on one `Props`, not by splicing two of them.
|
||||||
let p = Props::new("hx-get", "/api").with_prop(PropsOp::set("hx-swap", "outerHTML"));
|
let p = Props::new("hx-get", "/api").with_prop(PropsOp::set("hx-swap", "outerHTML"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p.unpack(&cx)) {} }.into_string(),
|
html! { button (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<button hx-get="/api" hx-swap="outerHTML"></button>"#
|
r#"<button hx-get="/api" hx-swap="outerHTML"></button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_inline_construction_in_html_macro() {
|
async fn props_inline_construction_in_html_macro() {
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (Props::new("hx-get", "/api").unpack(&cx)) { "Go" } }.into_string(),
|
html! { button (Props::new("hx-get", "/api").unpack(&mut cx)) { "Go" } }.into_string(),
|
||||||
r#"<button hx-get="/api">Go</button>"#
|
r#"<button hx-get="/api">Go</button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_conditional_expression_in_html_macro() {
|
async fn props_conditional_expression_in_html_macro() {
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
for (active, expected) in [
|
for (active, expected) in [
|
||||||
(true, r#"<button hx-get="/api">x</button>"#),
|
(true, r#"<button hx-get="/api">x</button>"#),
|
||||||
(false, "<button>x</button>"),
|
(false, "<button>x</button>"),
|
||||||
] {
|
] {
|
||||||
let markup = html! {
|
let markup = html! {
|
||||||
button (if active { Props::new("hx-get", "/api") } else { Props::default() }.unpack(&cx)) { "x" }
|
button (if active { Props::new("hx-get", "/api") } else { Props::default() }.unpack(&mut cx)) { "x" }
|
||||||
};
|
};
|
||||||
assert_eq!(markup.into_string(), expected);
|
assert_eq!(markup.into_string(), expected);
|
||||||
}
|
}
|
||||||
|
|
@ -219,9 +219,9 @@ 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
|
// A literal `#id` on the element takes precedence; `Props`'s own id is silently omitted instead
|
||||||
// of producing a duplicate `id` attribute.
|
// of producing a duplicate `id` attribute.
|
||||||
let p = Props::default().with_id("from-props");
|
let p = Props::default().with_id("from-props");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { div #fixed (p.unpack(&cx)) {} }.into_string(),
|
html! { div #fixed (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<div id="fixed"></div>"#
|
r#"<div id="fixed"></div>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -229,9 +229,9 @@ async fn props_id_collision_with_literal_omits_props_id() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_class_collision_with_literal_omits_props_classes() {
|
async fn props_class_collision_with_literal_omits_props_classes() {
|
||||||
let p = Props::classes("from-props-a from-props-b");
|
let p = Props::classes("from-props-a from-props-b");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { div.fixed (p.unpack(&cx)) {} }.into_string(),
|
html! { div.fixed (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<div class="fixed"></div>"#
|
r#"<div class="fixed"></div>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -241,9 +241,9 @@ async fn props_style_collision_with_literal_omits_props_styles() {
|
||||||
let p = Props::default()
|
let p = Props::default()
|
||||||
.with_prop(PropsOp::add_style("color", "red"))
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
.with_prop(PropsOp::add_style("font-weight", "bold"));
|
.with_prop(PropsOp::add_style("font-weight", "bold"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { div style="color: blue" (p.unpack(&cx)) {} }.into_string(),
|
html! { div style="color: blue" (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<div style="color: blue"></div>"#
|
r#"<div style="color: blue"></div>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -251,9 +251,9 @@ async fn props_style_collision_with_literal_omits_props_styles() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_named_attr_collision_with_literal_omits_props_value() {
|
async fn props_named_attr_collision_with_literal_omits_props_value() {
|
||||||
let p = Props::default().with_prop(PropsOp::set("title", "from-props"));
|
let p = Props::default().with_prop(PropsOp::set("title", "from-props"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span title="literal" (p.unpack(&cx)) {} }.into_string(),
|
html! { span title="literal" (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<span title="literal"></span>"#
|
r#"<span title="literal"></span>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -321,9 +321,9 @@ async fn get_prop_id_matches_get_id() {
|
||||||
async fn props_hx_target_value_with_hash_renders_correctly() {
|
async fn props_hx_target_value_with_hash_renders_correctly() {
|
||||||
// Regression: r#"..."# used to close prematurely when it found `"#list"`.
|
// Regression: r#"..."# used to close prematurely when it found `"#list"`.
|
||||||
let p = Props::new("hx-target", "#list");
|
let p = Props::new("hx-target", "#list");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p.unpack(&cx)) {} }.into_string(),
|
html! { button (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r##"<button hx-target="#list"></button>"##
|
r##"<button hx-target="#list"></button>"##
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -331,9 +331,9 @@ async fn props_hx_target_value_with_hash_renders_correctly() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_with_empty_value_renders_attr_with_empty_value() {
|
async fn props_with_empty_value_renders_attr_with_empty_value() {
|
||||||
let p = Props::new("data-expanded", "");
|
let p = Props::new("data-expanded", "");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (p.unpack(&cx)) {} }.into_string(),
|
html! { span (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<span data-expanded=""></span>"#
|
r#"<span data-expanded=""></span>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -348,9 +348,9 @@ async fn props_chained_set_and_remove_yields_expected_state() {
|
||||||
assert_eq!(p.get_prop("a"), Some("updated".to_string()));
|
assert_eq!(p.get_prop("a"), Some("updated".to_string()));
|
||||||
assert_eq!(p.get_prop("b"), None);
|
assert_eq!(p.get_prop("b"), None);
|
||||||
assert_eq!(p.get_prop("c"), Some("3".to_string()));
|
assert_eq!(p.get_prop("c"), Some("3".to_string()));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (p.unpack(&cx)) {} }.into_string(),
|
html! { span (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<span a="updated" c="3"></span>"#
|
r#"<span a="updated" c="3"></span>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -359,9 +359,9 @@ async fn props_chained_set_and_remove_yields_expected_state() {
|
||||||
async fn props_with_empty_attr_name_renders_without_validation() {
|
async fn props_with_empty_attr_name_renders_without_validation() {
|
||||||
// Documented behavior: names are not validated; the resulting HTML is not standard.
|
// Documented behavior: names are not validated; the resulting HTML is not standard.
|
||||||
let p = Props::new("", "val");
|
let p = Props::new("", "val");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (p.unpack(&cx)) {} }.into_string(),
|
html! { span (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<span ="val"></span>"#
|
r#"<span ="val"></span>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -299,9 +299,9 @@ async fn get_prop_class_matches_get_classes() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_classes_renders_class_attribute() {
|
async fn props_classes_renders_class_attribute() {
|
||||||
let p = Props::classes("btn btn-primary");
|
let p = Props::classes("btn btn-primary");
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p.unpack(&cx)) { "OK" } }.into_string(),
|
html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
|
||||||
r#"<button class="btn btn-primary">OK</button>"#
|
r#"<button class="btn btn-primary">OK</button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -309,9 +309,9 @@ async fn props_classes_renders_class_attribute() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_classes_can_be_extended_with_add_classes() {
|
async fn props_classes_can_be_extended_with_add_classes() {
|
||||||
let p = Props::classes("btn").with_prop(PropsOp::add_classes("active"));
|
let p = Props::classes("btn").with_prop(PropsOp::add_classes("active"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p.unpack(&cx)) { "OK" } }.into_string(),
|
html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
|
||||||
r#"<button class="btn active">OK</button>"#
|
r#"<button class="btn active">OK</button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,9 @@ async fn extras_not_emitted_in_html() {
|
||||||
let props = Props::default()
|
let props = Props::default()
|
||||||
.with_prop(PropsOp::set_extra("ext.flag", true))
|
.with_prop(PropsOp::set_extra("ext.flag", true))
|
||||||
.with_prop(PropsOp::add_classes("btn"));
|
.with_prop(PropsOp::add_classes("btn"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (props.unpack(&cx)) { "OK" } }.into_string(),
|
html! { button (props.unpack(&mut cx)) { "OK" } }.into_string(),
|
||||||
r#"<button class="btn">OK</button>"#
|
r#"<button class="btn">OK</button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,93 +2,121 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn default_flex_item_adds_nothing() {
|
async fn default_flex_item_adds_nothing() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(FlexItem::new()));
|
let props = Props::default().with_prop(PropsOp::flex_item(FlexItem::new()));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
|
||||||
assert_eq!(props.get_classes(), None);
|
assert_eq!(html, "<span></span>");
|
||||||
assert_eq!(props.get_styles(), None);
|
assert!(cx.render_assets().into_string().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn grow_adds_flex_grow_style() {
|
async fn grow_adds_flex_grow_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_grow(flex::ItemGrow::Is1),
|
FlexItem::new().with_grow(flex::ItemGrow::Is1),
|
||||||
));
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(props.get_styles(), Some("flex-grow: 1".to_string()));
|
assert!(html.contains(r#"class="_flex-item-grow_1_""#));
|
||||||
|
assert!(assets.contains("_flex-item-grow_1_{flex-grow:1}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn shrink_adds_flex_shrink_style() {
|
async fn shrink_adds_flex_shrink_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_shrink(flex::ItemShrink::Is0),
|
FlexItem::new().with_shrink(flex::ItemShrink::Is0),
|
||||||
));
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(props.get_styles(), Some("flex-shrink: 0".to_string()));
|
assert!(html.contains(r#"class="_flex-item-shrink_0_""#));
|
||||||
|
assert!(assets.contains("_flex-item-shrink_0_{flex-shrink:0}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn align_self_adds_matching_style() {
|
async fn align_self_adds_matching_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_align_self(flex::ItemAlign::Center),
|
FlexItem::new().with_align_self(flex::ItemAlign::Center),
|
||||||
));
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(props.get_styles(), Some("align-self: center".to_string()));
|
assert!(html.contains(r#"class="_flex-item-align_center_""#));
|
||||||
|
assert!(assets.contains("_flex-item-align_center_{align-self:center}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn order_adds_matching_style() {
|
async fn order_adds_matching_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_order(flex::ItemOrder::First),
|
FlexItem::new().with_order(flex::ItemOrder::First),
|
||||||
));
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(props.get_styles(), Some("order: -129".to_string()));
|
assert!(html.contains(r#"class="_flex-item-order_-129_""#));
|
||||||
|
assert!(assets.contains("_flex-item-order_-129_{order:-129}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn size_percent_adds_flex_basis_style() {
|
async fn size_percent_adds_flex_basis_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_size(flex::ItemSize::Percent33),
|
FlexItem::new().with_size(flex::ItemSize::Percent33),
|
||||||
));
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(props.get_styles(), Some("flex-basis: 33.3333%".to_string()));
|
assert!(html.contains(r#"class="_flex-item-basis_33_3333pct_""#));
|
||||||
|
assert!(assets.contains("_flex-item-basis_33_3333pct_{flex-basis:33.3333%}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn size_custom_adds_flex_basis_style() {
|
async fn size_custom_adds_flex_basis_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::Zero)),
|
FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::Zero)),
|
||||||
));
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(props.get_classes(), None);
|
assert!(html.contains(r#"class="_flex-item-basis_0_""#));
|
||||||
assert_eq!(props.get_styles(), Some("flex-basis: 0".to_string()));
|
assert!(assets.contains("_flex-item-basis_0_{flex-basis:0}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn offset_percent_adds_margin_inline_start_style() {
|
async fn offset_percent_adds_margin_inline_start_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_offset(flex::ItemOffset::Percent33),
|
FlexItem::new().with_offset(flex::ItemOffset::Percent33),
|
||||||
));
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(html.contains(r#"class="_flex-item-offset_33_3333pct_""#));
|
||||||
props.get_styles(),
|
assert!(assets.contains("_flex-item-offset_33_3333pct_{margin-inline-start:33.3333%}"));
|
||||||
Some("margin-inline-start: 33.3333%".to_string())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn offset_custom_adds_margin_inline_start_style() {
|
async fn offset_custom_adds_margin_inline_start_style() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_offset(flex::ItemOffset::Custom(UnitValue::Px(16))),
|
FlexItem::new().with_offset(flex::ItemOffset::Custom(UnitValue::Px(16))),
|
||||||
));
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(html.contains(r#"class="_flex-item-offset_16px_""#));
|
||||||
props.get_styles(),
|
assert!(assets.contains("_flex-item-offset_16px_{margin-inline-start:16px}"));
|
||||||
Some("margin-inline-start: 16px".to_string())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn combines_several_facets_in_one_call() {
|
async fn combines_several_facets_in_one_call() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let props = Props::default().with_prop(PropsOp::flex_item(
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new()
|
FlexItem::new()
|
||||||
.with_grow(flex::ItemGrow::Is1)
|
.with_grow(flex::ItemGrow::Is1)
|
||||||
|
|
@ -98,22 +126,31 @@ async fn combines_several_facets_in_one_call() {
|
||||||
.with_size(flex::ItemSize::Custom(UnitValue::Zero))
|
.with_size(flex::ItemSize::Custom(UnitValue::Zero))
|
||||||
.with_offset(flex::ItemOffset::Percent10),
|
.with_offset(flex::ItemOffset::Percent10),
|
||||||
));
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(html.contains("_flex-item-grow_1_"));
|
||||||
props.get_styles(),
|
assert!(html.contains("_flex-item-shrink_0_"));
|
||||||
Some(
|
assert!(html.contains("_flex-item-align_flex-start_"));
|
||||||
"flex-grow: 1; flex-shrink: 0; align-self: flex-start; order: 2; flex-basis: 0; \
|
assert!(html.contains("_flex-item-order_2_"));
|
||||||
margin-inline-start: 10%"
|
assert!(html.contains("_flex-item-basis_0_"));
|
||||||
.to_string()
|
assert!(html.contains("_flex-item-offset_10pct_"));
|
||||||
)
|
assert!(assets.contains("_flex-item-grow_1_{flex-grow:1}"));
|
||||||
);
|
assert!(assets.contains("_flex-item-shrink_0_{flex-shrink:0}"));
|
||||||
assert_eq!(props.get_classes(), None);
|
assert!(assets.contains("_flex-item-align_flex-start_{align-self:flex-start}"));
|
||||||
|
assert!(assets.contains("_flex-item-order_2_{order:2}"));
|
||||||
|
assert!(assets.contains("_flex-item-basis_0_{flex-basis:0}"));
|
||||||
|
assert!(assets.contains("_flex-item-offset_10pct_{margin-inline-start:10%}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn from_flex_item_for_props_op() {
|
async fn from_flex_item_for_props_op() {
|
||||||
|
let mut cx = Context::default();
|
||||||
let item = FlexItem::new().with_grow(flex::ItemGrow::Is1);
|
let item = FlexItem::new().with_grow(flex::ItemGrow::Is1);
|
||||||
let props = Props::default().with_prop(item.into());
|
let props = Props::default().with_prop(item.into());
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
assert_eq!(props.get_styles(), Some("flex-grow: 1".to_string()));
|
assert!(html.contains(r#"class="_flex-item-grow_1_""#));
|
||||||
|
assert!(assets.contains("_flex-item-grow_1_{flex-grow:1}"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -275,9 +275,9 @@ async fn props_styles_renders_style_attribute() {
|
||||||
let p = Props::default()
|
let p = Props::default()
|
||||||
.with_prop(PropsOp::add_style("color", "red"))
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
.with_prop(PropsOp::add_style("font-weight", "bold"));
|
.with_prop(PropsOp::add_style("font-weight", "bold"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p.unpack(&cx)) { "OK" } }.into_string(),
|
html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
|
||||||
r#"<button style="color: red; font-weight: bold">OK</button>"#
|
r#"<button style="color: red; font-weight: bold">OK</button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -289,9 +289,9 @@ async fn props_styles_render_after_class_and_before_other_attrs() {
|
||||||
.with_prop(PropsOp::add_classes("btn"))
|
.with_prop(PropsOp::add_classes("btn"))
|
||||||
.with_prop(PropsOp::add_style("color", "red"))
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
.with_prop(PropsOp::set("data-x", "1"));
|
.with_prop(PropsOp::set("data-x", "1"));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { button (p.unpack(&cx)) { "OK" } }.into_string(),
|
html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
|
||||||
r#"<button id="main" class="btn" style="color: red" data-x="1">OK</button>"#
|
r#"<button id="main" class="btn" style="color: red" data-x="1">OK</button>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -299,9 +299,9 @@ async fn props_styles_render_after_class_and_before_other_attrs() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn props_styles_escapes_double_quotes_in_value() {
|
async fn props_styles_escapes_double_quotes_in_value() {
|
||||||
let p = Props::default().with_prop(PropsOp::add_style("content", r#""hi""#));
|
let p = Props::default().with_prop(PropsOp::add_style("content", r#""hi""#));
|
||||||
let cx = Context::default();
|
let mut cx = Context::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
html! { span (p.unpack(&cx)) {} }.into_string(),
|
html! { span (p.unpack(&mut cx)) {} }.into_string(),
|
||||||
r#"<span style="content: "hi""></span>"#
|
r#"<span style="content: "hi""></span>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -357,9 +357,9 @@ async fn render_has_no_line_breaks() {
|
||||||
async fn context_add_responsive_style_feeds_responsives() {
|
async fn context_add_responsive_style_feeds_responsives() {
|
||||||
let cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
|
let cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
|
||||||
Some(Breakpoint::Md),
|
Some(Breakpoint::Md),
|
||||||
"col",
|
"col".into(),
|
||||||
"flex-basis",
|
"flex-basis".into(),
|
||||||
"50%",
|
"50%".into(),
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
||||||
|
|
@ -372,15 +372,15 @@ async fn context_add_responsive_style_accumulates_across_calls() {
|
||||||
let cx = Context::default()
|
let cx = Context::default()
|
||||||
.with_assets(AssetsOp::AddResponsiveStyle(
|
.with_assets(AssetsOp::AddResponsiveStyle(
|
||||||
Some(Breakpoint::Md),
|
Some(Breakpoint::Md),
|
||||||
"col",
|
"col".into(),
|
||||||
"flex-basis",
|
"flex-basis".into(),
|
||||||
"50%",
|
"50%".into(),
|
||||||
))
|
))
|
||||||
.with_assets(AssetsOp::AddResponsiveStyle(
|
.with_assets(AssetsOp::AddResponsiveStyle(
|
||||||
Some(Breakpoint::Md),
|
Some(Breakpoint::Md),
|
||||||
"col",
|
"col".into(),
|
||||||
"margin-inline-start",
|
"margin-inline-start".into(),
|
||||||
"0",
|
"0".into(),
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
||||||
|
|
@ -399,9 +399,9 @@ async fn context_default_has_no_responsive_styles() {
|
||||||
async fn render_assets_includes_style_tag_with_responsive_styles() {
|
async fn render_assets_includes_style_tag_with_responsive_styles() {
|
||||||
let mut cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
|
let mut cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
|
||||||
Some(Breakpoint::Xs),
|
Some(Breakpoint::Xs),
|
||||||
"col",
|
"col".into(),
|
||||||
"flex-basis",
|
"flex-basis".into(),
|
||||||
"100%",
|
"100%".into(),
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cx.render_assets().into_string(),
|
cx.render_assets().into_string(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue