@@ -336,7 +336,7 @@ impl Parser {
336336 return Ok(None);
337337 }
338338
339− if content.strip_prefix("@head").is_some() {
339+ if content == "@head" || content.starts_with("@head ") {
340340 // Collect indented body lines as raw head content
341341 let mut head_content = String::new();
342342 while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
@@ -682,6 +682,113 @@ impl Parser {
682682 return Ok(Some(all_nodes));
683683 }
684684
685+ // --- @match ---
686+
687+ if let Some(rest) = content.strip_prefix("@match ") {
688+ let match_val = substitute_vars(rest.trim(), &ctx.variables);
689+ track_var_refs(rest.trim(), &mut ctx.used_variables);
690+
691+ // Collect all child lines
692+ let mut match_lines = Vec::new();
693+ while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
694+ match_lines.push(self.lines[self.pos].clone());
695+ self.pos += 1;
696+ }
697+
698+ if match_lines.is_empty() {
699+ return Ok(None);
700+ }
701+
702+ let case_indent = match_lines[0].indent;
703+
704+ // Group into cases: (Some(value), body) or (None, body) for @default
705+ let mut cases: Vec<(Option<String>, Vec<Line>)> = Vec::new();
706+ let mut mi = 0;
707+ while mi < match_lines.len() {
708+ if match_lines[mi].indent == case_indent {
709+ if let LineContent::Normal(ref s) = match_lines[mi].content {
710+ let trimmed = s.trim();
711+ if let Some(case_val) = trimmed.strip_prefix("@case ") {
712+ let case_val = substitute_vars(case_val.trim(), &ctx.variables);
713+ mi += 1;
714+ let mut body = Vec::new();
715+ while mi < match_lines.len() && match_lines[mi].indent > case_indent {
716+ body.push(match_lines[mi].clone());
717+ mi += 1;
718+ }
719+ cases.push((Some(case_val), body));
720+ } else if trimmed == "@default" {
721+ mi += 1;
722+ let mut body = Vec::new();
723+ while mi < match_lines.len() && match_lines[mi].indent > case_indent {
724+ body.push(match_lines[mi].clone());
725+ mi += 1;
726+ }
727+ cases.push((None, body));
728+ } else {
729+ mi += 1;
730+ }
731+ } else {
732+ mi += 1;
733+ }
734+ } else {
735+ mi += 1;
736+ }
737+ }
738+
739+ // Find first matching case or @default
740+ let body_lines = cases
741+ .into_iter()
742+ .find(|(case_val, _)| match case_val {
743+ Some(v) => *v == match_val,
744+ None => true,
745+ })
746+ .map(|(_, lines)| lines)
747+ .unwrap_or_default();
748+
749+ if body_lines.is_empty() {
750+ return Ok(None);
751+ }
752+
753+ let min_indent = body_lines.iter().map(|l| l.indent).min().unwrap_or(0);
754+ let adjusted: Vec<Line> = body_lines
755+ .iter()
756+ .map(|l| Line {
757+ indent: l.indent - min_indent,
758+ content: l.content.clone(),
759+ line_num: l.line_num,
760+ })
761+ .collect();
762+
763+ let saved_vars = ctx.variables.clone();
764+ let mut body_parser = Parser {
765+ lines: adjusted,
766+ pos: 0,
767+ };
768+ let nodes = body_parser.parse_children(0, ctx);
769+ ctx.variables = saved_vars;
770+ return Ok(Some(nodes));
771+ }
772+
773+ // --- @warn / @debug ---
774+
775+ if let Some(rest) = content.strip_prefix("@warn ") {
776+ let msg = substitute_vars(rest.trim(), &ctx.variables);
777+ ctx.diagnostics.push(Diagnostic {
778+ line: line_num,
779+ message: msg,
780+ severity: Severity::Warning,
781+ source_line: Some(content.clone()),
782+ });
783+ return Ok(None);
784+ }
785+
786+ if let Some(rest) = content.strip_prefix("@debug ") {
787+ let msg = substitute_vars(rest.trim(), &ctx.variables);
788+ eprintln!("debug: line {}: {}", line_num, msg);
789+ return Ok(None);
790+ }
791+
685792 // --- @keyframes directive ---
686793
687794 if let Some(rest) = content.strip_prefix("@keyframes ") {
@@ -1015,11 +1122,16 @@ fn parse_single_element(
10151122const KNOWN_ELEMENTS: &[&str] = &[
10161123 "row", "column", "col", "el", "text", "paragraph", "p", "image", "img", "link", "children",
10171124 "input", "button", "select", "textarea", "option", "label", "slot",
1125+ "nav", "header", "footer", "main", "section", "article", "aside",
1126+ "list", "item", "li",
1127+ "table", "thead", "tbody", "tr", "td", "th",
1128+ "video", "audio",
10181129];
10191130
10201131const KNOWN_DIRECTIVES: &[&str] = &[
10211132 "page", "let", "define", "fn", "include", "import", "raw", "keyframes",
10221133 "if", "else", "each", "meta", "head", "style",
1134+ "match", "case", "default", "warn", "debug",
10231135];
10241136
10251137fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseError> {
@@ -1039,6 +1151,23 @@ fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseErro
10391151 "option" | "opt" => Ok(ElementKind::Option),
10401152 "label" => Ok(ElementKind::Label),
10411153 "slot" => Ok(ElementKind::Slot(String::new())), // slot name filled in by parse_single_element
1154+ "nav" => Ok(ElementKind::Nav),
1155+ "header" => Ok(ElementKind::Header),
1156+ "footer" => Ok(ElementKind::Footer),
1157+ "main" => Ok(ElementKind::Main),
1158+ "section" => Ok(ElementKind::Section),
1159+ "article" => Ok(ElementKind::Article),
1160+ "aside" => Ok(ElementKind::Aside),
1161+ "list" => Ok(ElementKind::List),
1162+ "item" | "li" => Ok(ElementKind::ListItem),
1163+ "table" => Ok(ElementKind::Table),
1164+ "thead" => Ok(ElementKind::TableHead),
1165+ "tbody" => Ok(ElementKind::TableBody),
1166+ "tr" => Ok(ElementKind::TableRow),
1167+ "td" => Ok(ElementKind::TableCell),
1168+ "th" => Ok(ElementKind::TableHeaderCell),
1169+ "video" => Ok(ElementKind::Video),
1170+ "audio" => Ok(ElementKind::Audio),
10421171 _ => {
10431172 let all_known: Vec<&str> = KNOWN_ELEMENTS
10441173 .iter()
@@ -1126,6 +1255,17 @@ const KNOWN_ATTRS: &[&str] = &[
11261255 "min", "max", "step", "pattern", "maxlength", "rows", "cols", "multiple",
11271256 // Accessibility
11281257 "alt", "role", "tabindex", "title",
1258+ // CSS: aspect-ratio, outline, logical properties, scroll-snap
1259+ "aspect-ratio", "outline",
1260+ "padding-inline", "padding-block", "margin-inline", "margin-block",
1261+ "scroll-snap-type", "scroll-snap-align",
1262+ // Media attributes
1263+ "controls", "autoplay", "loop", "muted", "poster", "preload",
1264+ "loading", "decoding",
1265+ // List
1266+ "ordered",
1267+ // Media src (explicit attribute form)
1268+ "src",
11291269];
11301270
11311271/// Attributes that expect purely numeric values (px-based) or values with CSS units.
@@ -1556,11 +1696,33 @@ fn element_kind_name(kind: &ElementKind) -> &'static str {
15561696 ElementKind::Option => "@option",
15571697 ElementKind::Label => "@label",
15581698 ElementKind::Slot(_) => "@slot",
1699+ ElementKind::Nav => "@nav",
1700+ ElementKind::Header => "@header",
1701+ ElementKind::Footer => "@footer",
1702+ ElementKind::Main => "@main",
1703+ ElementKind::Section => "@section",
1704+ ElementKind::Article => "@article",
1705+ ElementKind::Aside => "@aside",
1706+ ElementKind::List => "@list",
1707+ ElementKind::ListItem => "@item",
1708+ ElementKind::Table => "@table",
1709+ ElementKind::TableHead => "@thead",
1710+ ElementKind::TableBody => "@tbody",
1711+ ElementKind::TableRow => "@tr",
1712+ ElementKind::TableCell => "@td",
1713+ ElementKind::TableHeaderCell => "@th",
1714+ ElementKind::Video => "@video",
1715+ ElementKind::Audio => "@audio",
15591716 }
15601717}
15611718
15621719fn is_container(kind: &ElementKind) -> bool {
1563− matches!(kind, ElementKind::Row | ElementKind::Column | ElementKind::El)
1720+ matches!(kind,
1721+ ElementKind::Row | ElementKind::Column | ElementKind::El
1722+ | ElementKind::Nav | ElementKind::Header | ElementKind::Footer
1723+ | ElementKind::Main | ElementKind::Section | ElementKind::Article
1724+ | ElementKind::Aside | ElementKind::List | ElementKind::ListItem
1725+ )
15641726}
15651727
15661728fn validate_tree(
@@ -1651,6 +1813,34 @@ fn validate_tree(
16511813 source_line: None,
16521814 });
16531815 }
1816+
1817+ // 'ordered' only on @list
1818+ if base == "ordered" && !matches!(elem.kind, ElementKind::List) {
1819+ diagnostics.push(Diagnostic {
1820+ line: elem.line_num,
1821+ message: format!(
1822+ "'ordered' has no effect on {} (only works on @list)",
1823+ element_kind_name(&elem.kind)
1824+ ),
1825+ severity: Severity::Warning,
1826+ source_line: None,
1827+ });
1828+ }
1829+
1830+ // Media-specific attributes only on @video/@audio
1831+ if matches!(base, "controls" | "autoplay" | "loop" | "muted" | "poster" | "preload")
1832+ && !matches!(elem.kind, ElementKind::Video | ElementKind::Audio)
1833+ {
1834+ diagnostics.push(Diagnostic {
1835+ line: elem.line_num,
1836+ message: format!(
1837+ "'{}' has no effect on {} (only works on @video, @audio)",
1838+ base, element_kind_name(&elem.kind)
1839+ ),
1840+ severity: Severity::Warning,
1841+ source_line: None,
1842+ });
1843+ }
16541844 }
16551845 validate_tree(&elem.children, Some(&elem.kind), diagnostics);
16561846 }