@@ -22,6 +22,10 @@ struct StyleEntry {
2222 focus: String,
2323 /// Responsive overrides: (breakpoint_prefix, css)
2424 responsive: Vec<(String, String)>,
25+ /// Dark mode overrides
26+ dark: String,
27+ /// Print overrides
28+ print: String,
2529}
2630
2731struct StyleCollector {
@@ -45,12 +49,16 @@ impl StyleCollector {
4549 active: String,
4650 focus: String,
4751 responsive: Vec<(String, String)>,
52+ dark: String,
53+ print: String,
4854 ) -> Option<String> {
4955 if base.is_empty()
5056 && hover.is_empty()
5157 && active.is_empty()
5258 && focus.is_empty()
5359 && responsive.is_empty()
60+ && dark.is_empty()
61+ && print.is_empty()
5462 {
5563 return None;
5664 }
@@ -59,7 +67,7 @@ impl StyleCollector {
5967 .map(|(bp, css)| format!("{}={}", bp, css))
6068 .collect::<Vec<_>>()
6169 .join("|");
62− let key = format!("{}|{}|{}|{}|{}", base, hover, active, focus, resp_key);
70+ let key = format!("{}|{}|{}|{}|{}|{}|{}", base, hover, active, focus, resp_key, dark, print);
6371 if let Some(&idx) = self.index.get(&key) {
6472 return Some(self.entries[idx].class_name.clone());
6573 }
@@ -72,6 +80,8 @@ impl StyleCollector {
7280 active,
7381 focus,
7482 responsive,
83+ dark,
84+ print,
7585 });
7686 self.index.insert(key, idx);
7787 Some(name)
@@ -126,6 +136,44 @@ impl StyleCollector {
126136 }
127137 }
128138
139+ // Dark mode rules
140+ let mut dark_css = String::new();
141+ for e in &self.entries {
142+ if !e.dark.is_empty() {
143+ if dev {
144+ dark_css.push_str(&format!(" .{} {{{}}}\n", e.class_name, e.dark));
145+ } else {
146+ dark_css.push_str(&format!(".{}{{{}}}", e.class_name, e.dark));
147+ }
148+ }
149+ }
150+ if !dark_css.is_empty() {
151+ if dev {
152+ css.push_str(&format!("@media (prefers-color-scheme: dark) {{\n{}}}\n", dark_css));
153+ } else {
154+ css.push_str(&format!("@media(prefers-color-scheme:dark){{{}}}", dark_css));
155+ }
156+ }
157+
158+ // Print rules
159+ let mut print_css = String::new();
160+ for e in &self.entries {
161+ if !e.print.is_empty() {
162+ if dev {
163+ print_css.push_str(&format!(" .{} {{{}}}\n", e.class_name, e.print));
164+ } else {
165+ print_css.push_str(&format!(".{}{{{}}}", e.class_name, e.print));
166+ }
167+ }
168+ }
169+ if !print_css.is_empty() {
170+ if dev {
171+ css.push_str(&format!("@media print {{\n{}}}\n", print_css));
172+ } else {
173+ css.push_str(&format!("@media print{{{}}}", print_css));
174+ }
175+ }
176+
129177 css
130178 }
131179}
@@ -331,6 +379,8 @@ fn generate_node(
331379 | Some(ElementKind::Nav) | Some(ElementKind::Header) | Some(ElementKind::Footer)
332380 | Some(ElementKind::Main) | Some(ElementKind::Section) | Some(ElementKind::Article)
333381 | Some(ElementKind::Aside) | Some(ElementKind::ListItem)
382+ | Some(ElementKind::Form) | Some(ElementKind::Details) | Some(ElementKind::Figure)
383+ | Some(ElementKind::Blockquote)
334384 );
335385 if needs_wrap {
336386 out.push_str(&ctx.indent());
@@ -362,12 +412,21 @@ const HTML_PASSTHROUGH_ATTRS: &[&str] = &[
362412 "loading", "decoding",
363413 // Media src (explicit attribute form)
364414 "src",
415+ // Details
416+ "open",
417+ // Form
418+ "novalidate",
419+ // Progress/Meter
420+ "low", "high", "optimum",
421+ // Table
422+ "colspan", "rowspan", "scope",
365423];
366424
367425/// Boolean HTML attributes (rendered without a value, e.g., `<input disabled>`).
368426const BOOLEAN_HTML_ATTRS: &[&str] = &[
369427 "disabled", "required", "checked", "multiple",
370428 "controls", "autoplay", "loop", "muted",
429+ "open", "novalidate",
371430];
372431
373432fn emit_html_passthrough_attrs(out: &mut String, attrs: &[Attribute]) {
@@ -400,7 +459,7 @@ fn generate_element(
400459 ctx: &mut GenContext,
401460) {
402461 // Self-closing elements
403− if matches!(elem.kind, ElementKind::Image | ElementKind::Input) {
462+ if matches!(elem.kind, ElementKind::Image | ElementKind::Input | ElementKind::HorizontalRule) {
404463 generate_self_closing(elem, parent_kind, out, styles, ctx);
405464 return;
406465 }
@@ -444,7 +503,20 @@ fn generate_element(
444503 // Media
445504 ElementKind::Video => "video",
446505 ElementKind::Audio => "audio",
447− ElementKind::Image | ElementKind::Input | ElementKind::Children | ElementKind::Slot(_) => unreachable!(),
506+ // Additional semantic elements
507+ ElementKind::Form => "form",
508+ ElementKind::Details => "details",
509+ ElementKind::Summary => "summary",
510+ ElementKind::Blockquote => "blockquote",
511+ ElementKind::Cite => "cite",
512+ ElementKind::Code => "code",
513+ ElementKind::Pre => "pre",
514+ ElementKind::Figure => "figure",
515+ ElementKind::FigCaption => "figcaption",
516+ ElementKind::Progress => "progress",
517+ ElementKind::Meter => "meter",
518+ ElementKind::Image | ElementKind::Input | ElementKind::HorizontalRule
519+ | ElementKind::Children | ElementKind::Slot(_) => unreachable!(),
448520 };
449521
450522 let kind_label = match elem.kind {
@@ -476,6 +548,15 @@ fn generate_element(
476548 ElementKind::TableHeaderCell => "th",
477549 ElementKind::Video => "video",
478550 ElementKind::Audio => "audio",
551+ ElementKind::Form => "form",
552+ ElementKind::Details => "details",
553+ ElementKind::Summary => "summary",
554+ ElementKind::Blockquote => "blockquote",
555+ ElementKind::Cite => "cite",
556+ ElementKind::Code => "code",
557+ ElementKind::Pre => "pre",
558+ ElementKind::Figure => "figure",
559+ ElementKind::FigCaption => "figcaption",
479560 _ => "",
480561 };
481562
@@ -508,6 +589,17 @@ fn generate_element(
508589 }
509590 }
510591
592+ // Form action
593+ if elem.kind == ElementKind::Form {
594+ if let Some(action) = &elem.argument {
595+ out.push_str(" action=\"");
596+ out.push_str(&html_escape(action));
597+ out.push('"');
598+ }
599+ }
600+
601+ // Details open attribute (handled via HTML_PASSTHROUGH_ATTRS, no extra handling needed)
602+
511603 emit_class_attr(out, gen_class.as_deref(), user_class.as_deref());
512604
513605 if let Some(id) = id {
@@ -532,6 +624,10 @@ fn generate_element(
532624 | ElementKind::ListItem
533625 | ElementKind::TableCell
534626 | ElementKind::TableHeaderCell
627+ | ElementKind::Summary
628+ | ElementKind::Cite
629+ | ElementKind::Code
630+ | ElementKind::FigCaption
535631 ) {
536632 if let Some(text) = &elem.argument {
537633 out.push_str(&html_escape(text));
@@ -569,6 +665,7 @@ fn generate_self_closing(
569665 let (tag, kind_label) = match elem.kind {
570666 ElementKind::Image => ("img", "image"),
571667 ElementKind::Input => ("input", "input"),
668+ ElementKind::HorizontalRule => ("hr", "hr"),
572669 _ => unreachable!(),
573670 };
574671
@@ -600,6 +697,21 @@ fn generate_self_closing(
600697
601698 // Image optimization: auto-add loading="lazy" and decoding="async"
602699 if elem.kind == ElementKind::Image {
700+ // SVG inlining: @image [inline] logo.svg
701+ if elem.attrs.iter().any(|a| a.key == "inline") {
702+ if let Some(src) = &elem.argument {
703+ if src.ends_with(".svg") {
704+ if let Ok(svg_content) = std::fs::read_to_string(src) {
705+ // Close the tag we opened, then emit inline SVG instead
706+ out.truncate(out.rfind('<').unwrap_or(0));
707+ out.push_str(&ctx.indent());
708+ out.push_str(svg_content.trim());
709+ out.push_str(ctx.nl());
710+ return;
711+ }
712+ }
713+ }
714+ }
603715 if !elem.attrs.iter().any(|a| a.key == "loading") {
604716 out.push_str(" loading=\"lazy\"");
605717 }
@@ -655,7 +767,13 @@ fn compute_class(
655767 }
656768 }
657769
658− styles.get_class(base, hover, active, focus, responsive)
770+ // Dark mode
771+ let dark = attrs_to_css(attrs, "dark:", kind, parent_kind);
772+
773+ // Print
774+ let print = attrs_to_css(attrs, "print:", kind, parent_kind);
775+
776+ styles.get_class(base, hover, active, focus, responsive, dark, print)
659777}
660778
661779fn emit_class_attr(out: &mut String, gen_class: Option<&str>, user_class: Option<&str>) {
@@ -687,10 +805,12 @@ fn emit_class_attr(out: &mut String, gen_class: Option<&str>, user_class: Option
687805
688806const STATE_PREFIXES: &[&str] = &["hover:", "active:", "focus:"];
689807const RESPONSIVE_PREFIXES: &[&str] = &["sm:", "md:", "lg:", "xl:"];
808+const MEDIA_PREFIXES: &[&str] = &["dark:", "print:"];
690809
691810fn is_prefixed_attr(key: &str) -> bool {
692811 STATE_PREFIXES.iter().any(|p| key.starts_with(p))
693812 || RESPONSIVE_PREFIXES.iter().any(|p| key.starts_with(p))
813+ || MEDIA_PREFIXES.iter().any(|p| key.starts_with(p))
694814}
695815
696816fn attrs_to_css(
@@ -715,6 +835,18 @@ fn attrs_to_css(
715835 // Lists: reset browser defaults
716836 ElementKind::List => css.push_str("margin:0;padding-left:0;list-style:none;"),
717837 ElementKind::ListItem => css.push_str("display:flex;flex-direction:column;"),
838+ // Form: flex column like @el
839+ ElementKind::Form => css.push_str("display:flex;flex-direction:column;"),
840+ // Details: flex column
841+ ElementKind::Details => css.push_str("display:flex;flex-direction:column;"),
842+ // Figure: flex column
843+ ElementKind::Figure => css.push_str("display:flex;flex-direction:column;margin:0;"),
844+ // Blockquote: flex column, reset browser margin
845+ ElementKind::Blockquote => css.push_str("display:flex;flex-direction:column;margin:0;"),
846+ // Pre: preserve whitespace
847+ ElementKind::Pre => css.push_str("margin:0;white-space:pre;font-family:ui-monospace,monospace;"),
848+ // Code: monospace font
849+ ElementKind::Code => css.push_str("font-family:ui-monospace,monospace;"),
718850 _ => {}
719851 }
720852 }
@@ -1140,6 +1272,118 @@ fn attrs_to_css(
11401272 }
11411273 }
11421274
1275+ // Margin
1276+ "margin" => {
1277+ if let Some(v) = val {
1278+ push_css(&mut css, "margin", &css_px_multi(v));
1279+ }
1280+ }
1281+ "margin-x" => {
1282+ if let Some(v) = val {
1283+ push_css(&mut css, "margin-left", &css_px(v));
1284+ push_css(&mut css, "margin-right", &css_px(v));
1285+ }
1286+ }
1287+ "margin-y" => {
1288+ if let Some(v) = val {
1289+ push_css(&mut css, "margin-top", &css_px(v));
1290+ push_css(&mut css, "margin-bottom", &css_px(v));
1291+ }
1292+ }
1293+
1294+ // Filter
1295+ "filter" => {
1296+ if let Some(v) = val {
1297+ push_css(&mut css, "filter", v);
1298+ }
1299+ }
1300+
1301+ // Object fit/position (for images)
1302+ "object-fit" => {
1303+ if let Some(v) = val {
1304+ push_css(&mut css, "object-fit", v);
1305+ }
1306+ }
1307+ "object-position" => {
1308+ if let Some(v) = val {
1309+ push_css(&mut css, "object-position", v);
1310+ }
1311+ }
1312+
1313+ // Text shadow
1314+ "text-shadow" => {
1315+ if let Some(v) = val {
1316+ push_css(&mut css, "text-shadow", v);
1317+ }
1318+ }
1319+
1320+ // Text overflow
1321+ "text-overflow" => {
1322+ if let Some(v) = val {
1323+ push_css(&mut css, "text-overflow", v);
1324+ }
1325+ }
1326+
1327+ // Interaction
1328+ "pointer-events" => {
1329+ if let Some(v) = val {
1330+ push_css(&mut css, "pointer-events", v);
1331+ }
1332+ }
1333+ "user-select" => {
1334+ if let Some(v) = val {
1335+ push_css(&mut css, "user-select", v);
1336+ }
1337+ }
1338+
1339+ // Flexbox/grid alignment
1340+ "justify-content" => {
1341+ if let Some(v) = val {
1342+ push_css(&mut css, "justify-content", v);
1343+ }
1344+ }
1345+ "align-items" => {
1346+ if let Some(v) = val {
1347+ push_css(&mut css, "align-items", v);
1348+ }
1349+ }
1350+
1351+ // Flex item order
1352+ "order" => {
1353+ if let Some(v) = val {
1354+ push_css(&mut css, "order", v);
1355+ }
1356+ }
1357+
1358+ // Background extras
1359+ "background-size" => {
1360+ if let Some(v) = val {
1361+ push_css(&mut css, "background-size", v);
1362+ }
1363+ }
1364+ "background-position" => {
1365+ if let Some(v) = val {
1366+ push_css(&mut css, "background-position", v);
1367+ }
1368+ }
1369+ "background-repeat" => {
1370+ if let Some(v) = val {
1371+ push_css(&mut css, "background-repeat", v);
1372+ }
1373+ }
1374+
1375+ // Text wrapping
1376+ "word-break" => {
1377+ if let Some(v) = val {
1378+ push_css(&mut css, "word-break", v);
1379+ }
1380+ }
1381+ "overflow-wrap" => {
1382+ if let Some(v) = val {
1383+ push_css(&mut css, "overflow-wrap", v);
1384+ }
1385+ }
1386+
11431387 // Container queries
11441388 "container" => {
11451389 push_css(&mut css, "container-type", "inline-size");
@@ -1162,7 +1406,9 @@ fn attrs_to_css(
11621406 | "max" | "step" | "pattern" | "maxlength" | "rows" | "cols"
11631407 | "multiple" | "alt" | "role" | "tabindex" | "title"
11641408 | "controls" | "autoplay" | "loop" | "muted" | "poster" | "preload"
1165− | "loading" | "decoding" | "ordered" | "src" => {}
1409+ | "loading" | "decoding" | "ordered" | "src"
1410+ | "open" | "novalidate" | "low" | "high" | "optimum"
1411+ | "colspan" | "rowspan" | "scope" | "inline" => {}
11661412
11671413 _ => {}
11681414 }