@@ -257,7 +257,7 @@ fn error_unclosed_bracket() {
259fn error_recursive_function() {
259fn error_recursive_function() {
260− let input = "@fn loop\n @loop\n@loop";
260+ let input = "@let loop\n @loop\n@loop";
261 let diags = parse_diagnostics(input);
261 let diags = parse_diagnostics(input);
@@ -348,13 +348,13 @@ fn else_if_chain() {
350fn fn_default_used() {
350fn fn_default_used() {
351− let output = compile("@fn test $x=hello\n @text $x\n@test");
351+ let output = compile("@let test $x=hello\n @text $x\n@test");
352 assert!(output.contains("hello"));
352 assert!(output.contains("hello"));
356fn fn_default_overridden() {
356fn fn_default_overridden() {
357− let output = compile("@fn test $x=hello\n @text $x\n@test [x world]");
357+ let output = compile("@let test $x=hello\n @text $x\n@test [x world]");
358 assert!(output.contains("world"));
358 assert!(output.contains("world"));
359 assert!(!output.contains("hello"));
359 assert!(!output.contains("hello"));
@@ -446,7 +446,7 @@ fn no_warning_used_variable() {
448fn warning_unused_function() {
448fn warning_unused_function() {
449− let diags = parse_diagnostics("@fn card\n @el [padding 10]\n@el");
449+ let diags = parse_diagnostics("@let card\n @el [padding 10]\n@el");
@@ -458,7 +458,7 @@ fn warning_unused_function() {
460fn no_warning_used_function() {
460fn no_warning_used_function() {
461− let diags = parse_diagnostics("@fn card\n @el [padding 10]\n@card");
461+ let diags = parse_diagnostics("@let card\n @el [padding 10]\n@card");
463 !diags.iter().any(|d| d.message.contains("unused function")),
463 !diags.iter().any(|d| d.message.contains("unused function")),
464 "should not warn about used function, got: {:?}",
464 "should not warn about used function, got: {:?}",
@@ -468,19 +468,23 @@ fn no_warning_used_function() {
470fn warning_unused_define() {
470fn warning_unused_define() {
471− let diags = parse_diagnostics("@define card-style [padding 10]\n@el");
471+ let diags = parse_diagnostics("@let card-style [padding 10]\n@el");
473− diags.iter().any(|d| d.message.contains("unused define")),
475+ .any(|d| d.message.contains("unused attribute bundle")),
474− "expected unused define warning, got: {:?}",
476+ "expected unused attribute bundle warning, got: {:?}",
480fn no_warning_used_define() {
482fn no_warning_used_define() {
481− let diags = parse_diagnostics("@define card-style [padding 10]\n@el [$card-style]");
483+ let diags = parse_diagnostics("@let card-style [padding 10]\n@el [$card-style]");
483− !diags.iter().any(|d| d.message.contains("unused define")),
487+ .any(|d| d.message.contains("unused attribute bundle")),
484 "should not warn about used define, got: {:?}",
488 "should not warn about used define, got: {:?}",
@@ -559,7 +563,7 @@ fn each_range_with_index() {
560fn named_slot_basic() {
564fn named_slot_basic() {
561 let output = compile(
565 let output = compile(
562− "@fn card\n @el\n @slot header\n @children\n@card\n @slot header\n @text Title\n @text Body",
566+ "@let card\n @el\n @slot header\n @children\n@card\n @slot header\n @text Title\n @text Body",
564 assert!(output.contains("Title"));
568 assert!(output.contains("Title"));
565 assert!(output.contains("Body"));
569 assert!(output.contains("Body"));
@@ -568,7 +572,7 @@ fn named_slot_basic() {
569fn named_slot_default_content() {
573fn named_slot_default_content() {
570 let output = compile(
574 let output = compile(
571− "@fn card\n @el\n @slot header\n @text Default\n @children\n@card\n @text Body",
575+ "@let card\n @el\n @slot header\n @text Default\n @children\n@card\n @text Body",
573 assert!(output.contains("Default"));
577 assert!(output.contains("Default"));
574 assert!(output.contains("Body"));
578 assert!(output.contains("Body"));
@@ -3094,7 +3098,7 @@ fn test_theme_directive() {
3095fn test_deprecated_fn() {
3099fn test_deprecated_fn() {
3096 let result = htmlang::parser::parse(
3100 let result = htmlang::parser::parse(
3097− "@deprecated Use @new-card instead\n@fn old-card $title\n @text $title\n\n@old-card [title Hello]",
3101+ "@deprecated Use @new-card instead\n@let old-card $title\n @text $title\n\n@old-card [title Hello]",
3099 let warnings: Vec<_> = result
3103 let warnings: Vec<_> = result
3100 .diagnostics
3104 .diagnostics
@@ -3214,7 +3218,7 @@ fn test_autofocus_attribute() {
3214fn test_repl_components_feed_subcommands_recognized() {
3218fn test_repl_components_feed_subcommands_recognized() {
3215 // Just verify that the parser and codegen work for content that these commands would process
3219 // Just verify that the parser and codegen work for content that these commands would process
3216 let result = htmlang::parser::parse(
3220 let result = htmlang::parser::parse(
3217− "@page Test Site\n@meta description A test\n@fn card $title\n @text $title",
3221+ "@page Test Site\n@meta description A test\n@let card $title\n @text $title",
3219 assert!(
3223 assert!(
@@ -3357,12 +3361,12 @@ fn output_contains_layer_wrapping() {
3360−// --- Named slots in @fn assertions ---
3364+// --- Named slots in @let assertions ---
3363fn fn_named_slots() {
3367fn fn_named_slots() {
3364 let output = compile(
3368 let output = compile(
3365− "@fn layout\n @column\n @slot header\n Default Header\n @slot content\n@layout\n @slot header\n Custom Header\n @slot content\n Page body",
3369+ "@let layout\n @column\n @slot header\n Default Header\n @slot content\n@layout\n @slot header\n Custom Header\n @slot content\n Page body",
3367 assert!(
3371 assert!(
3368 output.contains("Custom Header"),
3372 output.contains("Custom Header"),
@@ -3384,7 +3388,7 @@ fn fn_named_slots() {
3385fn fn_named_slot_default() {
3389fn fn_named_slot_default() {
3386 let output = compile(
3390 let output = compile(
3387− "@fn layout\n @column\n @slot header\n Default Header\n @slot content\n@layout\n @slot content\n Only content",
3391+ "@let layout\n @column\n @slot header\n Default Header\n @slot content\n@layout\n @slot content\n Only content",
3389 assert!(
3393 assert!(
3390 output.contains("Default Header"),
3394 output.contains("Default Header"),
@@ -3628,7 +3632,7 @@ fn no_warning_script_attrs() {
3630// ---------------------------------------------------------------------------
3634// ---------------------------------------------------------------------------
3631−// @mixin and spread attributes
3635+// @let attribute bundles (spread attributes)
3632// ---------------------------------------------------------------------------
3636// ---------------------------------------------------------------------------
@@ -3638,7 +3642,7 @@ fn snapshot_mixin_spread() {
3640fn mixin_expands_in_attrs() {
3644fn mixin_expands_in_attrs() {
3641− let output = compile("@mixin card [padding 20, rounded 8]\n@el [...$card]\n Hi");
3645+ let output = compile("@let card [padding 20, rounded 8]\n@el [...$card]\n Hi");
3642 assert!(
3646 assert!(
3643 output.contains("padding:20px"),
3647 output.contains("padding:20px"),
3644 "mixin should expand padding: {}",
3648 "mixin should expand padding: {}",
@@ -3653,7 +3657,7 @@ fn mixin_expands_in_attrs() {
3655fn mixin_with_dollar_syntax() {
3659fn mixin_with_dollar_syntax() {
3656− let output = compile("@mixin card [padding 20, rounded 8]\n@el [$card]\n Hi");
3660+ let output = compile("@let card [padding 20, rounded 8]\n@el [$card]\n Hi");
3657 assert!(
3661 assert!(
3658 output.contains("padding:20px"),
3662 output.contains("padding:20px"),
3659 "mixin with $ syntax should expand: {}",
3663 "mixin with $ syntax should expand: {}",
@@ -3663,7 +3667,7 @@ fn mixin_with_dollar_syntax() {
3665fn mixin_compose_with_extra_attrs() {
3669fn mixin_compose_with_extra_attrs() {
3666− let output = compile("@mixin base [padding 10]\n@el [...$base, background red]\n Hi");
3670+ let output = compile("@let base [padding 10]\n@el [...$base, background red]\n Hi");
3667 assert!(
3671 assert!(
3668 output.contains("padding:10px"),
3672 output.contains("padding:10px"),
3669 "mixin should expand: {}",
3673 "mixin should expand: {}",
@@ -3678,19 +3682,23 @@ fn mixin_compose_with_extra_attrs() {
3680fn warning_unused_mixin() {
3684fn warning_unused_mixin() {
3681− let diags = parse_diagnostics("@mixin card [padding 10]\n@el [background red]");
3685+ let diags = parse_diagnostics("@let card [padding 10]\n@el [background red]");
3682 assert!(
3686 assert!(
3683− diags.iter().any(|d| d.message.contains("unused mixin")),
3689+ .any(|d| d.message.contains("unused attribute bundle")),
3684− "expected unused mixin warning, got: {:?}",
3690+ "expected unused attribute bundle warning, got: {:?}",
3690fn no_warning_used_mixin() {
3696fn no_warning_used_mixin() {
3691− let diags = parse_diagnostics("@mixin card [padding 10]\n@el [...$card]");
3697+ let diags = parse_diagnostics("@let card [padding 10]\n@el [...$card]");
3692 assert!(
3698 assert!(
3693− !diags.iter().any(|d| d.message.contains("unused mixin")),
3701+ .any(|d| d.message.contains("unused attribute bundle")),
3694 "should not warn about used mixin, got: {:?}",
3702 "should not warn about used mixin, got: {:?}",
@@ -4182,7 +4190,7 @@ fn test_error_for_missing_range() {
4182fn test_error_circular_include() {
4190fn test_error_circular_include() {
4183 // A file including itself would be circular, but we test via in-memory parse
4191 // A file including itself would be circular, but we test via in-memory parse
4184 // by testing that the parser detects self-referential definitions
4192 // by testing that the parser detects self-referential definitions
4185− let diags = parse_diagnostics("@fn recursive $x\n @recursive [$x]\n\n@recursive [hello]");
4193+ let diags = parse_diagnostics("@let recursive $x\n @recursive [$x]\n\n@recursive [hello]");
4186 assert!(
4194 assert!(
4187 diags.iter().any(|d| d.message.contains("recursive")),
4195 diags.iter().any(|d| d.message.contains("recursive")),
4188 "should report recursive function call"
4196 "should report recursive function call"
@@ -4222,7 +4230,7 @@ fn test_warning_unused_variable() {
4224fn test_warning_unused_function() {
4232fn test_warning_unused_function() {
4225− let diags = parse_diagnostics("@fn unused_fn\n @text Hello\n\n@text World");
4233+ let diags = parse_diagnostics("@let unused_fn\n @text Hello\n\n@text World");
4226 assert!(
4234 assert!(
4227 diags.iter().any(|d| d.message.contains("unused function")
4235 diags.iter().any(|d| d.message.contains("unused function")
4228 && d.severity == htmlang::parser::Severity::Warning),
4236 && d.severity == htmlang::parser::Severity::Warning),
@@ -4245,7 +4253,7 @@ fn test_each_index_variable() {
4246fn test_children_fallback_content() {
4254fn test_children_fallback_content() {
4247 let html = compile(
4255 let html = compile(
4248− "@fn wrapper\n @el [padding 10]\n @children\n @text Default content\n\n@wrapper",
4256+ "@let wrapper\n @el [padding 10]\n @children\n @text Default content\n\n@wrapper",
4250 assert!(
4258 assert!(
4251 html.contains("Default content"),
4259 html.contains("Default content"),
@@ -4255,7 +4263,7 @@ fn test_children_fallback_content() {
4257fn test_spread_define() {
4265fn test_spread_define() {
4258− let html = compile("@define btn [padding 12, bold]\n@el [...$btn]\n Click");
4266+ let html = compile("@let btn [padding 12, bold]\n@el [...$btn]\n Click");
4259 assert!(
4267 assert!(
4260 html.contains("padding:12px"),
4268 html.contains("padding:12px"),
4261 "spread define should apply padding"
4269 "spread define should apply padding"
@@ -5053,7 +5061,7 @@ fn import_with_alias_prefixes_definitions() {
5053 let _ = std::fs::remove_dir_all(&dir);
5061 let _ = std::fs::remove_dir_all(&dir);
5054 std::fs::create_dir_all(&dir).unwrap();
5062 std::fs::create_dir_all(&dir).unwrap();
5055 let lib_path = dir.join("lib.hl");
5063 let lib_path = dir.join("lib.hl");
5056− std::fs::write(&lib_path, "@fn card\n @el\n @text card-body\n").unwrap();
5064+ std::fs::write(&lib_path, "@let card\n @el\n @text card-body\n").unwrap();
5058 let input = "@import lib.hl as ui\n@ui.card\n";
5066 let input = "@import lib.hl as ui\n@ui.card\n";
5059 let result = htmlang::parser::parse_with_base(input, Some(&dir));
5067 let result = htmlang::parser::parse_with_base(input, Some(&dir));