Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/core/css_builder/css_builder_in_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,87 @@ mod tests {
assert_eq!(result[0].name, "test");
assert!(result[0].content.eq(".display\\=flex{display:flex;}"));
}

#[test]
fn test_builder_with_templated_scroll_invocation() {
let mut scrolls_map: HashMap<String, Vec<String>> = HashMap::new();
scrolls_map.insert(
"complex-card".to_string(),
vec!["h=$".to_string(), "c=$".to_string(), "w=$".to_string()],
);

let config = ConfigInMemory {
projects: vec![ConfigInMemoryEntry {
name: "test".to_string(),
content: vec!["<div g!complex-card=120px_red_100px;></div>".to_string()],
}],
variables: None,
scrolls: Some(scrolls_map),
custom_animations: HashMap::new(),
browserslist_content: None,
};

let optimizer = MockOptimizer;
let mut builder = CssBuilderInMemory::new(&config, &optimizer).unwrap();
let result = builder.build().unwrap();

assert_eq!(result.len(), 1);
let css = &result[0].content;

// The output must use the outer template selector, not the inner scroll spell selectors.
assert!(css.contains(".g\\!complex-card\\=120px\\_red\\_100px\\;{height:120px;}"));
assert!(css.contains(".g\\!complex-card\\=120px\\_red\\_100px\\;{color:red;}"));
assert!(css.contains(".g\\!complex-card\\=120px\\_red\\_100px\\;{width:100px;}"));

assert!(!css.contains(".h\\=120px"));
assert!(!css.contains(".c\\=red"));
assert!(!css.contains(".w\\=100px"));
}

#[test]
fn test_builder_with_templated_scroll_invocation_with_prefixes() {
let mut scrolls_map: HashMap<String, Vec<String>> = HashMap::new();
scrolls_map.insert(
"complex-card".to_string(),
vec!["h=$".to_string(), "c=$".to_string(), "w=$".to_string()],
);

let config = ConfigInMemory {
projects: vec![ConfigInMemoryEntry {
name: "test".to_string(),
// Prefixes live on the scroll invocation and must apply to all expanded spells.
// - md__ => @media (min-width: 768px)
// - hover: => :hover pseudo
// - {_>_p} => " > p" focus selector
content: vec![
"<div g!md__{_>_p}hover:complex-card=120px_red_100px;></div>".to_string(),
],
}],
variables: None,
scrolls: Some(scrolls_map),
custom_animations: HashMap::new(),
browserslist_content: None,
};

let optimizer = MockOptimizer;
let mut builder = CssBuilderInMemory::new(&config, &optimizer).unwrap();
let result = builder.build().unwrap();

assert_eq!(result.len(), 1);
let css = &result[0].content;

// Ensure the area prefix becomes a media query.
assert!(css.contains("@media (min-width: 768px)"));
// Ensure effects+focus survive the selector replacement.
assert!(css.contains(":hover > p"));

// Ensure the outer template selector is used (not inner h=/c=/w= selectors).
assert!(css.contains(
".g\\!md\\_\\_\\{\\_\\>\\_p\\}hover\\:complex-card\\=120px\\_red\\_100px\\;"
));

assert!(!css.contains(".md\\_\\_\\{\\_\\>\\_p\\}hover\\:h\\=120px"));
assert!(!css.contains(".md\\_\\_\\{\\_\\>\\_p\\}hover\\:c\\=red"));
assert!(!css.contains(".md\\_\\_\\{\\_\\>\\_p\\}hover\\:w\\=100px"));
}
}
83 changes: 82 additions & 1 deletion src/core/spell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,62 @@ impl Spell {
// Template spell: keep outer spell and parse inner spells.
if with_template && !raw_spell_split.is_empty() {
let mut scroll_spells: Vec<Spell> = Vec::new();

for rs in raw_spell_split {
if let Some(spell) = Spell::new(rs, shared_spells, scrolls, span, source.clone())? {
scroll_spells.push(spell);
let mut spell = spell;

// If a template part is a scroll invocation (e.g. complex-card=120px_red_100px),
// `Spell::new` will produce a *container spell* whose `scroll_spells` are the
// real property spells.
//
// For templates we want to flatten those property spells into the template list
// so the builder can generate CSS and unify the class name to the outer template.
let area = spell.area().to_string();
let focus = spell.focus().to_string();
let effects = spell.effects().to_string();

if let Some(inner_scroll_spells) = spell.scroll_spells.take() {
let has_prefix =
!area.is_empty() || !focus.is_empty() || !effects.is_empty();

if has_prefix {
let mut prefix = String::new();

if !area.is_empty() {
prefix.push_str(&area);
prefix.push_str("__");
}

if !focus.is_empty() {
prefix.push('{');
prefix.push_str(&focus);
prefix.push('}');
}

if !effects.is_empty() {
prefix.push_str(&effects);
prefix.push(':');
}

for inner in inner_scroll_spells {
let combined = format!("{prefix}{}", inner.raw_spell);
if let Some(reparsed) = Spell::new(
&combined,
shared_spells,
scrolls,
span,
source.clone(),
)? {
scroll_spells.push(reparsed);
}
}
} else {
scroll_spells.extend(inner_scroll_spells);
}
} else {
scroll_spells.push(spell);
}
}
}

Expand Down Expand Up @@ -453,6 +506,34 @@ mod tests {
assert_eq!(spells[1].component_target(), "flex");
}

#[test]
fn test_scroll_can_be_used_inside_template_attribute() {
let shared_spells = HashSet::new();
let mut scrolls_map: HashMap<String, Vec<String>> = HashMap::new();
scrolls_map.insert(
"complex-card".to_string(),
vec!["h=$".to_string(), "c=$".to_string(), "w=$".to_string()],
);
let scrolls = Some(scrolls_map);

// This is the desired HTML usage pattern: use scroll invocation via g! ... ;
// (i.e. not inside class="...").
let raw = "g!complex-card=120px_red_100px;";
let spell = Spell::new(raw, &shared_spells, &scrolls, (0, 0), None)
.expect("parse ok")
.expect("not None");

assert!(spell.with_template);
let spells = spell.scroll_spells.as_ref().expect("template spells");
assert_eq!(spells.len(), 3);
assert_eq!(spells[0].component(), "h");
assert_eq!(spells[0].component_target(), "120px");
assert_eq!(spells[1].component(), "c");
assert_eq!(spells[1].component_target(), "red");
assert_eq!(spells[2].component(), "w");
assert_eq!(spells[2].component_target(), "100px");
}

#[test]
fn test_non_grimoire_plain_class_is_ignored() {
let shared_spells = HashSet::new();
Expand Down
Loading