@@ -406,7 +406,7 @@ fn each_loop_expansion() {
408fn aria_data_attrs_accepted() {
408fn 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]");
@@ -518,7 +518,7 @@ fn no_warning_spacing_on_row() {
520fn warning_placeholder_on_row() {
520fn warning_placeholder_on_row() {
521− let diags = parse_diagnostics("@row [placeholder test]");
521+ let diags = parse_diagnostics("@row [placeholder=test]");
@@ -530,7 +530,7 @@ fn warning_placeholder_on_row() {
532fn warning_for_on_non_label() {
532fn warning_for_on_non_label() {
533− let diags = parse_diagnostics("@el [for email]");
533+ let diags = parse_diagnostics("@el [for=email]");
@@ -583,7 +583,7 @@ fn named_slot_default_content() {
584fn style_block_output() {
584fn style_block_output() {
585 let output = compile(
585 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",
588 assert!(output.contains(".custom{color:red;}") || output.contains(".custom { color: red; }"));
588 assert!(output.contains(".custom{color:red;}") || output.contains(".custom { color: red; }"));
589 assert!(output.contains("styled"));
589 assert!(output.contains("styled"));
@@ -965,7 +965,7 @@ fn image_auto_lazy_loading() {
967fn image_explicit_loading_not_doubled() {
967fn 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");
969 assert!(output.contains("loading=\"eager\""));
969 assert!(output.contains("loading=\"eager\""));
970 assert!(!output.contains("loading=\"lazy\""));
970 assert!(!output.contains("loading=\"lazy\""));
@@ -1102,22 +1102,18 @@ fn theme_color_meta_from_theme() {
1104fn aria_live_passthrough() {
1104fn 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");
1106 assert!(output.contains("aria-live=\"polite\""));
1106 assert!(output.contains("aria-live=\"polite\""));
1110−fn defer_directive() {
1110+fn defer_directive_was_removed() {
1111− let result = htmlang::parser::parse("@page T\n@defer\n @el\n Lazy content");
1111+ let diags = parse_diagnostics("@page T\n@defer\n @el\n Lazy content");
1112 assert!(
1112 assert!(
1113− result
1113+ diags.iter().any(|d| d.message.contains("`@defer` was removed")),
1114− .diagnostics
1114+ "{:?}",
1116− .all(|d| d.severity != htmlang::parser::Severity::Error)
1118− let html = htmlang::codegen::generate(&result.document);
1119− assert!(html.contains("data-hl-defer"));
1120− assert!(html.contains("IntersectionObserver"));
@@ -1245,7 +1241,7 @@ fn snapshot_each_destructuring() {
1247fn form_renders_form_tag() {
1243fn 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]");
1249 assert!(output.contains("<form"));
1245 assert!(output.contains("<form"));
1250 assert!(output.contains("action=\"/submit\""));
1246 assert!(output.contains("action=\"/submit\""));
1251 assert!(output.contains("method=\"post\""));
1247 assert!(output.contains("method=\"post\""));
@@ -1290,7 +1286,7 @@ fn hr_renders_self_closing() {
1292fn figure_figcaption_renders() {
1288fn 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");
1294 assert!(output.contains("<figure"));
1290 assert!(output.contains("<figure"));
1295 assert!(output.contains("<figcaption"));
1291 assert!(output.contains("<figcaption"));
1296 assert!(output.contains("Caption"));
1292 assert!(output.contains("Caption"));
@@ -1298,7 +1294,7 @@ fn figure_figcaption_renders() {
1300fn progress_renders() {
1296fn 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]");
1302 assert!(output.contains("<progress"));
1298 assert!(output.contains("<progress"));
1303 assert!(output.contains("value=\"70\""));
1299 assert!(output.contains("value=\"70\""));
1304 assert!(output.contains("max=\"100\""));
1300 assert!(output.contains("max=\"100\""));
@@ -1306,7 +1302,7 @@ fn progress_renders() {
1308fn meter_renders() {
1304fn 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]");
1310 assert!(output.contains("<meter"));
1306 assert!(output.contains("<meter"));
1311 assert!(output.contains("value=\"6\""));
1307 assert!(output.contains("value=\"6\""));
@@ -1464,7 +1460,7 @@ fn warning_missing_alt_on_image() {
1466fn no_warning_with_alt_on_image() {
1462fn 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");
1468 assert!(
1464 assert!(
@@ -1621,7 +1617,7 @@ fn checked_generates_pseudo() {
1623fn placeholder_generates_pseudo() {
1619fn 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]");
1625 assert!(output.contains("::placeholder"));
1621 assert!(output.contains("::placeholder"));
1626 assert!(output.contains("color:#999"));
1622 assert!(output.contains("color:#999"));
@@ -1692,13 +1688,13 @@ fn css_inset() {
1694fn css_accent_color() {
1690fn 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]");
1696 assert!(output.contains("accent-color:blue"));
1692 assert!(output.contains("accent-color:blue"));
1700fn css_caret_color() {
1696fn 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]");
1702 assert!(output.contains("caret-color:red"));
1698 assert!(output.contains("caret-color:red"));
@@ -1876,7 +1872,7 @@ fn snapshot_arithmetic() {
1878fn element_dialog() {
1874fn 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");
1880 assert!(output.contains("<dialog"));
1876 assert!(output.contains("<dialog"));
1881 assert!(output.contains("id=\"modal\""));
1877 assert!(output.contains("id=\"modal\""));
1882 assert!(output.contains("open"));
1878 assert!(output.contains("open"));
@@ -1894,7 +1890,7 @@ fn element_definition_list() {
1896fn element_fieldset_legend() {
1892fn 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]");
1898 assert!(output.contains("<fieldset"));
1894 assert!(output.contains("<fieldset"));
1899 assert!(output.contains("<legend>Info</legend>"));
1895 assert!(output.contains("<legend>Info</legend>"));
1900 assert!(output.contains("</fieldset>"));
1896 assert!(output.contains("</fieldset>"));
@@ -1903,7 +1899,7 @@ fn element_fieldset_legend() {
1904fn element_picture_source() {
1900fn element_picture_source() {
1905 let output = compile(
1901 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",
1908 assert!(output.contains("<picture"));
1904 assert!(output.contains("<picture"));
1909 assert!(output.contains("<source"));
1905 assert!(output.contains("<source"));
@@ -1913,7 +1909,7 @@ fn element_picture_source() {
1915fn element_time() {
1911fn 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");
1917 assert!(output.contains("<time"));
1913 assert!(output.contains("<time"));
1918 assert!(output.contains("datetime=\"2026-04-15\""));
1914 assert!(output.contains("datetime=\"2026-04-15\""));
1919 assert!(output.contains("April 15"));
1915 assert!(output.contains("April 15"));
@@ -1935,7 +1931,7 @@ fn element_kbd() {
1937fn element_abbr() {
1933fn 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");
1939 assert!(output.contains("<abbr"));
1935 assert!(output.contains("<abbr"));
1940 assert!(output.contains("title=\"HyperText Markup Language\""));
1936 assert!(output.contains("title=\"HyperText Markup Language\""));
1941 assert!(output.contains("HTML"));
1937 assert!(output.contains("HTML"));
@@ -1943,7 +1939,7 @@ fn element_abbr() {
1945fn element_datalist() {
1941fn 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");
1947 assert!(output.contains("<datalist"));
1943 assert!(output.contains("<datalist"));
1948 assert!(output.contains("id=\"browsers\""));
1944 assert!(output.contains("id=\"browsers\""));
1949 assert!(output.contains("</datalist>"));
1945 assert!(output.contains("</datalist>"));
@@ -2135,7 +2131,7 @@ fn let_arithmetic_divide() {
2137fn warning_missing_input_type() {
2133fn warning_missing_input_type() {
2138− let diags = parse_diagnostics("@input [name email]");
2134+ let diags = parse_diagnostics("@input [name=email]");
2139 assert!(
2135 assert!(
2140 diags.iter().any(|d| d.message.contains("missing 'type'")),
2136 diags.iter().any(|d| d.message.contains("missing 'type'")),
2141 "should warn about missing type on @input, got: {:?}",
2137 "should warn about missing type on @input, got: {:?}",
@@ -2300,7 +2296,7 @@ fn convert_image() {
2300 let hl = htmlang::convert::convert("<img src=\"photo.jpg\" alt=\"A photo\">");
2296 let hl = htmlang::convert::convert("<img src=\"photo.jpg\" alt=\"A photo\">");
2301 assert!(hl.contains("@image"), "img should become @image: {}", hl);
2297 assert!(hl.contains("@image"), "img should become @image: {}", hl);
2302 assert!(hl.contains("photo.jpg"), "src preserved: {}", hl);
2298 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);
@@ -2531,7 +2527,7 @@ fn element_iframe() {
2533fn element_canvas() {
2529fn element_canvas() {
2534− let output = compile("@canvas [width 400, height 300, id myCanvas]");
2530+ let output = compile("@canvas [width 400, height 300, id=myCanvas]");
2535 assert!(
2531 assert!(
2536 output.contains("<canvas"),
2532 output.contains("<canvas"),
2537 "should generate canvas tag: {}",
2533 "should generate canvas tag: {}",
@@ -2546,7 +2542,7 @@ fn element_canvas() {
2548fn element_output() {
2544fn element_output() {
2549− let output = compile("@output [for a b]\n 42");
2545+ let output = compile("@output [for=a b]\n 42");
2550 assert!(
2546 assert!(
2551 output.contains("<output"),
2547 output.contains("<output"),
2552 "should generate output tag: {}",
2548 "should generate output tag: {}",
@@ -3329,7 +3325,7 @@ fn test_color_filter_mix() {
3331fn test_autofocus_attribute() {
3327fn test_autofocus_attribute() {
3332− let result = htmlang::parser::parse("@input [type text, autofocus]");
3328+ let result = htmlang::parser::parse("@input [type=text, autofocus]");
3333 let html = htmlang::codegen::generate(&result.document);
3329 let html = htmlang::codegen::generate(&result.document);
3334 assert!(
3330 assert!(
3335 html.contains("autofocus"),
3331 html.contains("autofocus"),
@@ -3588,7 +3584,7 @@ fn snapshot_new_css_properties_4() {
3590fn script_element_with_src() {
3586fn script_element_with_src() {
3591− let output = compile("@script [src app.js, defer]");
3587+ let output = compile("@script [src=app.js, defer]");
3592 assert!(
3588 assert!(
3593 output.contains("<script src=\"app.js\" defer>"),
3589 output.contains("<script src=\"app.js\" defer>"),
3594 "script src: {}",
3590 "script src: {}",
@@ -3622,7 +3618,7 @@ fn address_element() {
3624fn search_element() {
3620fn search_element() {
3625− let output = compile("@search\n @input [type search]");
3621+ let output = compile("@search\n @input [type=search]");
3626 assert!(output.contains("<search>"), "search: {}", output);
3622 assert!(output.contains("<search>"), "search: {}", output);
@@ -3699,7 +3695,7 @@ fn target_pseudo() {
3701fn valid_invalid_pseudo() {
3697fn 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]");
3703 assert!(output.contains(":valid"), "valid pseudo: {}", output);
3699 assert!(output.contains(":valid"), "valid pseudo: {}", output);
@@ -3750,7 +3746,7 @@ fn no_warning_new_attrs_batch7() {
3751fn no_warning_script_attrs() {
3747fn no_warning_script_attrs() {
3752 let diags = parse_diagnostics(
3748 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]",
3755 assert!(
3751 assert!(
@@ -4031,7 +4027,7 @@ fn conditional_attr_boolean_false() {
4032fn function_with_style_is_scoped() {
4028fn function_with_style_is_scoped() {
4033 let html = compile(
4029 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",
4036 assert!(html.contains("hl-card\"><span class=\"t\">Hello"), "{}", html);
4032 assert!(html.contains("hl-card\"><span class=\"t\">Hello"), "{}", html);
4037 assert!(html.contains(".hl-card .t { color: red; }"), "{}", html);
4033 assert!(html.contains(".hl-card .t { color: red; }"), "{}", html);
@@ -4125,7 +4121,7 @@ fn critical_attr_inlines_styles() {
4127fn warning_input_without_label() {
4123fn warning_input_without_label() {
4128− let diags = parse_diagnostics("@input [type text]\n");
4124+ let diags = parse_diagnostics("@input [type=text]\n");
4129 let has_label_warning = diags
4125 let has_label_warning = diags
4131 .any(|d| d.message.contains("aria-label") || d.message.contains("@label"));
4127 .any(|d| d.message.contains("aria-label") || d.message.contains("@label"));
@@ -4156,14 +4152,14 @@ fn warning_button_without_text() {
4158fn warning_positive_tabindex() {
4154fn 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");
4160 let has_warning = diags.iter().any(|d| d.message.contains("tabindex"));
4156 let has_warning = diags.iter().any(|d| d.message.contains("tabindex"));
4161 assert!(has_warning, "should warn about positive tabindex");
4157 assert!(has_warning, "should warn about positive tabindex");
4165fn no_warning_input_with_aria_label() {
4161fn 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");
4167 let has_label_warning = diags
4163 let has_label_warning = diags
4169 .any(|d| d.message.contains("should have an") && d.message.contains("@label"));
4165 .any(|d| d.message.contains("should have an") && d.message.contains("@label"));
@@ -4175,7 +4171,7 @@ fn no_warning_input_with_aria_label() {
4177fn no_warning_input_in_label() {
4173fn 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");
4179 let has_label_warning = diags
4175 let has_label_warning = diags
4181 .any(|d| d.message.contains("should have an") && d.message.contains("@label"));
4177 .any(|d| d.message.contains("should have an") && d.message.contains("@label"));
@@ -4210,7 +4206,7 @@ fn snapshot_data_directive() {
4211fn test_popover_in_output() {
4207fn test_popover_in_output() {
4212 let html = compile(
4208 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",
4215 assert!(
4211 assert!(
4216 html.contains("popovertarget=\"my-pop\""),
4212 html.contains("popovertarget=\"my-pop\""),
@@ -4242,7 +4238,7 @@ fn test_appearance_css() {
4244fn test_inputmode_attr() {
4240fn test_inputmode_attr() {
4245− let html = compile("@input [type search, inputmode search]");
4241+ let html = compile("@input [type=search, inputmode=search]");
4246 assert!(
4242 assert!(
4247 html.contains("inputmode=\"search\""),
4243 html.contains("inputmode=\"search\""),
4248 "should pass through inputmode"
4244 "should pass through inputmode"
@@ -4251,7 +4247,7 @@ fn test_inputmode_attr() {
4253fn test_fetchpriority_attr() {
4249fn 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");
4255 assert!(
4251 assert!(
4256 html.contains("fetchpriority=\"high\""),
4252 html.contains("fetchpriority=\"high\""),
4257 "should pass through fetchpriority"
4253 "should pass through fetchpriority"
@@ -5010,7 +5006,7 @@ fn partial_output_dev() {
5012fn responsive_srcset_on_image() {
5008fn 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");
5014 assert!(
5010 assert!(
5015 output.contains("srcset=\""),
5011 output.contains("srcset=\""),
5016 "should generate srcset, got: {}",
5012 "should generate srcset, got: {}",
@@ -5063,7 +5059,7 @@ fn auto_image_dimensions_png() {
5064 std::fs::write(&png_path, &png_data).unwrap();
5060 std::fs::write(&png_path, &png_data).unwrap();
5066− let input = format!("@image [alt test] {}", png_path.display());
5062+ let input = format!("@image [alt=test] {}", png_path.display());
5067 let result = htmlang::parser::parse(&input);
5063 let result = htmlang::parser::parse(&input);
5068 let html = htmlang::codegen::generate(&result.document);
5064 let html = htmlang::codegen::generate(&result.document);
5069 assert!(
5065 assert!(
@@ -5083,7 +5079,7 @@ fn auto_image_dimensions_png() {
5084fn auto_image_dimensions_not_for_urls() {
5080fn auto_image_dimensions_not_for_urls() {
5085 // Remote URLs should not trigger dimension detection
5081 // 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");
5087 // Should not crash or add dimensions for remote URLs
5083 // Should not crash or add dimensions for remote URLs
5088 assert!(
5084 assert!(
5089 output.contains("src=\"https://example.com/photo.png\""),
5085 output.contains("src=\"https://example.com/photo.png\""),
@@ -5108,7 +5104,7 @@ fn auto_image_dimensions_respects_explicit() {
5108 std::fs::write(&png_path, &png_data).unwrap();
5104 std::fs::write(&png_path, &png_data).unwrap();
5110 let input = format!(
5106 let input = format!(
5111− "@image [width 100, height 100, alt test] {}",
5107+ "@image [width 100, height 100, alt=test] {}",
5112 png_path.display()
5108 png_path.display()
5114 let result = htmlang::parser::parse(&input);
5110 let result = htmlang::parser::parse(&input);