Compare commits
No commits in common. "0121fad94ac596ee4ce6e0dba3db788c86c9fa9d" and "0410b8c060e2622dcfca12589d5f8ecb5ef147a7" have entirely different histories.
0121fad94a
...
0410b8c060
5 changed files with 81 additions and 66 deletions
|
|
@ -203,11 +203,9 @@ impl StaticFilesBundle {
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
{
|
{
|
||||||
// Crea un directorio temporal único para el archivo CSS (basado en su nombre, para que
|
// Crea un directorio temporal para el archivo CSS.
|
||||||
// varias llamadas a from_scss en el mismo build.rs no se pisen).
|
|
||||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
let safe_name = target_name.replace(['.', '-'], "_");
|
let temp_dir = Path::new(&out_dir).join("from_scss_files");
|
||||||
let temp_dir = Path::new(&out_dir).join(format!("from_scss_{safe_name}"));
|
|
||||||
|
|
||||||
// Limpia el directorio temporal de ejecuciones previas, si existe.
|
// Limpia el directorio temporal de ejecuciones previas, si existe.
|
||||||
if temp_dir.exists() {
|
if temp_dir.exists() {
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,6 @@ impl DiagnosticParse for Element {
|
||||||
|| input.peek(Lit)
|
|| input.peek(Lit)
|
||||||
|| input.peek(Dot)
|
|| input.peek(Dot)
|
||||||
|| input.peek(Pound)
|
|| input.peek(Pound)
|
||||||
|| input.peek(Paren)
|
|
||||||
{
|
{
|
||||||
let attr = input.diagnostic_parse(diagnostics)?;
|
let attr = input.diagnostic_parse(diagnostics)?;
|
||||||
|
|
||||||
|
|
@ -347,10 +346,6 @@ pub enum Attribute {
|
||||||
name: HtmlName,
|
name: HtmlName,
|
||||||
attr_type: AttributeType,
|
attr_type: AttributeType,
|
||||||
},
|
},
|
||||||
Splice {
|
|
||||||
paren_token: Paren,
|
|
||||||
expr: Expr,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiagnosticParse for Attribute {
|
impl DiagnosticParse for Attribute {
|
||||||
|
|
@ -379,12 +374,6 @@ impl DiagnosticParse for Attribute {
|
||||||
pound_token: input.parse()?,
|
pound_token: input.parse()?,
|
||||||
name: input.diagnostic_parse(diagnostics)?,
|
name: input.diagnostic_parse(diagnostics)?,
|
||||||
})
|
})
|
||||||
} else if lookahead.peek(Paren) {
|
|
||||||
let content;
|
|
||||||
Ok(Self::Splice {
|
|
||||||
paren_token: parenthesized!(content in input),
|
|
||||||
expr: content.parse()?,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
let name = input.diagnostic_parse::<HtmlName>(diagnostics)?;
|
let name = input.diagnostic_parse::<HtmlName>(diagnostics)?;
|
||||||
|
|
||||||
|
|
@ -435,11 +424,6 @@ impl ToTokens for Attribute {
|
||||||
name.to_tokens(tokens);
|
name.to_tokens(tokens);
|
||||||
attr_type.to_tokens(tokens);
|
attr_type.to_tokens(tokens);
|
||||||
}
|
}
|
||||||
Self::Splice { paren_token, expr } => {
|
|
||||||
paren_token.surround(tokens, |tokens| {
|
|
||||||
expr.to_tokens(tokens);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ impl Generator {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attrs(&self, attrs: Vec<Attribute>, build: &mut Builder) {
|
fn attrs(&self, attrs: Vec<Attribute>, build: &mut Builder) {
|
||||||
let (classes, id, named_attrs, spliced) = split_attrs(attrs);
|
let (classes, id, named_attrs) = split_attrs(attrs);
|
||||||
|
|
||||||
if !classes.is_empty() {
|
if !classes.is_empty() {
|
||||||
let mut toggle_class_exprs = vec![];
|
let mut toggle_class_exprs = vec![];
|
||||||
|
|
@ -184,9 +184,6 @@ impl Generator {
|
||||||
for (name, attr_type) in named_attrs {
|
for (name, attr_type) in named_attrs {
|
||||||
self.attr(name, attr_type, build);
|
self.attr(name, attr_type, build);
|
||||||
}
|
}
|
||||||
for expr in spliced {
|
|
||||||
self.splice(expr, build);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn control_flow<E: Into<Element>>(&self, control_flow: ControlFlow<E>, build: &mut Builder) {
|
fn control_flow<E: Into<Element>>(&self, control_flow: ControlFlow<E>, build: &mut Builder) {
|
||||||
|
|
@ -319,12 +316,10 @@ fn split_attrs(
|
||||||
Vec<(HtmlNameOrMarkup, Option<Expr>)>,
|
Vec<(HtmlNameOrMarkup, Option<Expr>)>,
|
||||||
Option<HtmlNameOrMarkup>,
|
Option<HtmlNameOrMarkup>,
|
||||||
Vec<(HtmlName, AttributeType)>,
|
Vec<(HtmlName, AttributeType)>,
|
||||||
Vec<Expr>,
|
|
||||||
) {
|
) {
|
||||||
let mut classes = vec![];
|
let mut classes = vec![];
|
||||||
let mut id = None;
|
let mut id = None;
|
||||||
let mut named_attrs = vec![];
|
let mut named_attrs = vec![];
|
||||||
let mut spliced = vec![];
|
|
||||||
|
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
match attr {
|
match attr {
|
||||||
|
|
@ -333,11 +328,10 @@ fn split_attrs(
|
||||||
}
|
}
|
||||||
Attribute::Id { name, .. } => id = Some(name),
|
Attribute::Id { name, .. } => id = Some(name),
|
||||||
Attribute::Named { name, attr_type } => named_attrs.push((name, attr_type)),
|
Attribute::Named { name, attr_type } => named_attrs.push((name, attr_type)),
|
||||||
Attribute::Splice { expr, .. } => spliced.push(expr),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(classes, id, named_attrs, spliced)
|
(classes, id, named_attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{AutoDefault, builder_fn, util};
|
use crate::{AutoDefault, CowStr, builder_fn, util};
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
|
@ -7,27 +7,6 @@ use std::collections::HashSet;
|
||||||
/// Cada variante opera sobre **una o más clases** proporcionadas como una cadena separada por
|
/// Cada variante opera sobre **una o más clases** proporcionadas como una cadena separada por
|
||||||
/// espacios (p. ej. `"btn active"`), que se normalizan internamente a minúsculas en
|
/// espacios (p. ej. `"btn active"`), que se normalizan internamente a minúsculas en
|
||||||
/// [`Classes::with_classes()`].
|
/// [`Classes::with_classes()`].
|
||||||
///
|
|
||||||
/// # Orden de las clases y CSS
|
|
||||||
///
|
|
||||||
/// El navegador aplica los estilos según la especificidad de los selectores y el orden en que las
|
|
||||||
/// reglas aparecen en la **hoja de estilos**, no por el orden de las clases en el atributo `class`.
|
|
||||||
/// Por tanto, `"btn active"` y `"active btn"` producen exactamente el mismo resultado visual.
|
|
||||||
///
|
|
||||||
/// Las operaciones [`Add`](Self::Add) y [`Prepend`](Self::Prepend) permiten controlar ese orden
|
|
||||||
/// únicamente por legibilidad o por convención de proyecto, no porque afecte al comportamiento
|
|
||||||
/// del navegador.
|
|
||||||
///
|
|
||||||
/// # Reemplazar una clase
|
|
||||||
///
|
|
||||||
/// Para sustituir una clase por otra encadena [`Remove`](Self::Remove) y [`Add`](Self::Add):
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// let c = Classes::new("btn btn-primary active")
|
|
||||||
/// .with_classes(ClassesOp::Remove, "btn-primary")
|
|
||||||
/// .with_classes(ClassesOp::Add, "btn-secondary");
|
|
||||||
/// assert_eq!(c.get(), Some("btn active btn-secondary".to_string()));
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Debug, PartialEq)]
|
#[derive(AutoDefault, Clone, Debug, PartialEq)]
|
||||||
pub enum ClassesOp {
|
pub enum ClassesOp {
|
||||||
/// Añade las clases que no existan al final.
|
/// Añade las clases que no existan al final.
|
||||||
|
|
@ -37,6 +16,9 @@ pub enum ClassesOp {
|
||||||
Prepend,
|
Prepend,
|
||||||
/// Elimina las clases indicadas que existan.
|
/// Elimina las clases indicadas que existan.
|
||||||
Remove,
|
Remove,
|
||||||
|
/// Sustituye una o varias clases existentes (indicadas en la variante) por las clases
|
||||||
|
/// proporcionadas.
|
||||||
|
Replace(CowStr),
|
||||||
/// Alterna presencia/ausencia de una o más clases.
|
/// Alterna presencia/ausencia de una o más clases.
|
||||||
///
|
///
|
||||||
/// Si en una misma llamada se repite una clase (p. ej. `"a a"`) que ya existe, el resultado
|
/// Si en una misma llamada se repite una clase (p. ej. `"a a"`) que ya existe, el resultado
|
||||||
|
|
@ -44,7 +26,7 @@ pub enum ClassesOp {
|
||||||
/// final).
|
/// final).
|
||||||
Toggle,
|
Toggle,
|
||||||
/// Sustituye la lista completa por las clases indicadas.
|
/// Sustituye la lista completa por las clases indicadas.
|
||||||
Reset,
|
Set,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lista de clases CSS normalizadas para el atributo `class` de HTML.
|
/// Lista de clases CSS normalizadas para el atributo `class` de HTML.
|
||||||
|
|
@ -54,8 +36,8 @@ pub enum ClassesOp {
|
||||||
///
|
///
|
||||||
/// # Normalización
|
/// # Normalización
|
||||||
///
|
///
|
||||||
/// - El orden de las clases no afecta al resultado en CSS; las operaciones de ordenación
|
/// - Aunque el orden de las clases en el atributo `class` no afecta al resultado en CSS,
|
||||||
/// ([`Add`](ClassesOp::Add), [`Prepend`](ClassesOp::Prepend)) son puramente estéticas.
|
/// [`ClassesOp`] ofrece operaciones para controlar su orden de aparición por legibilidad.
|
||||||
/// - Solo se acepta una lista de clases con caracteres ASCII.
|
/// - Solo se acepta una lista de clases con caracteres ASCII.
|
||||||
/// - Las clases se almacenan en minúsculas.
|
/// - Las clases se almacenan en minúsculas.
|
||||||
/// - No se permiten clases duplicadas tras la normalización (por ejemplo, `Btn` y `btn` se
|
/// - No se permiten clases duplicadas tras la normalización (por ejemplo, `Btn` y `btn` se
|
||||||
|
|
@ -69,8 +51,7 @@ pub enum ClassesOp {
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// let classes = Classes::new("Btn btn-primary")
|
/// let classes = Classes::new("Btn btn-primary")
|
||||||
/// .with_classes(ClassesOp::Add, "Active")
|
/// .with_classes(ClassesOp::Add, "Active")
|
||||||
/// .with_classes(ClassesOp::Remove, "active")
|
/// .with_classes(ClassesOp::Replace("active".into()), "Disabled")
|
||||||
/// .with_classes(ClassesOp::Add, "Disabled")
|
|
||||||
/// .with_classes(ClassesOp::Remove, "btn-primary");
|
/// .with_classes(ClassesOp::Remove, "btn-primary");
|
||||||
///
|
///
|
||||||
/// assert_eq!(classes.get(), Some("btn disabled".to_string()));
|
/// assert_eq!(classes.get(), Some("btn disabled".to_string()));
|
||||||
|
|
@ -128,6 +109,26 @@ impl Classes {
|
||||||
}
|
}
|
||||||
self.0.retain(|c| !to_remove.contains(c.as_str()));
|
self.0.retain(|c| !to_remove.contains(c.as_str()));
|
||||||
}
|
}
|
||||||
|
ClassesOp::Replace(classes_to_replace) => {
|
||||||
|
let Some(classes_to_replace) = util::normalize_ascii_or_empty(
|
||||||
|
classes_to_replace.as_ref(),
|
||||||
|
"ClassesOp::Replace",
|
||||||
|
) else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
let mut pos = self.0.len();
|
||||||
|
let mut replaced = false;
|
||||||
|
for class in classes_to_replace.as_ref().split_ascii_whitespace() {
|
||||||
|
if let Some(replace_pos) = self.0.iter().position(|c| c == class) {
|
||||||
|
self.0.remove(replace_pos);
|
||||||
|
pos = pos.min(replace_pos);
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if replaced {
|
||||||
|
self.add(normalized.as_ref().split_ascii_whitespace(), pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
ClassesOp::Toggle => {
|
ClassesOp::Toggle => {
|
||||||
for class in normalized.as_ref().split_ascii_whitespace() {
|
for class in normalized.as_ref().split_ascii_whitespace() {
|
||||||
if let Some(pos) = self.0.iter().position(|c| c == class) {
|
if let Some(pos) = self.0.iter().position(|c| c == class) {
|
||||||
|
|
@ -137,7 +138,7 @@ impl Classes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClassesOp::Reset => {
|
ClassesOp::Set => {
|
||||||
self.0.clear();
|
self.0.clear();
|
||||||
self.add(normalized.as_ref().split_ascii_whitespace(), 0);
|
self.add(normalized.as_ref().split_ascii_whitespace(), 0);
|
||||||
}
|
}
|
||||||
|
|
@ -167,11 +168,6 @@ impl Classes {
|
||||||
|
|
||||||
// **< Classes GETTERS >************************************************************************
|
// **< Classes GETTERS >************************************************************************
|
||||||
|
|
||||||
/// Devuelve `true` si no hay clases.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.0.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la cadena de clases, si existe.
|
/// Devuelve la cadena de clases, si existe.
|
||||||
pub fn get(&self) -> Option<String> {
|
pub fn get(&self) -> Option<String> {
|
||||||
if self.0.is_empty() {
|
if self.0.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -79,19 +79,19 @@ async fn classes_prepend_ignores_empty_input() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_reset_replaces_entire_list_and_dedups() {
|
async fn classes_set_replaces_entire_list_and_dedups() {
|
||||||
let c = Classes::new("a b c").with_classes(ClassesOp::Reset, "X y y Z");
|
let c = Classes::new("a b c").with_classes(ClassesOp::Set, "X y y Z");
|
||||||
assert_classes(&c, Some("x y z"));
|
assert_classes(&c, Some("x y z"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_reset_with_empty_input_clears() {
|
async fn classes_set_with_empty_input_clears() {
|
||||||
let base = Classes::new("a b");
|
let base = Classes::new("a b");
|
||||||
let c = base.with_classes(ClassesOp::Reset, " \n ");
|
let c = base.with_classes(ClassesOp::Set, " \n ");
|
||||||
assert_classes(&c, None);
|
assert_classes(&c, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Mutation operations (remove/toggle) >********************************************************
|
// **< Mutation operations (remove/toggle/replace) >************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn classes_remove_is_case_insensitive() {
|
async fn classes_remove_is_case_insensitive() {
|
||||||
|
|
@ -138,6 +138,49 @@ async fn classes_toggle_duplicate_tokens_are_applied_sequentially() {
|
||||||
assert_classes(&c, Some("b a"));
|
assert_classes(&c, Some("b a"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn classes_replace_removes_targets_and_inserts_new_at_min_position() {
|
||||||
|
let c = Classes::new("a b c d").with_classes(ClassesOp::Replace("c a".into()), "x y");
|
||||||
|
assert_classes(&c, Some("x y b d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn classes_replace_when_none_found_does_nothing() {
|
||||||
|
let c = Classes::new("a b").with_classes(ClassesOp::Replace("x y".into()), "c d");
|
||||||
|
assert_classes(&c, Some("a b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn classes_replace_is_case_insensitive_on_targets_and_new_values_are_normalized() {
|
||||||
|
let c = Classes::new("btn btn-primary active")
|
||||||
|
.with_classes(ClassesOp::Replace("BTN-PRIMARY".into()), "Btn-Secondary");
|
||||||
|
assert_classes(&c, Some("btn btn-secondary active"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn classes_replace_with_empty_new_removes_only() {
|
||||||
|
let c = Classes::new("a b c").with_classes(ClassesOp::Replace("b".into()), " ");
|
||||||
|
assert_classes(&c, Some("a c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn classes_replace_dedups_against_existing_items() {
|
||||||
|
let c = Classes::new("a b c").with_classes(ClassesOp::Replace("b".into()), "c d");
|
||||||
|
assert_classes(&c, Some("a d c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn classes_replace_ignores_target_whitespace_and_repetition() {
|
||||||
|
let c = Classes::new("a b c").with_classes(ClassesOp::Replace(" b b ".into()), "x y");
|
||||||
|
assert_classes(&c, Some("a x y c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn classes_replace_rejects_non_ascii_targets_is_noop() {
|
||||||
|
let c = Classes::new("a b c").with_classes(ClassesOp::Replace("b ñ".into()), "x");
|
||||||
|
assert_classes(&c, Some("a b c"));
|
||||||
|
}
|
||||||
|
|
||||||
// **< Queries (contains) >*************************************************************************
|
// **< Queries (contains) >*************************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue