@@ -406,7 +406,7 @@ fn each_loop_expansion() {
406406
407407#[test]
408408fn aria_data_attrs_accepted() {
409− let diags = parse_diagnostics("@el [aria-label Test, data-id 42]");
409+ let diags = parse_diagnostics("@el [aria-label=Test, data-id=42]");
410410 assert!(
411411 !diags
412412 .iter()
@@ -518,7 +518,7 @@ fn no_warning_spacing_on_row() {
518518
519519#[test]
520520fn warning_placeholder_on_row() {
521− let diags = parse_diagnostics("@row [placeholder test]");
521+ let diags = parse_diagnostics("@row [placeholder=test]");
522522 assert!(
523523 diags
524524 .iter()
@@ -530,7 +530,7 @@ fn warning_placeholder_on_row() {
530530
531531#[test]
532532fn warning_for_on_non_label() {
533− let diags = parse_diagnostics("@el [for email]");
533+ let diags = parse_diagnostics("@el [for=email]");
534534 assert!(
535535 diags
536536 .iter()
@@ -583,7 +583,7 @@ fn named_slot_default_content() {
583583#[test]
584584fn style_block_output() {
585585 let output = compile(
586− "@page Test\n@style\n .custom { color: red; }\n@el [class custom]\n @text styled",
586+ "@page Test\n@style\n .custom { color: red; }\n@el [class=custom]\n @text styled",
587587 );
588588 assert!(output.contains(".custom{color:red;}") || output.contains(".custom { color: red; }"));
589589 assert!(output.contains("styled"));
@@ -965,7 +965,7 @@ fn image_auto_lazy_loading() {
965965
966966#[test]
967967fn image_explicit_loading_not_doubled() {
968− let output = compile("@page T\n@image [loading eager] https://example.com/photo.jpg");
968+ let output = compile("@page T\n@image [loading=eager] https://example.com/photo.jpg");
969969 assert!(output.contains("loading=\"eager\""));
970970 assert!(!output.contains("loading=\"lazy\""));
971971}
@@ -1102,22 +1102,18 @@ fn theme_color_meta_from_theme() {
11021102
11031103#[test]
11041104fn aria_live_passthrough() {
1105− let output = compile("@page T\n@el [aria-live polite]\n updating");
1105+ let output = compile("@page T\n@el [aria-live=polite]\n updating");
11061106 assert!(output.contains("aria-live=\"polite\""));
11071107}
11081108
11091109#[test]
1110−fn defer_directive() {
1111− let result = htmlang::parser::parse("@page T\n@defer\n @el\n Lazy content");
1110+fn defer_directive_was_removed() {
1111+ let diags = parse_diagnostics("@page T\n@defer\n @el\n Lazy content");
11121112 assert!(
1113− result
1114− .diagnostics
1115− .iter()
1116− .all(|d| d.severity != htmlang::parser::Severity::Error)
1113+ diags.iter().any(|d| d.message.contains("`@defer` was removed")),
1114+ "{:?}",
1115+ diags
11171116 );
1118− let html = htmlang::codegen::generate(&result.document);
1119− assert!(html.contains("data-hl-defer"));
1120− assert!(html.contains("IntersectionObserver"));
11211117}
11221118
11231119#[test]
@@ -1245,7 +1241,7 @@ fn snapshot_each_destructuring() {
12451241
12461242#[test]
12471243fn form_renders_form_tag() {
1248− let output = compile("@page T\n@form [method post] /submit\n @input [type text]");
1244+ let output = compile("@page T\n@form [method=post] /submit\n @input [type=text]");
12491245 assert!(output.contains("<form"));
12501246 assert!(output.contains("action=\"/submit\""));
12511247 assert!(output.contains("method=\"post\""));
@@ -1290,7 +1286,7 @@ fn hr_renders_self_closing() {
12901286
12911287#[test]
12921288fn figure_figcaption_renders() {
1293− let output = compile("@page T\n@figure\n @image [alt test] photo.jpg\n @figcaption Caption");
1289+ let output = compile("@page T\n@figure\n @image [alt=test] photo.jpg\n @figcaption Caption");
12941290 assert!(output.contains("<figure"));
12951291 assert!(output.contains("<figcaption"));
12961292 assert!(output.contains("Caption"));
@@ -1298,7 +1294,7 @@ fn figure_figcaption_renders() {
12981294
12991295#[test]
13001296fn progress_renders() {
1301− let output = compile("@page T\n@progress [value 70, max 100]");
1297+ let output = compile("@page T\n@progress [value=70, max=100]");
13021298 assert!(output.contains("<progress"));
13031299 assert!(output.contains("value=\"70\""));
13041300 assert!(output.contains("max=\"100\""));
@@ -1306,7 +1302,7 @@ fn progress_renders() {
13061302
13071303#[test]
13081304fn meter_renders() {
1309− let output = compile("@page T\n@meter [value 6, min 0, max 10]");
1305+ let output = compile("@page T\n@meter [value=6, min=0, max=10]");
13101306 assert!(output.contains("<meter"));
13111307 assert!(output.contains("value=\"6\""));
13121308}
@@ -1464,7 +1460,7 @@ fn warning_missing_alt_on_image() {
14641460
14651461#[test]
14661462fn no_warning_with_alt_on_image() {
1467− let diags = parse_diagnostics("@image [alt A photo] photo.jpg");
1463+ let diags = parse_diagnostics("@image [alt=A photo] photo.jpg");
14681464 assert!(
14691465 !diags
14701466 .iter()
@@ -1621,7 +1617,7 @@ fn checked_generates_pseudo() {
16211617
16221618#[test]
16231619fn placeholder_generates_pseudo() {
1624− let output = compile("@page T\n@input [type text, placeholder:color #999]");
1620+ let output = compile("@page T\n@input [type=text, placeholder:color #999]");
16251621 assert!(output.contains("::placeholder"));
16261622 assert!(output.contains("color:#999"));
16271623}
@@ -1692,13 +1688,13 @@ fn css_inset() {
16921688
16931689#[test]
16941690fn css_accent_color() {
1695− let output = compile("@page T\n@input [type checkbox, accent-color blue]");
1691+ let output = compile("@page T\n@input [type=checkbox, accent-color blue]");
16961692 assert!(output.contains("accent-color:blue"));
16971693}
16981694
16991695#[test]
17001696fn css_caret_color() {
1701− let output = compile("@page T\n@input [type text, caret-color red]");
1697+ let output = compile("@page T\n@input [type=text, caret-color red]");
17021698 assert!(output.contains("caret-color:red"));
17031699}
17041700
@@ -1876,7 +1872,7 @@ fn snapshot_arithmetic() {
18761872
18771873#[test]
18781874fn element_dialog() {
1879− let output = compile("@page T\n@dialog [id modal, open]\n @text Hello");
1875+ let output = compile("@page T\n@dialog [id=modal, open]\n @text Hello");
18801876 assert!(output.contains("<dialog"));
18811877 assert!(output.contains("id=\"modal\""));
18821878 assert!(output.contains("open"));
@@ -1894,7 +1890,7 @@ fn element_definition_list() {
18941890
18951891#[test]
18961892fn element_fieldset_legend() {
1897− let output = compile("@page T\n@fieldset\n @legend Info\n @input [type text, name n]");
1893+ let output = compile("@page T\n@fieldset\n @legend Info\n @input [type=text, name=n]");
18981894 assert!(output.contains("<fieldset"));
18991895 assert!(output.contains("<legend>Info</legend>"));
19001896 assert!(output.contains("</fieldset>"));
@@ -1903,7 +1899,7 @@ fn element_fieldset_legend() {
19031899#[test]
19041900fn element_picture_source() {
19051901 let output = compile(
1906− "@page T\n@picture\n @source [srcset wide.jpg, media (min-width: 800px)]\n @image [alt Photo] photo.jpg",
1902+ "@page T\n@picture\n @source [srcset=wide.jpg, media=(min-width: 800px)]\n @image [alt=Photo] photo.jpg",
19071903 );
19081904 assert!(output.contains("<picture"));
19091905 assert!(output.contains("<source"));
@@ -1913,7 +1909,7 @@ fn element_picture_source() {
19131909
19141910#[test]
19151911fn element_time() {
1916− let output = compile("@page T\n@time [datetime 2026-04-15] April 15");
1912+ let output = compile("@page T\n@time [datetime=2026-04-15] April 15");
19171913 assert!(output.contains("<time"));
19181914 assert!(output.contains("datetime=\"2026-04-15\""));
19191915 assert!(output.contains("April 15"));
@@ -1935,7 +1931,7 @@ fn element_kbd() {
19351931
19361932#[test]
19371933fn element_abbr() {
1938− let output = compile("@page T\n@abbr [title HyperText Markup Language] HTML");
1934+ let output = compile("@page T\n@abbr [title=HyperText Markup Language] HTML");
19391935 assert!(output.contains("<abbr"));
19401936 assert!(output.contains("title=\"HyperText Markup Language\""));
19411937 assert!(output.contains("HTML"));
@@ -1943,7 +1939,7 @@ fn element_abbr() {
19431939
19441940#[test]
19451941fn element_datalist() {
1946− let output = compile("@page T\n@datalist [id browsers]\n @option Chrome\n @option Firefox");
1942+ let output = compile("@page T\n@datalist [id=browsers]\n @option Chrome\n @option Firefox");
19471943 assert!(output.contains("<datalist"));
19481944 assert!(output.contains("id=\"browsers\""));
19491945 assert!(output.contains("</datalist>"));
@@ -2135,7 +2131,7 @@ fn let_arithmetic_divide() {
21352131
21362132#[test]
21372133fn warning_missing_input_type() {
2138− let diags = parse_diagnostics("@input [name email]");
2134+ let diags = parse_diagnostics("@input [name=email]");
21392135 assert!(
21402136 diags.iter().any(|d| d.message.contains("missing 'type'")),
21412137 "should warn about missing type on @input, got: {:?}",
@@ -2300,7 +2296,7 @@ fn convert_image() {
23002296 let hl = htmlang::convert::convert("<img src=\"photo.jpg\" alt=\"A photo\">");
23012297 assert!(hl.contains("@image"), "img should become @image: {}", hl);
23022298 assert!(hl.contains("photo.jpg"), "src preserved: {}", hl);
2303− assert!(hl.contains("alt A photo"), "alt preserved: {}", hl);
2299+ assert!(hl.contains("alt=A photo"), "alt preserved: {}", hl);
23042300}
23052301
23062302#[test]
@@ -2531,7 +2527,7 @@ fn element_iframe() {
25312527
25322528#[test]
25332529fn element_canvas() {
2534− let output = compile("@canvas [width 400, height 300, id myCanvas]");
2530+ let output = compile("@canvas [width 400, height 300, id=myCanvas]");
25352531 assert!(
25362532 output.contains("<canvas"),
25372533 "should generate canvas tag: {}",
@@ -2546,7 +2542,7 @@ fn element_canvas() {
25462542
25472543#[test]
25482544fn element_output() {
2549− let output = compile("@output [for a b]\n 42");
2545+ let output = compile("@output [for=a b]\n 42");
25502546 assert!(
25512547 output.contains("<output"),
25522548 "should generate output tag: {}",
@@ -3329,7 +3325,7 @@ fn test_color_filter_mix() {
33293325
33303326#[test]
33313327fn test_autofocus_attribute() {
3332− let result = htmlang::parser::parse("@input [type text, autofocus]");
3328+ let result = htmlang::parser::parse("@input [type=text, autofocus]");
33333329 let html = htmlang::codegen::generate(&result.document);
33343330 assert!(
33353331 html.contains("autofocus"),
@@ -3588,7 +3584,7 @@ fn snapshot_new_css_properties_4() {
35883584
35893585#[test]
35903586fn script_element_with_src() {
3591− let output = compile("@script [src app.js, defer]");
3587+ let output = compile("@script [src=app.js, defer]");
35923588 assert!(
35933589 output.contains("<script src=\"app.js\" defer>"),
35943590 "script src: {}",
@@ -3622,7 +3618,7 @@ fn address_element() {
36223618
36233619#[test]
36243620fn search_element() {
3625− let output = compile("@search\n @input [type search]");
3621+ let output = compile("@search\n @input [type=search]");
36263622 assert!(output.contains("<search>"), "search: {}", output);
36273623}
36283624
@@ -3699,7 +3695,7 @@ fn target_pseudo() {
36993695
37003696#[test]
37013697fn valid_invalid_pseudo() {
3702− let output = compile("@input [type email, valid:border 2 green]");
3698+ let output = compile("@input [type=email, valid:border 2 green]");
37033699 assert!(output.contains(":valid"), "valid pseudo: {}", output);
37043700}
37053701
@@ -3750,7 +3746,7 @@ fn no_warning_new_attrs_batch7() {
37503746#[test]
37513747fn no_warning_script_attrs() {
37523748 let diags = parse_diagnostics(
3753− "@script [src app.js, defer, async, crossorigin anonymous, integrity sha384-abc, nomodule]",
3749+ "@script [src=app.js, defer, async, crossorigin anonymous, integrity sha384-abc, nomodule]",
37543750 );
37553751 assert!(
37563752 !diags
@@ -4031,7 +4027,7 @@ fn conditional_attr_boolean_false() {
40314027#[test]
40324028fn function_with_style_is_scoped() {
40334029 let html = compile(
4034− "@let card $title\n @style\n .t { color: red; }\n @text [class t] $title\n@card [title Hello]\n",
4030+ "@let card $title\n @style\n .t { color: red; }\n @text [class=t] $title\n@card [title Hello]\n",
40354031 );
40364032 assert!(html.contains("hl-card\"><span class=\"t\">Hello"), "{}", html);
40374033 assert!(html.contains(".hl-card .t { color: red; }"), "{}", html);
@@ -4125,7 +4121,7 @@ fn critical_attr_inlines_styles() {
41254121
41264122#[test]
41274123fn warning_input_without_label() {
4128− let diags = parse_diagnostics("@input [type text]\n");
4124+ let diags = parse_diagnostics("@input [type=text]\n");
41294125 let has_label_warning = diags
41304126 .iter()
41314127 .any(|d| d.message.contains("aria-label") || d.message.contains("@label"));
@@ -4156,14 +4152,14 @@ fn warning_button_without_text() {
41564152
41574153#[test]
41584154fn warning_positive_tabindex() {
4159− let diags = parse_diagnostics("@el [tabindex 5]\n test\n");
4155+ let diags = parse_diagnostics("@el [tabindex=5]\n test\n");
41604156 let has_warning = diags.iter().any(|d| d.message.contains("tabindex"));
41614157 assert!(has_warning, "should warn about positive tabindex");
41624158}
41634159
41644160#[test]
41654161fn no_warning_input_with_aria_label() {
4166− let diags = parse_diagnostics("@input [type text, aria-label Search]\n");
4162+ let diags = parse_diagnostics("@input [type=text, aria-label=Search]\n");
41674163 let has_label_warning = diags
41684164 .iter()
41694165 .any(|d| d.message.contains("should have an") && d.message.contains("@label"));
@@ -4175,7 +4171,7 @@ fn no_warning_input_with_aria_label() {
41754171
41764172#[test]
41774173fn no_warning_input_in_label() {
4178− let diags = parse_diagnostics("@label\n @input [type text]\n");
4174+ let diags = parse_diagnostics("@label\n @input [type=text]\n");
41794175 let has_label_warning = diags
41804176 .iter()
41814177 .any(|d| d.message.contains("should have an") && d.message.contains("@label"));
@@ -4210,7 +4206,7 @@ fn snapshot_data_directive() {
42104206#[test]
42114207fn test_popover_in_output() {
42124208 let html = compile(
4213− "@button [popovertarget my-pop] Open\n@el [popover, id my-pop, padding 10]\n Hello",
4209+ "@button [popovertarget=my-pop] Open\n@el [popover, id=my-pop, padding 10]\n Hello",
42144210 );
42154211 assert!(
42164212 html.contains("popovertarget=\"my-pop\""),
@@ -4242,7 +4238,7 @@ fn test_appearance_css() {
42424238
42434239#[test]
42444240fn test_inputmode_attr() {
4245− let html = compile("@input [type search, inputmode search]");
4241+ let html = compile("@input [type=search, inputmode=search]");
42464242 assert!(
42474243 html.contains("inputmode=\"search\""),
42484244 "should pass through inputmode"
@@ -4251,7 +4247,7 @@ fn test_inputmode_attr() {
42514247
42524248#[test]
42534249fn test_fetchpriority_attr() {
4254− let html = compile("@image [fetchpriority high, width 100] hero.jpg");
4250+ let html = compile("@image [fetchpriority=high, width 100] hero.jpg");
42554251 assert!(
42564252 html.contains("fetchpriority=\"high\""),
42574253 "should pass through fetchpriority"
@@ -5010,7 +5006,7 @@ fn partial_output_dev() {
50105006
50115007#[test]
50125008fn responsive_srcset_on_image() {
5013− let output = compile("@image [responsive 400 800 1200, alt Photo] photo.jpg");
5009+ let output = compile("@image [responsive 400 800 1200, alt=Photo] photo.jpg");
50145010 assert!(
50155011 output.contains("srcset=\""),
50165012 "should generate srcset, got: {}",
@@ -5063,7 +5059,7 @@ fn auto_image_dimensions_png() {
50635059 ];
50645060 std::fs::write(&png_path, &png_data).unwrap();
50655061
5066− let input = format!("@image [alt test] {}", png_path.display());
5062+ let input = format!("@image [alt=test] {}", png_path.display());
50675063 let result = htmlang::parser::parse(&input);
50685064 let html = htmlang::codegen::generate(&result.document);
50695065 assert!(
@@ -5083,7 +5079,7 @@ fn auto_image_dimensions_png() {
50835079#[test]
50845080fn auto_image_dimensions_not_for_urls() {
50855081 // Remote URLs should not trigger dimension detection
5086− let output = compile("@image [alt test] https://example.com/photo.png");
5082+ let output = compile("@image [alt=test] https://example.com/photo.png");
50875083 // Should not crash or add dimensions for remote URLs
50885084 assert!(
50895085 output.contains("src=\"https://example.com/photo.png\""),
@@ -5108,7 +5104,7 @@ fn auto_image_dimensions_respects_explicit() {
51085104 std::fs::write(&png_path, &png_data).unwrap();
51095105
51105106 let input = format!(
5111− "@image [width 100, height 100, alt test] {}",
5107+ "@image [width 100, height 100, alt=test] {}",
51125108 png_path.display()
51135109 );
51145110 let result = htmlang::parser::parse(&input);