@@ -230,9 +230,9 @@ fn collect_images_recursive(nodes: &[Node], hints: &mut Vec<crate::ast::PreloadH
230 for node in nodes {
230 for node in nodes {
231 if hints.len() >= max { return; }
231 if hints.len() >= max { return; }
232 if let Node::Element(elem) = node {
232 if let Node::Element(elem) = node {
233− if elem.kind == ElementKind::Image {
233+ if elem.kind == ElementKind::Image
234− if let Some(ref src) = elem.argument {
234+ && let Some(ref src) = elem.argument
235− if !src.is_empty()
235+ && !src.is_empty()
236 && !src.starts_with("data:")
236 && !src.starts_with("data:")
237 && !src.starts_with('#')
237 && !src.starts_with('#')
@@ -242,8 +242,6 @@ fn collect_images_recursive(nodes: &[Node], hints: &mut Vec<crate::ast::PreloadH
242 crossorigin: false,
242 crossorigin: false,
247 collect_images_recursive(&elem.children, hints, max);
245 collect_images_recursive(&elem.children, hints, max);
@@ -270,11 +268,9 @@ fn preprocess(input: &str) -> Vec<Line> {
270 let indent = line.len() - line.trim_start().len();
268 let indent = line.len() - line.trim_start().len();
272 // Handle @raw """..."""
270 // Handle @raw """..."""
273− if trimmed.starts_with("@raw") {
271+ if let Some(raw_rest) = trimmed.strip_prefix("@raw") {
274− let after_raw = trimmed[4..].trim_start();
272+ let after_raw = raw_rest.trim_start();
275− if after_raw.starts_with("\"\"\"") {
276− let after_open = &after_raw[3..];
273+ if let Some(after_open) = after_raw.strip_prefix("\"\"\"") {
278 // Single-line: @raw """content"""
274 // Single-line: @raw """content"""
279 if after_open.ends_with("\"\"\"") && after_open.len() >= 3 {
275 if after_open.ends_with("\"\"\"") && after_open.len() >= 3 {
280 let content = &after_open[..after_open.len() - 3];
276 let content = &after_open[..after_open.len() - 3];
@@ -443,8 +439,8 @@ impl Parser {
443 if let Some((name, value)) = rest.split_once(' ') {
439 if let Some((name, value)) = rest.split_once(' ') {
444 let value = value.trim();
440 let value = value.trim();
445 // Support @let name = expr syntax (strip leading =)
441 // Support @let name = expr syntax (strip leading =)
446− let value = if value.starts_with("= ") {
442+ let value = if let Some(after_eq) = value.strip_prefix("= ") {
447− value[2..].trim()
443+ after_eq.trim()
448 } else if value == "=" {
444 } else if value == "=" {
@@ -453,8 +449,7 @@ impl Parser {
454 // Multi-line @let with triple quotes: @let name """..."""
450 // Multi-line @let with triple quotes: @let name """..."""
455 let value_str;
451 let value_str;
456− let value = if value.starts_with("\"\"\"") {
457− let after_open = &value[3..];
452+ let value = if let Some(after_open) = value.strip_prefix("\"\"\"") {
458 if after_open.ends_with("\"\"\"") && after_open.len() >= 3 {
453 if after_open.ends_with("\"\"\"") && after_open.len() >= 3 {
459 // Single-line triple-quote: @let name """value"""
454 // Single-line triple-quote: @let name """value"""
460 value_str = after_open[..after_open.len()-3].to_string();
455 value_str = after_open[..after_open.len()-3].to_string();
@@ -1024,11 +1019,10 @@ impl Parser {
1024 .cloned()
1019 .cloned()
1025 .collect();
1020 .collect();
1026 for name in new_var_keys {
1021 for name in new_var_keys {
1027− if let Some(val) = ctx.variables.remove(&name) {
1022+ if let Some(val) = ctx.variables.remove(&name)
1028− if !name.starts_with("__") {
1023+ && !name.starts_with("__") {
1029 ctx.variables.insert(format!("{}.{}", prefix, name), val);
1024 ctx.variables.insert(format!("{}.{}", prefix, name), val);
1033 } else {
1027 } else {
1034 // Parse the file but discard DOM nodes — only keep definitions
1028 // Parse the file but discard DOM nodes — only keep definitions
@@ -1063,8 +1057,8 @@ impl Parser {
1063 (rest, None)
1057 (rest, None)
1065 let env_val = std::env::var(var_name).ok().or_else(|| default_val.map(|d| {
1059 let env_val = std::env::var(var_name).ok().or_else(|| default_val.map(|d| {
1066− let d = substitute_vars(d, &ctx.variables);
1061+ substitute_vars(d, &ctx.variables)
1069 match env_val {
1063 match env_val {
1070 Some(val) => {
1064 Some(val) => {
@@ -1357,10 +1351,10 @@ impl Parser {
1357 if let Some(rest) = content.strip_prefix("@use ") {
1351 if let Some(rest) = content.strip_prefix("@use ") {
1358 let rest = rest.trim();
1352 let rest = rest.trim();
1359 // @use "./file.hl" fn1, fn2, define1
1353 // @use "./file.hl" fn1, fn2, define1
1360− let (filename, names_str) = if rest.starts_with('"') {
1354+ let (filename, names_str) = if let Some(after_quote) = rest.strip_prefix('"') {
1361− if let Some(end_quote) = rest[1..].find('"') {
1355+ if let Some(end_quote) = after_quote.find('"') {
1362− let filename = &rest[1..end_quote + 1];
1356+ let filename = &after_quote[..end_quote];
1363− let names = rest[end_quote + 2..].trim();
1357+ let names = after_quote[end_quote + 1..].trim();
1364 (filename.to_string(), names.to_string())
1358 (filename.to_string(), names.to_string())
1365 } else {
1359 } else {
1366 return Err(ParseError {
1360 return Err(ParseError {
@@ -1529,14 +1523,13 @@ impl Parser {
1529 ctx.variables.insert(var_name.clone(), stems.join(" "));
1523 ctx.variables.insert(var_name.clone(), stems.join(" "));
1530 // Also parse each JSON file and store keys as prefix_stem_key
1524 // Also parse each JSON file and store keys as prefix_stem_key
1531 for (stem, text) in &items {
1525 for (stem, text) in &items {
1532− if let Some(json) = parse_json(text.trim()) {
1526+ if let Some(json) = parse_json(text.trim())
1533− if let JsonValue::Object(ref obj) = json {
1527+ && let JsonValue::Object(ref obj) = json {
1534 for (key, val) in obj {
1528 for (key, val) in obj {
1535 let var_key = format!("{}_{}", stem, key);
1529 let var_key = format!("{}_{}", stem, key);
1536 ctx.variables.insert(var_key, json_value_to_string(val));
1530 ctx.variables.insert(var_key, json_value_to_string(val));
1542 return Ok(None);
1535 return Ok(None);
@@ -1734,7 +1727,7 @@ impl Parser {
1734 .and_then(|v| v.parse().ok())
1727 .and_then(|v| v.parse().ok())
1735 .unwrap_or(1);
1728 .unwrap_or(1);
1736 let total_items = items.len();
1729 let total_items = items.len();
1737− let total_pages = (total_items + size - 1) / size;
1730+ let total_pages = total_items.div_ceil(size);
1738 let start = (current_page - 1) * size;
1731 let start = (current_page - 1) * size;
1739 let end = (start + size).min(total_items);
1732 let end = (start + size).min(total_items);
1740 let page_items: Vec<String> = if start < total_items {
1733 let page_items: Vec<String> = if start < total_items {
@@ -1757,17 +1750,15 @@ impl Parser {
1758 // Check for @else block (empty-state fallback)
1751 // Check for @else block (empty-state fallback)
1759 let mut else_lines = Vec::new();
1752 let mut else_lines = Vec::new();
1760− if self.pos < self.lines.len() && self.lines[self.pos].indent == current_indent {
1753+ if self.pos < self.lines.len() && self.lines[self.pos].indent == current_indent
1761− if let LineContent::Normal(ref s) = self.lines[self.pos].content {
1754+ && let LineContent::Normal(ref s) = self.lines[self.pos].content
1762− if s.trim() == "@else" {
1755+ && s.trim() == "@else" {
1763 self.pos += 1; // consume @else
1756 self.pos += 1; // consume @else
1764 while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
1757 while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
1765 else_lines.push(self.lines[self.pos].clone());
1758 else_lines.push(self.lines[self.pos].clone());
1766 self.pos += 1;
1759 self.pos += 1;
1772 if body_lines.is_empty() {
1763 if body_lines.is_empty() {
1773 return Ok(None);
1764 return Ok(None);
@@ -1817,7 +1808,7 @@ impl Parser {
1819 let has_extra_vars = var_names.len() > 2
1810 let has_extra_vars = var_names.len() > 2
1820− || (var_names.len() == 2 && items.first().map_or(false, |it| it.contains(' ')));
1811+ || (var_names.len() == 2 && items.first().is_some_and(|it| it.contains(' ')));
1822 for (i, item) in items.iter().enumerate() {
1813 for (i, item) in items.iter().enumerate() {
1823 // Always expose $_index for the current iteration
1814 // Always expose $_index for the current iteration
@@ -2018,9 +2009,7 @@ impl Parser {
2020 // Also emit the IntersectionObserver script as a sibling raw node
2011 // Also emit the IntersectionObserver script as a sibling raw node
2021− let script = Node::Raw(format!(
2022− "<script>(function(){{var d=document.querySelectorAll('[data-hl-defer]');if(!d.length)return;var o=new IntersectionObserver(function(e){{e.forEach(function(i){{if(i.isIntersecting){{i.target.removeAttribute('hidden');i.target.classList.remove('hl-defer-placeholder');o.unobserve(i.target)}}}});}},{{rootMargin:'200px'}});d.forEach(function(el){{o.observe(el)}})}})()</script>"
2012+ let script = Node::Raw("<script>(function(){var d=document.querySelectorAll('[data-hl-defer]');if(!d.length)return;var o=new IntersectionObserver(function(e){e.forEach(function(i){if(i.isIntersecting){i.target.removeAttribute('hidden');i.target.classList.remove('hl-defer-placeholder');o.unobserve(i.target)}});},{rootMargin:'200px'});d.forEach(function(el){o.observe(el)})})()</script>".to_string());
2025 return Ok(Some(vec![Node::Element(wrapper), script]));
2014 return Ok(Some(vec![Node::Element(wrapper), script]));
@@ -2410,7 +2399,7 @@ impl Parser {
2410 let value = substitute_vars(value.trim(), &ctx.variables);
2399 let value = substitute_vars(value.trim(), &ctx.variables);
2411 ctx.translations
2400 ctx.translations
2412 .entry(current_locale.clone())
2401 .entry(current_locale.clone())
2413− .or_insert_with(HashMap::new)
2402+ .or_default()
2414 .insert(key, value);
2403 .insert(key, value);
@@ -2435,16 +2424,14 @@ impl Parser {
2435 if let Some(rest) = content.strip_prefix("@deprecated ") {
2424 if let Some(rest) = content.strip_prefix("@deprecated ") {
2436 let message = rest.trim().to_string();
2425 let message = rest.trim().to_string();
2437 // Peek at the next line to get the function name
2426 // Peek at the next line to get the function name
2438− if self.pos < self.lines.len() {
2427+ if self.pos < self.lines.len()
2439− if let LineContent::Normal(ref s) = self.lines[self.pos].content {
2428+ && let LineContent::Normal(ref s) = self.lines[self.pos].content
2440− if let Some(fn_rest) = s.trim().strip_prefix("@fn ") {
2429+ && let Some(fn_rest) = s.trim().strip_prefix("@fn ") {
2441 let parts: Vec<&str> = fn_rest.split_whitespace().collect();
2430 let parts: Vec<&str> = fn_rest.split_whitespace().collect();
2442 if let Some(&fn_name) = parts.first() {
2431 if let Some(&fn_name) = parts.first() {
2443 ctx.deprecated_fns.insert(fn_name.to_string(), message);
2432 ctx.deprecated_fns.insert(fn_name.to_string(), message);
2448 return Ok(None);
2435 return Ok(None);
@@ -2640,14 +2627,13 @@ impl Parser {
2640 let mut in_style = false;
2627 let mut in_style = false;
2641 let mut style_indent = 0;
2628 let mut style_indent = 0;
2642 while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
2629 while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
2643− if let LineContent::Normal(ref s) = self.lines[self.pos].content {
2630+ if let LineContent::Normal(ref s) = self.lines[self.pos].content
2644− if s.trim() == "@style" {
2631+ && s.trim() == "@style" {
2645 in_style = true;
2632 in_style = true;
2646 style_indent = self.lines[self.pos].indent;
2633 style_indent = self.lines[self.pos].indent;
2647 self.pos += 1;
2634 self.pos += 1;
2648 continue;
2635 continue;
2651 if in_style && self.lines[self.pos].indent > style_indent {
2637 if in_style && self.lines[self.pos].indent > style_indent {
2652 style_lines.push(self.lines[self.pos].clone());
2638 style_lines.push(self.lines[self.pos].clone());
2653 } else {
2639 } else {
@@ -2805,14 +2791,12 @@ impl Parser {
2805 let mut slot_contents: HashMap<String, Vec<Node>> = HashMap::new();
2791 let mut slot_contents: HashMap<String, Vec<Node>> = HashMap::new();
2806 let mut caller_children = Vec::new();
2792 let mut caller_children = Vec::new();
2807 for child in all_caller_children {
2793 for child in all_caller_children {
2808− if let Node::Element(ref elem) = child {
2794+ if let Node::Element(ref elem) = child
2809− if let ElementKind::Slot(ref slot_name) = elem.kind {
2795+ && let ElementKind::Slot(ref slot_name) = elem.kind
2810− if !slot_name.is_empty() {
2796+ && !slot_name.is_empty() {
2811 slot_contents.entry(slot_name.clone()).or_default().extend(elem.children.clone());
2797 slot_contents.entry(slot_name.clone()).or_default().extend(elem.children.clone());
2812 continue;
2798 continue;
2816 caller_children.push(child);
2800 caller_children.push(child);
@@ -2956,7 +2940,7 @@ fn replace_children_and_slots(
2957fn extract_element_name(content: &str) -> &str {
2941fn extract_element_name(content: &str) -> &str {
2958 let without_at = &content[1..];
2942 let without_at = &content[1..];
2959− match without_at.find(|c: char| c == ' ' || c == '[') {
2943+ match without_at.find([' ', '[']) {
2960 Some(i) => &without_at[..i],
2944 Some(i) => &without_at[..i],
2961 None => without_at,
2945 None => without_at,
@@ -2970,9 +2954,8 @@ fn parse_single_element(
2970 let (kind, rest) = if content.starts_with('[') {
2954 let (kind, rest) = if content.starts_with('[') {
2971 // Implicit @el
2955 // Implicit @el
2972 (ElementKind::El, content.to_string())
2956 (ElementKind::El, content.to_string())
2973− } else if content.starts_with('@') {
2974− let without_at = &content[1..];
2957+ } else if let Some(without_at) = content.strip_prefix('@') {
2975− match without_at.find(|c: char| c == ' ' || c == '[') {
2958+ match without_at.find([' ', '[']) {
2976 Some(i) => {
2959 Some(i) => {
2977 let kind_str = &without_at[..i];
2960 let kind_str = &without_at[..i];
2978 let rest = if without_at.as_bytes()[i] == b'[' {
2961 let rest = if without_at.as_bytes()[i] == b'[' {
@@ -3313,8 +3296,8 @@ fn check_undefined_vars(input: &str, vars: &HashMap<String, String>, line_num: u
3313 end -= 1;
3296 end -= 1;
3315 let name: String = chars[start..end].iter().collect();
3298 let name: String = chars[start..end].iter().collect();
3316− if !name.is_empty() && !vars.contains_key(&name) {
3299+ if !name.is_empty() && !vars.contains_key(&name)
3317− if let Some(closest) = suggest_var_name(&name, vars) {
3300+ && let Some(closest) = suggest_var_name(&name, vars) {
3318 warnings.push(Diagnostic {
3301 warnings.push(Diagnostic {
3319 line: line_num,
3302 line: line_num,
3320 column: Some(col),
3303 column: Some(col),
@@ -3323,7 +3306,6 @@ fn check_undefined_vars(input: &str, vars: &HashMap<String, String>, line_num: u
3323 source_line: Some(input.to_string()),
3306 source_line: Some(input.to_string()),
3327 i = end;
3309 i = end;
3328 } else {
3310 } else {
@@ -3777,8 +3759,7 @@ fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validat
3779 // ...$name spread — expand mixin or define attributes
3761 // ...$name spread — expand mixin or define attributes
3780− if part.starts_with("...$") {
3781− let name = &part[4..];
3762+ if let Some(name) = part.strip_prefix("...$") {
3782 if let Some(mixin_attrs) = ctx.mixins.get(name) {
3763 if let Some(mixin_attrs) = ctx.mixins.get(name) {
3783 ctx.used_mixins.insert(name.to_string());
3764 ctx.used_mixins.insert(name.to_string());
3784 attrs.extend(mixin_attrs.clone());
3765 attrs.extend(mixin_attrs.clone());
@@ -3792,8 +3773,7 @@ fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validat
3794 // $define reference — expand
3775 // $define reference — expand
3795− if part.starts_with('$') {
3796− let name = &part[1..];
3776+ if let Some(name) = part.strip_prefix('$') {
3797 if let Some(define_attrs) = ctx.defines.get(name) {
3777 if let Some(define_attrs) = ctx.defines.get(name) {
3798 ctx.used_defines.insert(name.to_string());
3778 ctx.used_defines.insert(name.to_string());
3799 attrs.extend(define_attrs.clone());
3779 attrs.extend(define_attrs.clone());
@@ -3877,9 +3857,9 @@ fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validat
3879 // Color validation for hex colors
3859 // Color validation for hex colors
3880− if matches!(strip_all_prefixes(&attr.key), "background" | "color") {
3860+ if matches!(strip_all_prefixes(&attr.key), "background" | "color")
3881− if let Some(ref val) = attr.value {
3861+ && let Some(ref val) = attr.value
3882− if val.starts_with('#') && !is_valid_hex_color(val) {
3862+ && val.starts_with('#') && !is_valid_hex_color(val) {
3883 ctx.diagnostics.push(Diagnostic {
3863 ctx.diagnostics.push(Diagnostic {
3884 line: line_num,
3864 line: line_num,
3885 column: None,
3865 column: None,
@@ -3888,8 +3868,6 @@ fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validat
3888 source_line: None,
3868 source_line: None,
3895 // Warn on unknown attributes
3873 // Warn on unknown attributes
@@ -4251,8 +4229,7 @@ fn strip_all_prefixes(key: &str) -> &str {
4251 .or_else(|| key.strip_prefix("invalid:"))
4229 .or_else(|| key.strip_prefix("invalid:"))
4252 .or_else(|| {
4230 .or_else(|| {
4253 // Handle nth:EXPR: prefix (e.g., nth:3:background -> background)
4231 // Handle nth:EXPR: prefix (e.g., nth:3:background -> background)
4254− if key.starts_with("nth:") {
4232+ if let Some(rest) = key.strip_prefix("nth:") {
4255− let rest = &key[4..];
4256 rest.find(':').map(|pos| &rest[pos + 1..])
4233 rest.find(':').map(|pos| &rest[pos + 1..])
4257 } else {
4234 } else {
@@ -4288,13 +4265,13 @@ fn strip_all_prefixes(key: &str) -> &str {
4288 .or_else(|| key.strip_prefix("portrait:"))
4265 .or_else(|| key.strip_prefix("portrait:"))
4289 .unwrap_or(key);
4266 .unwrap_or(key);
4290 // Strip container query prefixes (cq-sm:, cq-md:, etc.)
4267 // Strip container query prefixes (cq-sm:, cq-md:, etc.)
4291− let key = key.strip_prefix("cq-sm:")
4269+ (key.strip_prefix("cq-sm:")
4292 .or_else(|| key.strip_prefix("cq-md:"))
4270 .or_else(|| key.strip_prefix("cq-md:"))
4293 .or_else(|| key.strip_prefix("cq-lg:"))
4271 .or_else(|| key.strip_prefix("cq-lg:"))
4294 .or_else(|| key.strip_prefix("cq-xl:"))
4272 .or_else(|| key.strip_prefix("cq-xl:"))
4295 .or_else(|| key.strip_prefix("cq-2xl:"))
4273 .or_else(|| key.strip_prefix("cq-2xl:"))
4296− .unwrap_or(key);
4274+ .unwrap_or(key)) as _
4300/// Attributes that only make sense on container elements (@row, @column, @el).
4277/// Attributes that only make sense on container elements (@row, @column, @el).
@@ -4409,8 +4386,8 @@ fn validate_tree(
4409 if let Node::Element(elem) = node {
4386 if let Node::Element(elem) = node {
4410 for attr in &elem.attrs {
4387 for attr in &elem.attrs {
4411 let base = strip_all_prefixes(&attr.key);
4388 let base = strip_all_prefixes(&attr.key);
4412− if base == "width" && attr.value.as_deref() == Some("fill") {
4389+ if base == "width" && attr.value.as_deref() == Some("fill")
4413− if !matches!(parent_kind, Some(ElementKind::Row)) {
4390+ && !matches!(parent_kind, Some(ElementKind::Row)) {
4414 diagnostics.push(Diagnostic {
4391 diagnostics.push(Diagnostic {
4415 line: elem.line_num,
4392 line: elem.line_num,
4416 column: None,
4393 column: None,
@@ -4420,9 +4397,8 @@ fn validate_tree(
4420 source_line: None,
4397 source_line: None,
4424− if base == "height" && attr.value.as_deref() == Some("fill") {
4400+ if base == "height" && attr.value.as_deref() == Some("fill")
4425− if !matches!(parent_kind, Some(ElementKind::Column)) {
4401+ && !matches!(parent_kind, Some(ElementKind::Column)) {
4426 diagnostics.push(Diagnostic {
4402 diagnostics.push(Diagnostic {
4427 line: elem.line_num,
4403 line: elem.line_num,
4428 column: None,
4404 column: None,
@@ -4433,7 +4409,6 @@ fn validate_tree(
4433 source_line: None,
4409 source_line: None,
4438 // Container-only attributes on non-container elements
4413 // Container-only attributes on non-container elements
4439 if CONTAINER_ONLY_ATTRS.contains(&base) && !is_container(&elem.kind) {
4414 if CONTAINER_ONLY_ATTRS.contains(&base) && !is_container(&elem.kind) {
@@ -4526,8 +4501,8 @@ fn validate_tree(
4528 // Missing alt text on @image
4503 // Missing alt text on @image
4529− if matches!(elem.kind, ElementKind::Image) {
4504+ if matches!(elem.kind, ElementKind::Image)
4530− if !elem.attrs.iter().any(|a| a.key == "alt") {
4505+ && !elem.attrs.iter().any(|a| a.key == "alt") {
4531 diagnostics.push(Diagnostic {
4506 diagnostics.push(Diagnostic {
4532 line: elem.line_num,
4507 line: elem.line_num,
4533 column: None,
4508 column: None,
@@ -4536,9 +4511,8 @@ fn validate_tree(
4536 source_line: None,
4511 source_line: None,
4540− if matches!(elem.kind, ElementKind::Input) {
4514+ if matches!(elem.kind, ElementKind::Input)
4541− if !elem.attrs.iter().any(|a| a.key == "type") {
4515+ && !elem.attrs.iter().any(|a| a.key == "type") {
4542 diagnostics.push(Diagnostic {
4516 diagnostics.push(Diagnostic {
4543 line: elem.line_num,
4517 line: elem.line_num,
4544 column: None,
4518 column: None,
@@ -4547,7 +4521,6 @@ fn validate_tree(
4547 source_line: None,
4521 source_line: None,
4551 if matches!(elem.kind, ElementKind::Link) {
4524 if matches!(elem.kind, ElementKind::Link) {
4552 // For @link, argument is the URL, not text content
4525 // For @link, argument is the URL, not text content
4553 let has_text = !elem.children.is_empty();
4526 let has_text = !elem.children.is_empty();
@@ -4570,8 +4543,8 @@ fn validate_tree(
4570 let fg_color = elem.attrs.iter()
4543 let fg_color = elem.attrs.iter()
4571 .find(|a| strip_all_prefixes(&a.key) == "color")
4544 .find(|a| strip_all_prefixes(&a.key) == "color")
4572 .and_then(|a| a.value.as_deref());
4545 .and_then(|a| a.value.as_deref());
4573− if let (Some(bg), Some(fg)) = (bg_color, fg_color) {
4546+ if let (Some(bg), Some(fg)) = (bg_color, fg_color)
4574− if let (Some(bg_rgb), Some(fg_rgb)) = (parse_hex_rgb(bg), parse_hex_rgb(fg)) {
4547+ && let (Some(bg_rgb), Some(fg_rgb)) = (parse_hex_rgb(bg), parse_hex_rgb(fg)) {
4575 let ratio = contrast_ratio(bg_rgb, fg_rgb);
4548 let ratio = contrast_ratio(bg_rgb, fg_rgb);
4576 if ratio < 4.5 {
4549 if ratio < 4.5 {
4577 diagnostics.push(Diagnostic {
4550 diagnostics.push(Diagnostic {
@@ -4586,7 +4559,6 @@ fn validate_tree(
4592 // @form inputs should have associated @label
4564 // @form inputs should have associated @label
@@ -4610,8 +4582,8 @@ fn validate_tree(
4612 // @iframe should have title attribute
4584 // @iframe should have title attribute
4613− if matches!(elem.kind, ElementKind::Iframe) {
4585+ if matches!(elem.kind, ElementKind::Iframe)
4614− if !elem.attrs.iter().any(|a| a.key == "title") {
4586+ && !elem.attrs.iter().any(|a| a.key == "title") {
4615 diagnostics.push(Diagnostic {
4587 diagnostics.push(Diagnostic {
4616 line: elem.line_num,
4588 line: elem.line_num,
4617 column: None,
4589 column: None,
@@ -4620,7 +4592,6 @@ fn validate_tree(
4620 source_line: None,
4592 source_line: None,
4625 // @button should have accessible text
4596 // @button should have accessible text
4626 if matches!(elem.kind, ElementKind::Button) {
4597 if matches!(elem.kind, ElementKind::Button) {
@@ -4655,10 +4626,10 @@ fn validate_tree(
4657 // Tabindex > 0 is an anti-pattern
4628 // Tabindex > 0 is an anti-pattern
4658− if let Some(tabindex_attr) = elem.attrs.iter().find(|a| a.key == "tabindex") {
4629+ if let Some(tabindex_attr) = elem.attrs.iter().find(|a| a.key == "tabindex")
4659− if let Some(ref val) = tabindex_attr.value {
4630+ && let Some(ref val) = tabindex_attr.value
4660− if let Ok(n) = val.parse::<i32>() {
4631+ && let Ok(n) = val.parse::<i32>()
4661− if n > 0 {
4632+ && n > 0 {
4662 diagnostics.push(Diagnostic {
4633 diagnostics.push(Diagnostic {
4663 line: elem.line_num,
4634 line: elem.line_num,
4664 column: None,
4635 column: None,
@@ -4667,9 +4638,6 @@ fn validate_tree(
4667 source_line: None,
4638 source_line: None,
4674 validate_tree(&elem.children, Some(&elem.kind), diagnostics);
4642 validate_tree(&elem.children, Some(&elem.kind), diagnostics);
@@ -4875,11 +4843,10 @@ fn replace_extends_slots(
4876fn apply_filter(value: &str, filter: &str) -> String {
4844fn apply_filter(value: &str, filter: &str) -> String {
4877 if let Some(arg) = filter.strip_prefix("truncate:") {
4845 if let Some(arg) = filter.strip_prefix("truncate:") {
4878− if let Ok(n) = arg.parse::<usize>() {
4846+ if let Ok(n) = arg.parse::<usize>()
4879− if value.len() > n {
4847+ && value.len() > n {
4880 return format!("{}...", &value[..n]);
4848 return format!("{}...", &value[..n]);
4883 return value.to_string();
4850 return value.to_string();
4885 if let Some(rest) = filter.strip_prefix("replace:") {
4852 if let Some(rest) = filter.strip_prefix("replace:") {
@@ -4896,37 +4863,34 @@ fn apply_filter(value: &str, filter: &str) -> String {
4897 // Color functions: lighten:N, darken:N, alpha:N, mix:COLOR:N
4864 // Color functions: lighten:N, darken:N, alpha:N, mix:COLOR:N
4898 if let Some(arg) = filter.strip_prefix("lighten:") {
4865 if let Some(arg) = filter.strip_prefix("lighten:") {
4899− if let Ok(amount) = arg.parse::<f64>() {
4866+ if let Ok(amount) = arg.parse::<f64>()
4900− if let Some(rgb) = parse_hex_rgb(value) {
4867+ && let Some(rgb) = parse_hex_rgb(value) {
4901 let (r, g, b) = lighten_color(rgb, amount / 100.0);
4868 let (r, g, b) = lighten_color(rgb, amount / 100.0);
4902 return format!("#{:02x}{:02x}{:02x}", r, g, b);
4869 return format!("#{:02x}{:02x}{:02x}", r, g, b);
4905 return value.to_string();
4871 return value.to_string();
4907 if let Some(arg) = filter.strip_prefix("darken:") {
4873 if let Some(arg) = filter.strip_prefix("darken:") {
4908− if let Ok(amount) = arg.parse::<f64>() {
4874+ if let Ok(amount) = arg.parse::<f64>()
4909− if let Some(rgb) = parse_hex_rgb(value) {
4875+ && let Some(rgb) = parse_hex_rgb(value) {
4910 let (r, g, b) = darken_color(rgb, amount / 100.0);
4876 let (r, g, b) = darken_color(rgb, amount / 100.0);
4911 return format!("#{:02x}{:02x}{:02x}", r, g, b);
4877 return format!("#{:02x}{:02x}{:02x}", r, g, b);
4914 return value.to_string();
4879 return value.to_string();
4916 if let Some(arg) = filter.strip_prefix("alpha:") {
4881 if let Some(arg) = filter.strip_prefix("alpha:") {
4917− if let Ok(a) = arg.parse::<f64>() {
4882+ if let Ok(a) = arg.parse::<f64>()
4918− if let Some((r, g, b)) = parse_hex_rgb(value) {
4883+ && let Some((r, g, b)) = parse_hex_rgb(value) {
4919 let a8 = (a.clamp(0.0, 1.0) * 255.0) as u8;
4884 let a8 = (a.clamp(0.0, 1.0) * 255.0) as u8;
4920 return format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a8);
4885 return format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a8);
4923 return value.to_string();
4887 return value.to_string();
4925 if let Some(arg) = filter.strip_prefix("mix:") {
4889 if let Some(arg) = filter.strip_prefix("mix:") {
4926 // mix:COLOR:PERCENTAGE (e.g., mix:#ffffff:50)
4890 // mix:COLOR:PERCENTAGE (e.g., mix:#ffffff:50)
4927 let parts: Vec<&str> = arg.splitn(2, ':').collect();
4891 let parts: Vec<&str> = arg.splitn(2, ':').collect();
4928− if parts.len() == 2 {
4892+ if parts.len() == 2
4929− if let (Some(c1), Some(c2), Ok(pct)) = (
4893+ && let (Some(c1), Some(c2), Ok(pct)) = (
4930 parse_hex_rgb(value),
4894 parse_hex_rgb(value),
4931 parse_hex_rgb(parts[0]),
4895 parse_hex_rgb(parts[0]),
4932 parts[1].parse::<f64>(),
4896 parts[1].parse::<f64>(),
@@ -4934,7 +4898,6 @@ fn apply_filter(value: &str, filter: &str) -> String {
4934 let (r, g, b) = mix_colors(c1, c2, pct / 100.0);
4898 let (r, g, b) = mix_colors(c1, c2, pct / 100.0);
4935 return format!("#{:02x}{:02x}{:02x}", r, g, b);
4899 return format!("#{:02x}{:02x}{:02x}", r, g, b);
4938 return value.to_string();
4901 return value.to_string();
4940 match filter {
4903 match filter {
@@ -5309,7 +5272,7 @@ fn flatten_json(prefix: &str, value: &JsonValue, vars: &mut HashMap<String, Stri
5309 vars.insert(prefix.to_string(), csv.join(","));
5272 vars.insert(prefix.to_string(), csv.join(","));
5310 } else {
5273 } else {
5311 // Primitive array: comma-separated
5274 // Primitive array: comma-separated
5312− let csv: Vec<String> = items.iter().map(|v| json_value_to_string(v)).collect();
5275+ let csv: Vec<String> = items.iter().map(json_value_to_string).collect();
5313 vars.insert(prefix.to_string(), csv.join(","));
5276 vars.insert(prefix.to_string(), csv.join(","));
5315 // Also set indexed access: prefix.0, prefix.1, etc.
5278 // Also set indexed access: prefix.0, prefix.1, etc.
@@ -5390,14 +5353,13 @@ fn fetch_url_blocking(url: &str) -> Result<String, String> {
5390 let body = &response_str[body_start + 4..];
5353 let body = &response_str[body_start + 4..];
5392 // Check status code
5355 // Check status code
5393− if let Some(first_line) = headers.lines().next() {
5356+ if let Some(first_line) = headers.lines().next()
5394− if let Some(code_str) = first_line.split_whitespace().nth(1) {
5357+ && let Some(code_str) = first_line.split_whitespace().nth(1) {
5395 let code: u16 = code_str.parse().unwrap_or(0);
5358 let code: u16 = code_str.parse().unwrap_or(0);
5396 if code >= 400 {
5359 if code >= 400 {
5397 return Err(format!("HTTP {}", code));
5360 return Err(format!("HTTP {}", code));
5402 // Handle chunked transfer encoding
5364 // Handle chunked transfer encoding
5403 if headers.to_lowercase().contains("transfer-encoding: chunked") {
5365 if headers.to_lowercase().contains("transfer-encoding: chunked") {
@@ -5503,7 +5465,7 @@ fn markdown_to_html(lines: &[String]) -> String {
5503 let trimmed = line.trim();
5465 let trimmed = line.trim();
5505 // Fenced code blocks
5467 // Fenced code blocks
5506− if trimmed.starts_with("```") {
5468+ if let Some(after_fence) = trimmed.strip_prefix("```") {
5507 if in_code_block {
5469 if in_code_block {
5508 html.push_str("<pre><code");
5470 html.push_str("<pre><code");
5509 if !code_lang.is_empty() {
5471 if !code_lang.is_empty() {
@@ -5517,7 +5479,7 @@ fn markdown_to_html(lines: &[String]) -> String {
5517 in_code_block = false;
5479 in_code_block = false;
5518 } else {
5480 } else {
5519 flush_para(&mut para_buf, &mut html);
5481 flush_para(&mut para_buf, &mut html);
5520− code_lang = trimmed[3..].trim().to_string();
5482+ code_lang = after_fence.trim().to_string();
5521 in_code_block = true;
5483 in_code_block = true;
5523 continue;
5485 continue;
@@ -5565,15 +5527,14 @@ fn markdown_to_html(lines: &[String]) -> String {
5567 // Ordered list items
5529 // Ordered list items
5568− if let Some(dot_pos) = trimmed.find(". ") {
5530+ if let Some(dot_pos) = trimmed.find(". ")
5569− if dot_pos <= 3 && trimmed[..dot_pos].chars().all(|c| c.is_ascii_digit()) {
5531+ && dot_pos <= 3 && trimmed[..dot_pos].chars().all(|c| c.is_ascii_digit()) {
5570 flush_para(&mut para_buf, &mut html);
5532 flush_para(&mut para_buf, &mut html);
5571 if in_list { html.push_str("</ul>\n"); in_list = false; }
5533 if in_list { html.push_str("</ul>\n"); in_list = false; }
5572 // We use <ol> but just emit <li> for simplicity
5534 // We use <ol> but just emit <li> for simplicity
5573 html.push_str(&format!("<li>{}</li>\n", md_inline(&trimmed[dot_pos + 2..])));
5535 html.push_str(&format!("<li>{}</li>\n", md_inline(&trimmed[dot_pos + 2..])));
5574 continue;
5536 continue;
5578 // Horizontal rule
5539 // Horizontal rule
5579 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
5540 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
@@ -5584,9 +5545,9 @@ fn markdown_to_html(lines: &[String]) -> String {
5586 // Blockquote
5547 // Blockquote
5587− if trimmed.starts_with("> ") {
5548+ if let Some(quote_content) = trimmed.strip_prefix("> ") {
5588 flush_para(&mut para_buf, &mut html);
5549 flush_para(&mut para_buf, &mut html);
5589− html.push_str(&format!("<blockquote><p>{}</p></blockquote>\n", md_inline(&trimmed[2..])));
5550+ html.push_str(&format!("<blockquote><p>{}</p></blockquote>\n", md_inline(quote_content)));
5590 continue;
5551 continue;
@@ -5635,8 +5596,8 @@ fn md_inline(text: &str) -> String {
5637 // Inline code: `text`
5598 // Inline code: `text`
5638− if chars[i] == '`' {
5599+ if chars[i] == '`'
5639− if let Some(end) = chars[i+1..].iter().position(|&c| c == '`') {
5600+ && let Some(end) = chars[i+1..].iter().position(|&c| c == '`') {
5640 let inner: String = chars[i+1..i+1+end].iter().collect();
5601 let inner: String = chars[i+1..i+1+end].iter().collect();
5641 result.push_str("<code>");
5602 result.push_str("<code>");
5642 result.push_str(&html_escape_md(&inner));
5603 result.push_str(&html_escape_md(&inner));
@@ -5644,22 +5605,19 @@ fn md_inline(text: &str) -> String {
5644 i = i + 2 + end;
5605 i = i + 2 + end;
5645 continue;
5606 continue;
5648 // Links: [text](url)
5608 // Links: [text](url)
5649− if chars[i] == '[' {
5609+ if chars[i] == '['
5650− if let Some(close_bracket) = chars[i+1..].iter().position(|&c| c == ']') {
5610+ && let Some(close_bracket) = chars[i+1..].iter().position(|&c| c == ']') {
5651 let after = i + 1 + close_bracket + 1;
5611 let after = i + 1 + close_bracket + 1;
5652− if after < chars.len() && chars[after] == '(' {
5612+ if after < chars.len() && chars[after] == '('
5653− if let Some(close_paren) = chars[after+1..].iter().position(|&c| c == ')') {
5613+ && let Some(close_paren) = chars[after+1..].iter().position(|&c| c == ')') {
5654 let text: String = chars[i+1..i+1+close_bracket].iter().collect();
5614 let text: String = chars[i+1..i+1+close_bracket].iter().collect();
5655 let url: String = chars[after+1..after+1+close_paren].iter().collect();
5615 let url: String = chars[after+1..after+1+close_paren].iter().collect();
5656 result.push_str(&format!("<a href=\"{}\">{}</a>", html_escape_md(&url), md_inline(&text)));
5616 result.push_str(&format!("<a href=\"{}\">{}</a>", html_escape_md(&url), md_inline(&text)));
5657 i = after + 2 + close_paren;
5617 i = after + 2 + close_paren;
5658 continue;
5618 continue;
5663 result.push(chars[i]);
5621 result.push(chars[i]);
@@ -5679,12 +5637,7 @@ fn find_closing_double(chars: &[char], start: usize, marker: char) -> Option<usi
5681fn find_closing_single(chars: &[char], start: usize, marker: char) -> Option<usize> {
5639fn find_closing_single(chars: &[char], start: usize, marker: char) -> Option<usize> {
5682− for i in start..chars.len() {
5683− if chars[i] == marker {
5640+ (start..chars.len()).find(|&i| chars[i] == marker)
5690/// Simple glob matching supporting `*` as wildcard for any characters and `?` for a single character.
5643/// Simple glob matching supporting `*` as wildcard for any characters and `?` for a single character.