@@ -226,12 +226,22 @@ impl Evaluator {
226226 nodes
227227 }
228228
229− fn eval(&mut self, node: &Syntax, ctx: &mut ParseContext) -> Result<Option<Vec<Node>>, ParseError> {
229+ fn eval(
230+ &mut self,
231+ node: &Syntax,
232+ ctx: &mut ParseContext,
233+ ) -> Result<Option<Vec<Node>>, ParseError> {
230234 let line_num = node.line();
231235 ctx.current_line = line_num;
232236 let (content, current_indent, children) = match node {
233237 Syntax::Raw { text, .. } => return Ok(Some(vec![Node::Raw(text.clone())])),
234− Syntax::Function { name, params, defaults, body, .. } => {
238+ Syntax::Function {
239+ name,
240+ params,
241+ defaults,
242+ body,
243+ ..
244+ } => {
235245 self.define_function(name, params, defaults, body, line_num, ctx);
236246 return Ok(None);
237247 }
@@ -250,10 +260,20 @@ impl Evaluator {
250260 }
251261 return Ok(chosen.map(|body| self.eval_scoped(body, ctx)));
252262 }
253− Syntax::Each { header, body, empty, .. } => {
263+ Syntax::Each {
264+ header,
265+ body,
266+ empty,
267+ ..
268+ } => {
254269 return self.eval_each(header, body, empty, line_num, ctx).map(Some);
255270 }
256− Syntax::Line { text, indent, children, .. } => (text.clone(), *indent, children),
271+ Syntax::Line {
272+ text,
273+ indent,
274+ children,
275+ ..
276+ } => (text.clone(), *indent, children),
257277 };
258278
259279 // --- Directives ---
@@ -288,7 +308,6 @@ impl Evaluator {
288308 return Ok(None);
289309 }
290310
291−
292311 if let Some(rest) = content.strip_prefix("@let ") {
293312 let rest = rest.trim();
294313
@@ -355,7 +374,6 @@ impl Evaluator {
355374 return Ok(None);
356375 }
357376
358−
359377 if content == "@head" || content.starts_with("@head ") {
360378 let trimmed = block_text(children).trim().to_string();
361379 if !trimmed.is_empty() {
@@ -373,13 +391,13 @@ impl Evaluator {
373391 return Ok(None);
374392 }
375393
376−
377394 // --- @markdown block or file (convert markdown to HTML) ---
378395 if content.trim() == "@markdown" || content.trim().starts_with("@markdown ") {
379396 let arg = content.trim().strip_prefix("@markdown").unwrap().trim();
380397 if arg.is_empty() {
381398 // Inline markdown block: indented children
382− let md_lines: Vec<String> = block_text(children).lines().map(String::from).collect();
399+ let md_lines: Vec<String> =
400+ block_text(children).lines().map(String::from).collect();
383401 let html = markdown_to_html(&md_lines);
384402 return Ok(Some(vec![Node::Raw(html)]));
385403 } else {
@@ -416,8 +434,6 @@ impl Evaluator {
416434 }
417435 }
418436
419−
420−
421437 if let Some(rest) = content.strip_prefix("@include ") {
422438 let rest = rest.trim();
423439 let filename = rest
@@ -487,8 +503,6 @@ impl Evaluator {
487503 return Ok(Some(included_nodes));
488504 }
489505
490−
491−
492506 // --- @data (load JSON file into variables) ---
493507
494508 // @data $name file.json values as $name.key
@@ -737,7 +751,10 @@ impl Evaluator {
737751 message: "@each requires: @each $item in LIST".to_string(),
738752 });
739753 };
740− let names: Vec<&str> = names.split(',').map(|v| v.trim().trim_start_matches('$')).collect();
754+ let names: Vec<&str> = names
755+ .split(',')
756+ .map(|v| v.trim().trim_start_matches('$'))
757+ .collect();
741758 if names.len() > 2 {
742759 return Err(ParseError {
743760 line: line_num,
@@ -752,11 +769,16 @@ impl Evaluator {
752769
753770 // A list loaded from JSON, by name
754771 let data_list = list_src.strip_prefix('$').and_then(|name| {
755− let len = ctx.variables.get(&format!("{}#", name))?.parse::<usize>().ok()?;
772+ let len = ctx
773+ .variables
774+ .get(&format!("{}#", name))?
775+ .parse::<usize>()
776+ .ok()?;
756777 Some((name.to_string(), len))
757778 });
758779 let undefined = list_src.strip_prefix('$').filter(|name| {
759− name.chars().all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.'))
780+ name.chars()
781+ .all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.'))
760782 && !ctx.variables.contains_key(*name)
761783 });
762784 let text_items: Vec<String> = match (&data_list, undefined) {
@@ -963,7 +985,10 @@ impl Evaluator {
963985
964986 // Attributes that aren't parameters style the function's root
965987 // element, and a scoped @style's class goes on it too.
966− let scope_class = ctx.scoped_functions.contains(name).then(|| format!("hl-{}", name));
988+ let scope_class = ctx
989+ .scoped_functions
990+ .contains(name)
991+ .then(|| format!("hl-{}", name));
967992 if !forwarded.is_empty() || scope_class.is_some() {
968993 let mut roots = result_nodes.iter_mut().filter_map(|n| match n {
969994 Node::Element(e) => Some(e),
@@ -981,7 +1006,8 @@ impl Evaluator {
9811006 match root.attrs.iter_mut().find(|a| a.html && a.key == "class") {
9821007 Some(attr) => {
9831008 let existing = attr.value.take().unwrap_or_default();
984− attr.value = Some(format!("{} {}", existing, class).trim().to_string());
1009+ attr.value =
1010+ Some(format!("{} {}", existing, class).trim().to_string());
9851011 }
9861012 None => root.attrs.push(Attribute {
9871013 key: "class".to_string(),
@@ -1184,7 +1210,10 @@ fn parse_single_element(
11841210 Some(restore_escapes(&rest_sub))
11851211 }
11861212 } else {
1187− Some(restore_escapes(&substitute_vars(&protect_escapes(&rest), &ctx.variables)))
1213+ Some(restore_escapes(&substitute_vars(
1214+ &protect_escapes(&rest),
1215+ &ctx.variables,
1216+ )))
11881217 };
11891218
11901219 // For @slot, the argument is the slot name
@@ -1231,17 +1260,7 @@ pub fn known_directives() -> &'static [&'static str] {
12311260}
12321261
12331262const KNOWN_DIRECTIVES: &[&str] = &[
1234− "page",
1235− "let",
1236− "include",
1237− "raw",
1238− "if",
1239− "else",
1240− "each",
1241− "meta",
1242− "head",
1243− "style",
1244− "markdown",
1263+ "page", "let", "include", "raw", "if", "else", "each", "meta", "head", "style", "markdown",
12451264 "data",
12461265];
12471266
@@ -1935,7 +1954,10 @@ fn parse_attr_list(
19351954
19361955 // Color validation for hex colors
19371956 if !attr.html
1938− && matches!(crate::vocab::base_attribute(&attr.key), "background" | "color")
1957+ && matches!(
1958+ crate::vocab::base_attribute(&attr.key),
1959+ "background" | "color"
1960+ )
19391961 && let Some(ref val) = attr.value
19401962 && val.starts_with('#')
19411963 && !is_valid_hex_color(val)
@@ -2249,8 +2271,6 @@ fn numeric_range(start: i64, end: i64, step: i64) -> Vec<String> {
22492271 items
22502272}
22512273
2252−
2253−
22542274/// Evaluate `if(cond, a)` or `if(cond, a, b)` when it is all of `text`,
22552275/// returning the chosen branch (empty when `cond` fails and there is no `b`).
22562276fn choose_if(text: &str, ctx: &mut ParseContext, line: usize) -> Option<String> {
@@ -2376,10 +2396,11 @@ pub fn lint(nodes: &[Node]) -> Vec<Diagnostic> {
23762396 depth
23772397 ));
23782398 }
2379− if matches!(elem.kind, ElementKind::Row | ElementKind::El)
2380− && elem.children.is_empty()
2381− {
2382− warn(format!("empty container (@{}) has no children", elem.kind.name()));
2399+ if matches!(elem.kind, ElementKind::Row | ElementKind::El) && elem.children.is_empty() {
2400+ warn(format!(
2401+ "empty container (@{}) has no children",
2402+ elem.kind.name()
2403+ ));
23832404 }
23842405 if elem.kind.is_tag("button") && !elem.attrs.iter().any(|a| a.key == "type") {
23852406 warn("@button missing 'type' attribute (defaults to submit)".to_string());
@@ -2392,7 +2413,6 @@ pub fn lint(nodes: &[Node]) -> Vec<Diagnostic> {
23922413 out
23932414}
23942415
2395−
23962416fn validate_tree(
23972417 nodes: &[Node],
23982418 parent_kind: Option<&ElementKind>,
@@ -2475,8 +2495,7 @@ fn validate_tree(
24752495 }
24762496
24772497 // 'rows'/'cols' only on @textarea
2478− if (base == "rows" || base == "cols") && !elem.kind.is_tag("textarea")
2479− {
2498+ if (base == "rows" || base == "cols") && !elem.kind.is_tag("textarea") {
24802499 diagnostics.push(Diagnostic {
24812500 line: elem.line_num,
24822501 column: None,
@@ -2526,9 +2545,7 @@ fn validate_tree(
25262545 source_line: None,
25272546 });
25282547 }
2529− if elem.kind.is_tag("input")
2530− && !elem.attrs.iter().any(|a| a.key == "type")
2531− {
2548+ if elem.kind.is_tag("input") && !elem.attrs.iter().any(|a| a.key == "type") {
25322549 diagnostics.push(Diagnostic {
25332550 line: elem.line_num,
25342551 column: None,
@@ -2570,7 +2587,10 @@ fn validate_tree(
25702587 .find(|a| crate::vocab::base_attribute(&a.key) == "color")
25712588 .and_then(|a| a.value.as_deref());
25722589 if let (Some(bg), Some(fg)) = (bg_color, fg_color)
2573− && let (Some(bg_rgb), Some(fg_rgb)) = (crate::expr::parse_hex_rgb(bg), crate::expr::parse_hex_rgb(fg))
2590+ && let (Some(bg_rgb), Some(fg_rgb)) = (
2591+ crate::expr::parse_hex_rgb(bg),
2592+ crate::expr::parse_hex_rgb(fg),
2593+ )
25742594 {
25752595 let ratio = contrast_ratio(bg_rgb, fg_rgb);
25762596 if ratio < 4.5 {
@@ -2612,9 +2632,7 @@ fn validate_tree(
26122632 }
26132633
26142634 // @iframe should have title attribute
2615− if elem.kind.is_tag("iframe")
2616− && !elem.attrs.iter().any(|a| a.key == "title")
2617− {
2635+ if elem.kind.is_tag("iframe") && !elem.attrs.iter().any(|a| a.key == "title") {
26182636 diagnostics.push(Diagnostic {
26192637 line: elem.line_num,
26202638 column: None,
@@ -2729,7 +2747,9 @@ fn substitute_vars(input: &str, vars: &HashMap<String, String>) -> String {
27292747 rest = &after[close + 1..];
27302748 continue;
27312749 }
2732− let mut end = after.find(|c: char| !is_name_char(c)).unwrap_or(after.len());
2750+ let mut end = after
2751+ .find(|c: char| !is_name_char(c))
2752+ .unwrap_or(after.len());
27332753 while end > 0 && after[..end].ends_with('.') {
27342754 end -= 1;
27352755 }
@@ -2756,7 +2776,10 @@ fn inline_svg(node: Node, line_num: usize, ctx: &mut ParseContext) -> Node {
27562776 };
27572777 let is_inline_svg = elem.kind == ElementKind::Image
27582778 && elem.attrs.iter().any(|a| a.key == "inline" && !a.html)
2759− && elem.argument.as_deref().is_some_and(|src| src.ends_with(".svg"));
2779+ && elem
2780+ .argument
2781+ .as_deref()
2782+ .is_some_and(|src| src.ends_with(".svg"));
27602783 if !is_inline_svg {
27612784 return node;
27622785 }
@@ -3231,10 +3254,17 @@ fn flatten_json(prefix: &str, value: &JsonValue, vars: &mut HashMap<String, Stri
32313254/// JSON array, else its text.
32323255fn lookup(vars: &HashMap<String, String>, name: &str) -> Option<crate::expr::Value> {
32333256 use crate::expr::Value;
3234− match vars.get(&format!("{}#", name)).and_then(|n| n.parse::<usize>().ok()) {
3257+ match vars
3258+ .get(&format!("{}#", name))
3259+ .and_then(|n| n.parse::<usize>().ok())
3260+ {
32353261 Some(len) => Some(Value::List(
32363262 (0..len)
3237− .map(|i| vars.get(&format!("{}.{}", name, i)).cloned().unwrap_or_default())
3263+ .map(|i| {
3264+ vars.get(&format!("{}.{}", name, i))
3265+ .cloned()
3266+ .unwrap_or_default()
3267+ })
32383268 .collect(),
32393269 )),
32403270 None => vars.get(name).cloned().map(Value::Str),