@@ -30,6 +30,8 @@ struct StyleEntry {
30 motion_reduce: String,
30 motion_reduce: String,
31 landscape: String,
31 landscape: String,
32 portrait: String,
32 portrait: String,
33+ /// Container query overrides: (breakpoint_prefix, css)
34+ container: Vec<(String, String)>,
35struct StyleCollector {
37struct StyleCollector {
@@ -57,6 +59,7 @@ impl StyleCollector {
57 motion_reduce: String,
59 motion_reduce: String,
58 landscape: String,
60 landscape: String,
59 portrait: String,
61 portrait: String,
62+ container: Vec<(String, String)>,
60 ) -> Option<String> {
63 ) -> Option<String> {
61 if base.is_empty()
64 if base.is_empty()
62 && pseudo.is_empty()
65 && pseudo.is_empty()
@@ -67,6 +70,7 @@ impl StyleCollector {
67 && motion_reduce.is_empty()
70 && motion_reduce.is_empty()
68 && landscape.is_empty()
71 && landscape.is_empty()
69 && portrait.is_empty()
72 && portrait.is_empty()
73+ && container.is_empty()
71 return None;
75 return None;
@@ -80,7 +84,12 @@ impl StyleCollector {
80 .map(|(bp, css)| format!("{}={}", bp, css))
84 .map(|(bp, css)| format!("{}={}", bp, css))
81 .collect::<Vec<_>>()
85 .collect::<Vec<_>>()
82 .join("|");
86 .join("|");
87+ let cq_key: String = container
89+ .map(|(bp, css)| format!("{}={}", bp, css))
83− let key = format!("{}|{}|{}|{}|{}|{}|{}|{}|{}", base, pseudo_key, resp_key, dark, print, motion_safe, motion_reduce, landscape, portrait);
92+ let key = format!("{}|{}|{}|{}|{}|{}|{}|{}|{}|{}", base, pseudo_key, resp_key, dark, print, motion_safe, motion_reduce, landscape, portrait, cq_key);
84 if let Some(&idx) = self.index.get(&key) {
93 if let Some(&idx) = self.index.get(&key) {
85 return Some(self.entries[idx].class_name.clone());
94 return Some(self.entries[idx].class_name.clone());
@@ -97,6 +106,7 @@ impl StyleCollector {
97 motion_reduce,
106 motion_reduce,
98 landscape,
107 landscape,
99 portrait,
108 portrait,
101 self.index.insert(key, idx);
111 self.index.insert(key, idx);
102 Some(name)
112 Some(name)
@@ -261,6 +271,35 @@ impl StyleCollector {
274+ // Container query rules grouped by breakpoint
275+ for &(bp_name, bp_width) in BREAKPOINTS {
276+ let mut cq_css = String::new();
277+ for e in &self.entries {
278+ for (bp, rule_css) in &e.container {
279+ if bp == bp_name && !rule_css.is_empty() {
281+ cq_css.push_str(&format!(" .{} {{{}}}\n", e.class_name, rule_css));
283+ cq_css.push_str(&format!(".{}{{{}}}", e.class_name, rule_css));
288+ if !cq_css.is_empty() {
290+ css.push_str(&format!(
291+ "@container (min-width: {}) {{\n{}}}\n",
295+ css.push_str(&format!(
296+ "@container(min-width:{}){{{}}}",
@@ -535,7 +574,7 @@ fn generate_node(
535 | Some(ElementKind::Blockquote)
574 | Some(ElementKind::Blockquote)
536 | Some(ElementKind::Dialog) | Some(ElementKind::DefinitionList)
575 | Some(ElementKind::Dialog) | Some(ElementKind::DefinitionList)
537 | Some(ElementKind::DefinitionDescription) | Some(ElementKind::Fieldset)
576 | Some(ElementKind::DefinitionDescription) | Some(ElementKind::Fieldset)
538− | Some(ElementKind::Datalist)
577+ | Some(ElementKind::Datalist) | Some(ElementKind::Grid) | Some(ElementKind::Stack)
540 if needs_wrap {
579 if needs_wrap {
541 out.push_str(&ctx.indent());
580 out.push_str(&ctx.indent());
@@ -616,7 +655,7 @@ fn generate_element(
616 ctx: &mut GenContext,
655 ctx: &mut GenContext,
618 // Self-closing elements
657 // Self-closing elements
619− if matches!(elem.kind, ElementKind::Image | ElementKind::Input | ElementKind::HorizontalRule | ElementKind::Source) {
658+ if matches!(elem.kind, ElementKind::Image | ElementKind::Input | ElementKind::HorizontalRule | ElementKind::Source | ElementKind::Spacer) {
620 generate_self_closing(elem, parent_kind, out, styles, ctx);
659 generate_self_closing(elem, parent_kind, out, styles, ctx);
@@ -635,7 +674,8 @@ fn generate_element(
637 let tag = match &elem.kind {
676 let tag = match &elem.kind {
638− ElementKind::Row | ElementKind::Column | ElementKind::El => "div",
677+ ElementKind::Row | ElementKind::Column | ElementKind::El
678+ | ElementKind::Grid | ElementKind::Stack => "div",
639 ElementKind::Text => "span",
679 ElementKind::Text => "span",
640 ElementKind::Paragraph => "p",
680 ElementKind::Paragraph => "p",
641 ElementKind::Link => "a",
681 ElementKind::Link => "a",
@@ -695,9 +735,11 @@ fn generate_element(
695 ElementKind::Iframe => "iframe",
735 ElementKind::Iframe => "iframe",
696 ElementKind::Output => "output",
736 ElementKind::Output => "output",
697 ElementKind::Canvas => "canvas",
737 ElementKind::Canvas => "canvas",
738+ ElementKind::Badge => "span",
739+ ElementKind::Tooltip => "span",
698 ElementKind::Image | ElementKind::Input | ElementKind::HorizontalRule
740 ElementKind::Image | ElementKind::Input | ElementKind::HorizontalRule
699 | ElementKind::Children | ElementKind::Slot(_) | ElementKind::Fragment
741 | ElementKind::Children | ElementKind::Slot(_) | ElementKind::Fragment
700− | ElementKind::Source => unreachable!(),
742+ | ElementKind::Source | ElementKind::Spacer => unreachable!(),
703 let kind_label = match elem.kind {
745 let kind_label = match elem.kind {
@@ -754,6 +796,10 @@ fn generate_element(
754 ElementKind::Iframe => "iframe",
796 ElementKind::Iframe => "iframe",
755 ElementKind::Output => "output",
797 ElementKind::Output => "output",
756 ElementKind::Canvas => "canvas",
798 ElementKind::Canvas => "canvas",
799+ ElementKind::Grid => "grid",
800+ ElementKind::Stack => "stack",
801+ ElementKind::Badge => "badge",
802+ ElementKind::Tooltip => "tooltip",
@@ -804,6 +850,15 @@ fn generate_element(
854+ if elem.kind == ElementKind::Tooltip {
855+ if let Some(text) = &elem.argument {
856+ out.push_str(" title=\"");
857+ out.push_str(&html_escape(text));
807 // Time datetime
862 // Time datetime
808 if elem.kind == ElementKind::Time {
863 if elem.kind == ElementKind::Time {
809 if let Some(dt) = elem.attrs.iter().find(|a| a.key == "datetime") {
864 if let Some(dt) = elem.attrs.iter().find(|a| a.key == "datetime") {
@@ -855,6 +910,8 @@ fn generate_element(
855 | ElementKind::Abbr
910 | ElementKind::Abbr
856 | ElementKind::Time
911 | ElementKind::Time
857 | ElementKind::DefinitionDescription
912 | ElementKind::DefinitionDescription
913+ | ElementKind::Badge
914+ | ElementKind::Tooltip
859 if let Some(text) = &elem.argument {
916 if let Some(text) = &elem.argument {
860 out.push_str(&html_escape(text));
917 out.push_str(&html_escape(text));
@@ -894,6 +951,7 @@ fn generate_self_closing(
894 ElementKind::Input => ("input", "input"),
951 ElementKind::Input => ("input", "input"),
895 ElementKind::HorizontalRule => ("hr", "hr"),
952 ElementKind::HorizontalRule => ("hr", "hr"),
896 ElementKind::Source => ("source", "source"),
953 ElementKind::Source => ("source", "source"),
954+ ElementKind::Spacer => ("div", "spacer"),
897 _ => unreachable!(),
955 _ => unreachable!(),
@@ -905,12 +963,34 @@ fn generate_self_closing(
905 out.push('<');
963 out.push('<');
906 out.push_str(tag);
964 out.push_str(tag);
908− // Image src
966+ // Image src (with optional non-SVG base64 inlining)
909 if elem.kind == ElementKind::Image {
967 if elem.kind == ElementKind::Image {
910 let src = elem.argument.as_deref().unwrap_or("");
968 let src = elem.argument.as_deref().unwrap_or("");
969+ let is_inline = elem.attrs.iter().any(|a| a.key == "inline");
970+ if is_inline && !src.is_empty() && !src.ends_with(".svg") {
971+ let mime = if src.ends_with(".png") { "image/png" }
972+ else if src.ends_with(".jpg") || src.ends_with(".jpeg") { "image/jpeg" }
973+ else if src.ends_with(".gif") { "image/gif" }
974+ else if src.ends_with(".webp") { "image/webp" }
975+ else if src.ends_with(".avif") { "image/avif" }
976+ else { "application/octet-stream" };
977+ if let Ok(data) = std::fs::read(src) {
978+ let b64 = base64_encode(&data);
979+ out.push_str(" src=\"data:");
981+ out.push_str(";base64,");
911− out.push_str(" src=\"");
982+ out.push_str(&b64);
985+ out.push_str(" src=\"");
986+ out.push_str(&html_escape(src));
912− out.push_str(&html_escape(src));
990+ out.push_str(" src=\"");
991+ out.push_str(&html_escape(src));
913− out.push('"');
992+ out.push('"');
916 emit_class_attr(out, gen_class.as_deref(), user_class.as_deref());
996 emit_class_attr(out, gen_class.as_deref(), user_class.as_deref());
@@ -996,6 +1076,28 @@ fn compute_class(
1079+ // Collect nth:EXPR: dynamic pseudo selectors
1080+ let mut nth_prefixes: Vec<String> = Vec::new();
1081+ for attr in attrs {
1082+ if attr.key.starts_with("nth:") {
1083+ let rest = &attr.key[4..];
1084+ if let Some(colon_pos) = rest.find(':') {
1085+ let prefix = format!("nth:{}:", &rest[..colon_pos]);
1086+ if !nth_prefixes.contains(&prefix) {
1087+ nth_prefixes.push(prefix);
1092+ for prefix in &nth_prefixes {
1093+ let expr = &prefix[4..prefix.len() - 1];
1094+ let selector = format!(":nth-child({})", expr);
1095+ let css = attrs_to_css(attrs, prefix, kind, parent_kind);
1096+ if !css.is_empty() {
1097+ pseudo.push((selector, css));
999 // Collect responsive overrides
1101 // Collect responsive overrides
1000 let mut responsive = Vec::new();
1102 let mut responsive = Vec::new();
1001 for &(bp_name, _) in BREAKPOINTS {
1103 for &(bp_name, _) in BREAKPOINTS {
@@ -1006,6 +1108,16 @@ fn compute_class(
1111+ // Collect container query overrides
1112+ let mut container = Vec::new();
1113+ for &(bp_name, _) in BREAKPOINTS {
1114+ let prefix = format!("cq-{}:", bp_name);
1115+ let css = attrs_to_css(attrs, &prefix, kind, parent_kind);
1116+ if !css.is_empty() {
1117+ container.push((bp_name.to_string(), css));
1009 let dark = attrs_to_css(attrs, "dark:", kind, parent_kind);
1121 let dark = attrs_to_css(attrs, "dark:", kind, parent_kind);
1010 let print = attrs_to_css(attrs, "print:", kind, parent_kind);
1122 let print = attrs_to_css(attrs, "print:", kind, parent_kind);
1011 let motion_safe = attrs_to_css(attrs, "motion-safe:", kind, parent_kind);
1123 let motion_safe = attrs_to_css(attrs, "motion-safe:", kind, parent_kind);
@@ -1013,7 +1125,7 @@ fn compute_class(
1013 let landscape = attrs_to_css(attrs, "landscape:", kind, parent_kind);
1125 let landscape = attrs_to_css(attrs, "landscape:", kind, parent_kind);
1014 let portrait = attrs_to_css(attrs, "portrait:", kind, parent_kind);
1126 let portrait = attrs_to_css(attrs, "portrait:", kind, parent_kind);
1016− styles.get_class(base, pseudo, responsive, dark, print, motion_safe, motion_reduce, landscape, portrait)
1128+ styles.get_class(base, pseudo, responsive, dark, print, motion_safe, motion_reduce, landscape, portrait, container)
1019fn emit_class_attr(out: &mut String, gen_class: Option<&str>, user_class: Option<&str>) {
1131fn emit_class_attr(out: &mut String, gen_class: Option<&str>, user_class: Option<&str>) {
@@ -1059,14 +1171,18 @@ const PSEUDO_PREFIXES: &[(&str, &str)] = &[
1059 ("even:", ":nth-child(even)"),
1171 ("even:", ":nth-child(even)"),
1060 ("before:", "::before"),
1172 ("before:", "::before"),
1061 ("after:", "::after"),
1173 ("after:", "::after"),
1174+ ("selection:", "::selection"),
1063const RESPONSIVE_PREFIXES: &[&str] = &["sm:", "md:", "lg:", "xl:", "2xl:"];
1176const RESPONSIVE_PREFIXES: &[&str] = &["sm:", "md:", "lg:", "xl:", "2xl:"];
1064const MEDIA_PREFIXES: &[&str] = &["dark:", "print:", "motion-safe:", "motion-reduce:", "landscape:", "portrait:"];
1177const MEDIA_PREFIXES: &[&str] = &["dark:", "print:", "motion-safe:", "motion-reduce:", "landscape:", "portrait:"];
1178+const CONTAINER_QUERY_PREFIXES: &[&str] = &["cq-sm:", "cq-md:", "cq-lg:", "cq-xl:", "cq-2xl:"];
1066fn is_prefixed_attr(key: &str) -> bool {
1180fn is_prefixed_attr(key: &str) -> bool {
1067 PSEUDO_PREFIXES.iter().any(|&(p, _)| key.starts_with(p))
1181 PSEUDO_PREFIXES.iter().any(|&(p, _)| key.starts_with(p))
1068 || RESPONSIVE_PREFIXES.iter().any(|p| key.starts_with(p))
1182 || RESPONSIVE_PREFIXES.iter().any(|p| key.starts_with(p))
1069 || MEDIA_PREFIXES.iter().any(|p| key.starts_with(p))
1183 || MEDIA_PREFIXES.iter().any(|p| key.starts_with(p))
1184+ || CONTAINER_QUERY_PREFIXES.iter().any(|p| key.starts_with(p))
1185+ || key.starts_with("nth:")
1072fn attrs_to_css(
1188fn attrs_to_css(
@@ -1109,6 +1225,11 @@ fn attrs_to_css(
1109 ElementKind::DefinitionDescription => css.push_str("margin:0;display:flex;flex-direction:column;"),
1225 ElementKind::DefinitionDescription => css.push_str("margin:0;display:flex;flex-direction:column;"),
1110 ElementKind::Fieldset => css.push_str("display:flex;flex-direction:column;border:1px solid currentColor;padding:0.5em;margin:0;"),
1226 ElementKind::Fieldset => css.push_str("display:flex;flex-direction:column;border:1px solid currentColor;padding:0.5em;margin:0;"),
1111 ElementKind::Kbd => css.push_str("font-family:ui-monospace,monospace;"),
1227 ElementKind::Kbd => css.push_str("font-family:ui-monospace,monospace;"),
1228+ ElementKind::Grid => css.push_str("display:grid;"),
1229+ ElementKind::Stack => css.push_str("position:relative;"),
1230+ ElementKind::Badge => css.push_str("display:inline-flex;align-items:center;justify-content:center;padding:2px 8px;border-radius:9999px;font-size:0.75rem;font-weight:600;line-height:1;"),
1231+ ElementKind::Tooltip => css.push_str("position:relative;cursor:help;"),
1232+ ElementKind::Spacer => css.push_str("flex:1;"),
@@ -1839,6 +1960,10 @@ fn attrs_to_css(
1839 "content-visibility" => {
1960 "content-visibility" => {
1840 if let Some(v) = val { push_css(&mut css, "content-visibility", v); }
1961 if let Some(v) = val { push_css(&mut css, "content-visibility", v); }
1964+ if let Some(v) = val { push_css(&mut css, "direction", v); }
1842 "content" => {
1967 "content" => {
1843 if let Some(v) = val {
1968 if let Some(v) = val {
1844 // Wrap in quotes if not already quoted and not a CSS keyword
1969 // Wrap in quotes if not already quoted and not a CSS keyword