on master help sign in
Apply clippy fixes: collapsible if statements, strip_prefix, iterators, and type simplifications
commit503750d0648220e08b5899267eca54ed9d51cc2b authoriko <mail@iko.soy> dateThu 16 Apr 2026, 07:59 (6 months ago) parent96ba572
11 files changed+313−461
crates/htmlang-core/src/codegen.rs+38−59 crates/htmlang-core/src/parser.rs+90−137 src/bin/htmlang_lsp/analysis.rs+30−43 src/bin/htmlang_lsp/completion.rs+11−14 src/bin/htmlang_lsp/hover.rs+11−14 src/bin/htmlang_lsp/main.rs+7−10 src/bin/htmlang_lsp/navigation.rs+15−25 src/convert.rs+37−55 src/fmt.rs+5−7 src/main.rs+65−92 src/serve.rs+4−5
crates/htmlang-core/src/codegen.rs+38−59
@@ -71,6 +71,7 @@ impl StyleCollector {
7171 }
7272
7373 /// Returns a class name for this style combination, or None if all empty.
74+ #[allow(clippy::too_many_arguments)]
7475 fn get_class(
7576 &mut self,
7677 base: String,
@@ -1348,10 +1349,7 @@ fn generate_element(
13481349 match child {
13491350 Node::Text(segments) => {
13501351 for seg in segments {
1351− match seg {
1352− TextSegment::Plain(text) => out.push_str(text),
1353− _ => {}
1354− }
1352+ if let TextSegment::Plain(text) = seg { out.push_str(text) }
13551353 }
13561354 }
13571355 Node::Raw(content) => out.push_str(content),
@@ -1582,8 +1580,8 @@ fn generate_element(
15821580 out.push_str(" id=\"hl-main\"");
15831581 }
15841582
1585− if elem.kind == ElementKind::Link {
1586− if let Some(url) = &elem.argument {
1583+ if elem.kind == ElementKind::Link
1584+ && let Some(url) = &elem.argument {
15871585 out.push_str(" href=\"");
15881586 out.push_str(&html_escape(url));
15891587 out.push('"');
@@ -1600,54 +1598,47 @@ fn generate_element(
16001598 }
16011599 }
16021600 }
1603− }
16041601
16051602 // Video/Audio src
1606− if matches!(elem.kind, ElementKind::Video | ElementKind::Audio) {
1607− if let Some(src) = &elem.argument {
1603+ if matches!(elem.kind, ElementKind::Video | ElementKind::Audio)
1604+ && let Some(src) = &elem.argument {
16081605 out.push_str(" src=\"");
16091606 out.push_str(&html_escape(src));
16101607 out.push('"');
16111608 }
1612− }
16131609
16141610 // Form action
1615− if elem.kind == ElementKind::Form {
1616− if let Some(action) = &elem.argument {
1611+ if elem.kind == ElementKind::Form
1612+ && let Some(action) = &elem.argument {
16171613 out.push_str(" action=\"");
16181614 out.push_str(&html_escape(action));
16191615 out.push('"');
16201616 }
1621− }
16221617
16231618 // Iframe src
1624− if elem.kind == ElementKind::Iframe {
1625− if let Some(src) = &elem.argument {
1619+ if elem.kind == ElementKind::Iframe
1620+ && let Some(src) = &elem.argument {
16261621 out.push_str(" src=\"");
16271622 out.push_str(&html_escape(src));
16281623 out.push('"');
16291624 }
1630− }
16311625
16321626 // Tooltip title
1633− if elem.kind == ElementKind::Tooltip {
1634− if let Some(text) = &elem.argument {
1627+ if elem.kind == ElementKind::Tooltip
1628+ && let Some(text) = &elem.argument {
16351629 out.push_str(" title=\"");
16361630 out.push_str(&html_escape(text));
16371631 out.push('"');
16381632 }
1639− }
16401633
16411634 // Time datetime
1642− if elem.kind == ElementKind::Time {
1643− if let Some(dt) = elem.attrs.iter().find(|a| a.key == "datetime") {
1644− if let Some(val) = &dt.value {
1635+ if elem.kind == ElementKind::Time
1636+ && let Some(dt) = elem.attrs.iter().find(|a| a.key == "datetime")
1637+ && let Some(val) = &dt.value {
16451638 out.push_str(" datetime=\"");
16461639 out.push_str(&html_escape(val));
16471640 out.push('"');
16481641 }
1649− }
1650− }
16511642
16521643 // Critical CSS: inline styles directly instead of using a class
16531644 let is_critical = elem.attrs.iter().any(|a| a.key == "critical");
@@ -1659,7 +1650,7 @@ fn generate_element(
16591650 out.push('"');
16601651 }
16611652 // Still use class for non-base styles (pseudo, responsive, etc.)
1662− if gen_class.as_deref().map_or(false, |c| !c.is_empty()) {
1653+ if gen_class.as_deref().is_some_and(|c| !c.is_empty()) {
16631654 emit_class_attr(out, gen_class.as_deref(), user_class.as_deref());
16641655 } else if let Some(ref uc) = user_class {
16651656 emit_class_attr(out, None, Some(uc));
@@ -1710,11 +1701,10 @@ fn generate_element(
17101701 | ElementKind::Tooltip
17111702 | ElementKind::Chip
17121703 | ElementKind::Tag
1713− ) {
1714− if let Some(text) = &elem.argument {
1704+ )
1705+ && let Some(text) = &elem.argument {
17151706 out.push_str(&html_escape(text));
17161707 }
1717− }
17181708
17191709 // Children
17201710 ctx.depth += 1;
@@ -1809,10 +1799,10 @@ fn generate_self_closing(
18091799 // Image optimization: auto-add loading="lazy" and decoding="async"
18101800 if elem.kind == ElementKind::Image {
18111801 // SVG inlining: @image [inline] logo.svg
1812− if elem.attrs.iter().any(|a| a.key == "inline") {
1813− if let Some(src) = &elem.argument {
1814− if src.ends_with(".svg") {
1815− if let Ok(svg_content) = std::fs::read_to_string(src) {
1802+ if elem.attrs.iter().any(|a| a.key == "inline")
1803+ && let Some(src) = &elem.argument
1804+ && src.ends_with(".svg")
1805+ && let Ok(svg_content) = std::fs::read_to_string(src) {
18161806 // Close the tag we opened, then emit inline SVG instead
18171807 out.truncate(out.rfind('<').unwrap_or(0));
18181808 out.push_str(&ctx.indent());
@@ -1820,13 +1810,10 @@ fn generate_self_closing(
18201810 out.push_str(ctx.nl());
18211811 return;
18221812 }
1823− }
1824− }
1825− }
18261813 // Responsive srcset: @image photo.jpg [responsive 400 800 1200]
18271814 let responsive_attr = elem.attrs.iter().find(|a| a.key == "responsive");
1828− if let Some(resp) = responsive_attr {
1829− if let Some(ref sizes_str) = resp.value {
1815+ if let Some(resp) = responsive_attr
1816+ && let Some(ref sizes_str) = resp.value {
18301817 let widths: Vec<&str> = sizes_str.split_whitespace().collect();
18311818 if !widths.is_empty() {
18321819 let src = elem.argument.as_deref().unwrap_or("");
@@ -1849,15 +1836,14 @@ fn generate_self_closing(
18491836 }
18501837 }
18511838 }
1852− }
18531839
18541840 // Auto image dimensions: read local image file to inject width/height + aspect-ratio
18551841 let has_width = elem.attrs.iter().any(|a| a.key == "width");
18561842 let has_height = elem.attrs.iter().any(|a| a.key == "height");
1857− if !has_width || !has_height {
1858− if let Some(ref src) = elem.argument {
1859− if !src.starts_with("http://") && !src.starts_with("https://") && !src.starts_with("data:") {
1860− if let Some((w, h)) = read_image_dimensions(src) {
1843+ if (!has_width || !has_height)
1844+ && let Some(ref src) = elem.argument
1845+ && !src.starts_with("http://") && !src.starts_with("https://") && !src.starts_with("data:")
1846+ && let Some((w, h)) = read_image_dimensions(src) {
18611847 if !has_width {
18621848 out.push_str(&format!(" width=\"{}\"", w));
18631849 }
@@ -1869,9 +1855,6 @@ fn generate_self_closing(
18691855 out.push_str(&format!(" style=\"aspect-ratio:{}/{}\"", w, h));
18701856 }
18711857 }
1872− }
1873− }
1874− }
18751858
18761859 // Smart image loading: first 3 images get fetchpriority="high" (above the fold),
18771860 // subsequent images get loading="lazy" + decoding="async"
@@ -1965,19 +1948,18 @@ fn compute_class(
19651948 // Collect has(...): dynamic pseudo selectors
19661949 let mut has_prefixes: Vec<String> = Vec::new();
19671950 for attr in attrs {
1968− if attr.key.starts_with("has(") {
1969− if let Some(close) = attr.key.find("):") {
1951+ if attr.key.starts_with("has(")
1952+ && let Some(close) = attr.key.find("):") {
19701953 let prefix = format!("{}:", &attr.key[..close + 1]);
19711954 if !has_prefixes.contains(&prefix) {
19721955 has_prefixes.push(prefix);
19731956 }
19741957 }
1975− }
19761958 }
19771959 for prefix in &has_prefixes {
19781960 let inner = &prefix[4..prefix.len() - 2]; // extract selector from has(selector):
19791961 let selector = format!(":has({})", inner);
1980− let css = attrs_to_css(attrs, &prefix, kind, parent_kind);
1962+ let css = attrs_to_css(attrs, prefix, kind, parent_kind);
19811963 if !css.is_empty() {
19821964 pseudo.push((selector, css));
19831965 }
@@ -3143,7 +3125,7 @@ fn css_px(value: &str) -> String {
31433125fn css_px_multi(value: &str) -> String {
31443126 value
31453127 .split_whitespace()
3146− .map(|p| css_px(p))
3128+ .map(css_px)
31473129 .collect::<Vec<_>>()
31483130 .join(" ")
31493131}
@@ -3246,13 +3228,11 @@ fn read_svg_dimensions(text: &str) -> Option<(u32, u32)> {
32463228 let rest = &text[vb_start + 9..];
32473229 if let Some(end) = rest.find('"') {
32483230 let parts: Vec<&str> = rest[..end].split_whitespace().collect();
3249− if parts.len() == 4 {
3250− if let (Ok(w), Ok(h)) = (parts[2].parse::<f64>(), parts[3].parse::<f64>()) {
3251− if w > 0.0 && h > 0.0 {
3231+ if parts.len() == 4
3232+ && let (Ok(w), Ok(h)) = (parts[2].parse::<f64>(), parts[3].parse::<f64>())
3233+ && w > 0.0 && h > 0.0 {
32523234 return Some((w.round() as u32, h.round() as u32));
32533235 }
3254− }
3255− }
32563236 }
32573237 }
32583238 // Fall back to width/height attributes on <svg>
@@ -3293,11 +3273,10 @@ fn read_avif_dimensions(data: &[u8]) -> Option<(u32, u32)> {
32933273 // Recurse into container boxes (meta, iprp, ipco)
32943274 if matches!(box_type, b"meta" | b"iprp" | b"ipco" | b"moov" | b"trak" | b"mdia") {
32953275 let header_size = if box_type == b"meta" { 12 } else { 8 };
3296− if i + header_size < box_end {
3297− if let Some(dims) = read_avif_dimensions(&data[i + header_size..box_end]) {
3276+ if i + header_size < box_end
3277+ && let Some(dims) = read_avif_dimensions(&data[i + header_size..box_end]) {
32983278 return Some(dims);
32993279 }
3300− }
33013280 }
33023281 i = box_end;
33033282 }
@@ -3435,7 +3414,7 @@ fn collect_dns_prefetch(html: &str, dev: bool) -> String {
34353414 while let Some(pos) = rest.find("https://") {
34363415 let start = pos + 8; // skip "https://"
34373416 rest = &rest[start..];
3438− let end = rest.find(|c: char| c == '/' || c == '"' || c == '\'' || c == ' ' || c == '>' || c == ')').unwrap_or(rest.len());
3417+ let end = rest.find(['/', '"', '\'', ' ', '>', ')']).unwrap_or(rest.len());
34393418 let domain = &rest[..end];
34403419 if !domain.is_empty() && domain.contains('.') && !domains.contains(&domain.to_string()) {
34413420 domains.push(domain.to_string());
crates/htmlang-core/src/parser.rs+90−137
@@ -230,9 +230,9 @@ fn collect_images_recursive(nodes: &[Node], hints: &mut Vec<crate::ast::PreloadH
230230 for node in nodes {
231231 if hints.len() >= max { return; }
232232 if let Node::Element(elem) = node {
233− if elem.kind == ElementKind::Image {
234− if let Some(ref src) = elem.argument {
235− if !src.is_empty()
233+ if elem.kind == ElementKind::Image
234+ && let Some(ref src) = elem.argument
235+ && !src.is_empty()
236236 && !src.starts_with("data:")
237237 && !src.starts_with('#')
238238 {
@@ -242,8 +242,6 @@ fn collect_images_recursive(nodes: &[Node], hints: &mut Vec<crate::ast::PreloadH
242242 crossorigin: false,
243243 });
244244 }
245− }
246− }
247245 collect_images_recursive(&elem.children, hints, max);
248246 }
249247 }
@@ -270,11 +268,9 @@ fn preprocess(input: &str) -> Vec<Line> {
270268 let indent = line.len() - line.trim_start().len();
271269
272270 // Handle @raw """..."""
273− if trimmed.starts_with("@raw") {
274− let after_raw = trimmed[4..].trim_start();
275− if after_raw.starts_with("\"\"\"") {
276− let after_open = &after_raw[3..];
277−
271+ if let Some(raw_rest) = trimmed.strip_prefix("@raw") {
272+ let after_raw = raw_rest.trim_start();
273+ if let Some(after_open) = after_raw.strip_prefix("\"\"\"") {
278274 // Single-line: @raw """content"""
279275 if after_open.ends_with("\"\"\"") && after_open.len() >= 3 {
280276 let content = &after_open[..after_open.len() - 3];
@@ -443,8 +439,8 @@ impl Parser {
443439 if let Some((name, value)) = rest.split_once(' ') {
444440 let value = value.trim();
445441 // Support @let name = expr syntax (strip leading =)
446− let value = if value.starts_with("= ") {
447− value[2..].trim()
442+ let value = if let Some(after_eq) = value.strip_prefix("= ") {
443+ after_eq.trim()
448444 } else if value == "=" {
449445 ""
450446 } else {
@@ -453,8 +449,7 @@ impl Parser {
453449
454450 // Multi-line @let with triple quotes: @let name """..."""
455451 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("\"\"\"") {
458453 if after_open.ends_with("\"\"\"") && after_open.len() >= 3 {
459454 // Single-line triple-quote: @let name """value"""
460455 value_str = after_open[..after_open.len()-3].to_string();
@@ -1024,11 +1019,10 @@ impl Parser {
10241019 .cloned()
10251020 .collect();
10261021 for name in new_var_keys {
1027− if let Some(val) = ctx.variables.remove(&name) {
1028− if !name.starts_with("__") {
1022+ if let Some(val) = ctx.variables.remove(&name)
1023+ && !name.starts_with("__") {
10291024 ctx.variables.insert(format!("{}.{}", prefix, name), val);
10301025 }
1031− }
10321026 }
10331027 } else {
10341028 // Parse the file but discard DOM nodes — only keep definitions
@@ -1063,8 +1057,8 @@ impl Parser {
10631057 (rest, None)
10641058 };
10651059 let env_val = std::env::var(var_name).ok().or_else(|| default_val.map(|d| {
1066− let d = substitute_vars(d, &ctx.variables);
1067− d
1060+
1061+ substitute_vars(d, &ctx.variables)
10681062 }));
10691063 match env_val {
10701064 Some(val) => {
@@ -1357,10 +1351,10 @@ impl Parser {
13571351 if let Some(rest) = content.strip_prefix("@use ") {
13581352 let rest = rest.trim();
13591353 // @use "./file.hl" fn1, fn2, define1
1360− let (filename, names_str) = if rest.starts_with('"') {
1361− if let Some(end_quote) = rest[1..].find('"') {
1362− let filename = &rest[1..end_quote + 1];
1363− let names = rest[end_quote + 2..].trim();
1354+ let (filename, names_str) = if let Some(after_quote) = rest.strip_prefix('"') {
1355+ if let Some(end_quote) = after_quote.find('"') {
1356+ let filename = &after_quote[..end_quote];
1357+ let names = after_quote[end_quote + 1..].trim();
13641358 (filename.to_string(), names.to_string())
13651359 } else {
13661360 return Err(ParseError {
@@ -1529,14 +1523,13 @@ impl Parser {
15291523 ctx.variables.insert(var_name.clone(), stems.join(" "));
15301524 // Also parse each JSON file and store keys as prefix_stem_key
15311525 for (stem, text) in &items {
1532− if let Some(json) = parse_json(text.trim()) {
1533− if let JsonValue::Object(ref obj) = json {
1526+ if let Some(json) = parse_json(text.trim())
1527+ && let JsonValue::Object(ref obj) = json {
15341528 for (key, val) in obj {
15351529 let var_key = format!("{}_{}", stem, key);
15361530 ctx.variables.insert(var_key, json_value_to_string(val));
15371531 }
15381532 }
1539− }
15401533 }
15411534
15421535 return Ok(None);
@@ -1734,7 +1727,7 @@ impl Parser {
17341727 .and_then(|v| v.parse().ok())
17351728 .unwrap_or(1);
17361729 let total_items = items.len();
1737− let total_pages = (total_items + size - 1) / size;
1730+ let total_pages = total_items.div_ceil(size);
17381731 let start = (current_page - 1) * size;
17391732 let end = (start + size).min(total_items);
17401733 let page_items: Vec<String> = if start < total_items {
@@ -1757,17 +1750,15 @@ impl Parser {
17571750
17581751 // Check for @else block (empty-state fallback)
17591752 let mut else_lines = Vec::new();
1760− 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 {
1762− if s.trim() == "@else" {
1753+ if self.pos < self.lines.len() && self.lines[self.pos].indent == current_indent
1754+ && let LineContent::Normal(ref s) = self.lines[self.pos].content
1755+ && s.trim() == "@else" {
17631756 self.pos += 1; // consume @else
17641757 while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
17651758 else_lines.push(self.lines[self.pos].clone());
17661759 self.pos += 1;
17671760 }
17681761 }
1769− }
1770− }
17711762
17721763 if body_lines.is_empty() {
17731764 return Ok(None);
@@ -1817,7 +1808,7 @@ impl Parser {
18171808 }
18181809
18191810 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(' ')));
18211812
18221813 for (i, item) in items.iter().enumerate() {
18231814 // Always expose $_index for the current iteration
@@ -2018,9 +2009,7 @@ impl Parser {
20182009 };
20192010
20202011 // 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>"
2023− ));
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());
20242013
20252014 return Ok(Some(vec![Node::Element(wrapper), script]));
20262015 }
@@ -2410,7 +2399,7 @@ impl Parser {
24102399 let value = substitute_vars(value.trim(), &ctx.variables);
24112400 ctx.translations
24122401 .entry(current_locale.clone())
2413− .or_insert_with(HashMap::new)
2402+ .or_default()
24142403 .insert(key, value);
24152404 }
24162405 }
@@ -2435,16 +2424,14 @@ impl Parser {
24352424 if let Some(rest) = content.strip_prefix("@deprecated ") {
24362425 let message = rest.trim().to_string();
24372426 // Peek at the next line to get the function name
2438− if self.pos < self.lines.len() {
2439− if let LineContent::Normal(ref s) = self.lines[self.pos].content {
2440− if let Some(fn_rest) = s.trim().strip_prefix("@fn ") {
2427+ if self.pos < self.lines.len()
2428+ && let LineContent::Normal(ref s) = self.lines[self.pos].content
2429+ && let Some(fn_rest) = s.trim().strip_prefix("@fn ") {
24412430 let parts: Vec<&str> = fn_rest.split_whitespace().collect();
24422431 if let Some(&fn_name) = parts.first() {
24432432 ctx.deprecated_fns.insert(fn_name.to_string(), message);
24442433 }
24452434 }
2446− }
2447− }
24482435 return Ok(None);
24492436 }
24502437
@@ -2640,14 +2627,13 @@ impl Parser {
26402627 let mut in_style = false;
26412628 let mut style_indent = 0;
26422629 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 {
2644− if s.trim() == "@style" {
2630+ if let LineContent::Normal(ref s) = self.lines[self.pos].content
2631+ && s.trim() == "@style" {
26452632 in_style = true;
26462633 style_indent = self.lines[self.pos].indent;
26472634 self.pos += 1;
26482635 continue;
26492636 }
2650− }
26512637 if in_style && self.lines[self.pos].indent > style_indent {
26522638 style_lines.push(self.lines[self.pos].clone());
26532639 } else {
@@ -2805,14 +2791,12 @@ impl Parser {
28052791 let mut slot_contents: HashMap<String, Vec<Node>> = HashMap::new();
28062792 let mut caller_children = Vec::new();
28072793 for child in all_caller_children {
2808− if let Node::Element(ref elem) = child {
2809− if let ElementKind::Slot(ref slot_name) = elem.kind {
2810− if !slot_name.is_empty() {
2794+ if let Node::Element(ref elem) = child
2795+ && let ElementKind::Slot(ref slot_name) = elem.kind
2796+ && !slot_name.is_empty() {
28112797 slot_contents.entry(slot_name.clone()).or_default().extend(elem.children.clone());
28122798 continue;
28132799 }
2814− }
2815− }
28162800 caller_children.push(child);
28172801 }
28182802
@@ -2956,7 +2940,7 @@ fn replace_children_and_slots(
29562940
29572941fn extract_element_name(content: &str) -> &str {
29582942 let without_at = &content[1..];
2959− match without_at.find(|c: char| c == ' ' || c == '[') {
2943+ match without_at.find([' ', '[']) {
29602944 Some(i) => &without_at[..i],
29612945 None => without_at,
29622946 }
@@ -2970,9 +2954,8 @@ fn parse_single_element(
29702954 let (kind, rest) = if content.starts_with('[') {
29712955 // Implicit @el
29722956 (ElementKind::El, content.to_string())
2973− } else if content.starts_with('@') {
2974− let without_at = &content[1..];
2975− match without_at.find(|c: char| c == ' ' || c == '[') {
2957+ } else if let Some(without_at) = content.strip_prefix('@') {
2958+ match without_at.find([' ', '[']) {
29762959 Some(i) => {
29772960 let kind_str = &without_at[..i];
29782961 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
33133296 end -= 1;
33143297 }
33153298 let name: String = chars[start..end].iter().collect();
3316− if !name.is_empty() && !vars.contains_key(&name) {
3317− if let Some(closest) = suggest_var_name(&name, vars) {
3299+ if !name.is_empty() && !vars.contains_key(&name)
3300+ && let Some(closest) = suggest_var_name(&name, vars) {
33183301 warnings.push(Diagnostic {
33193302 line: line_num,
33203303 column: Some(col),
@@ -3323,7 +3306,6 @@ fn check_undefined_vars(input: &str, vars: &HashMap<String, String>, line_num: u
33233306 source_line: Some(input.to_string()),
33243307 });
33253308 }
3326− }
33273309 i = end;
33283310 } else {
33293311 i += 1;
@@ -3777,8 +3759,7 @@ fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validat
37773759 }
37783760
37793761 // ...$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("...$") {
37823763 if let Some(mixin_attrs) = ctx.mixins.get(name) {
37833764 ctx.used_mixins.insert(name.to_string());
37843765 attrs.extend(mixin_attrs.clone());
@@ -3792,8 +3773,7 @@ fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validat
37923773 }
37933774
37943775 // $define reference — expand
3795− if part.starts_with('$') {
3796− let name = &part[1..];
3776+ if let Some(name) = part.strip_prefix('$') {
37973777 if let Some(define_attrs) = ctx.defines.get(name) {
37983778 ctx.used_defines.insert(name.to_string());
37993779 attrs.extend(define_attrs.clone());
@@ -3877,9 +3857,9 @@ fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validat
38773857 }
38783858
38793859 // Color validation for hex colors
3880− if matches!(strip_all_prefixes(&attr.key), "background" | "color") {
3881− if let Some(ref val) = attr.value {
3882− if val.starts_with('#') && !is_valid_hex_color(val) {
3860+ if matches!(strip_all_prefixes(&attr.key), "background" | "color")
3861+ && let Some(ref val) = attr.value
3862+ && val.starts_with('#') && !is_valid_hex_color(val) {
38833863 ctx.diagnostics.push(Diagnostic {
38843864 line: line_num,
38853865 column: None,
@@ -3888,8 +3868,6 @@ fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validat
38883868 source_line: None,
38893869 });
38903870 }
3891− }
3892− }
38933871 }
38943872
38953873 // Warn on unknown attributes
@@ -4251,8 +4229,7 @@ fn strip_all_prefixes(key: &str) -> &str {
42514229 .or_else(|| key.strip_prefix("invalid:"))
42524230 .or_else(|| {
42534231 // Handle nth:EXPR: prefix (e.g., nth:3:background -> background)
4254− if key.starts_with("nth:") {
4255− let rest = &key[4..];
4232+ if let Some(rest) = key.strip_prefix("nth:") {
42564233 rest.find(':').map(|pos| &rest[pos + 1..])
42574234 } else {
42584235 None
@@ -4288,13 +4265,13 @@ fn strip_all_prefixes(key: &str) -> &str {
42884265 .or_else(|| key.strip_prefix("portrait:"))
42894266 .unwrap_or(key);
42904267 // Strip container query prefixes (cq-sm:, cq-md:, etc.)
4291− let key = key.strip_prefix("cq-sm:")
4268+
4269+ (key.strip_prefix("cq-sm:")
42924270 .or_else(|| key.strip_prefix("cq-md:"))
42934271 .or_else(|| key.strip_prefix("cq-lg:"))
42944272 .or_else(|| key.strip_prefix("cq-xl:"))
42954273 .or_else(|| key.strip_prefix("cq-2xl:"))
4296− .unwrap_or(key);
4297− key
4274+ .unwrap_or(key)) as _
42984275}
42994276
43004277/// Attributes that only make sense on container elements (@row, @column, @el).
@@ -4409,8 +4386,8 @@ fn validate_tree(
44094386 if let Node::Element(elem) = node {
44104387 for attr in &elem.attrs {
44114388 let base = strip_all_prefixes(&attr.key);
4412− if base == "width" && attr.value.as_deref() == Some("fill") {
4413− if !matches!(parent_kind, Some(ElementKind::Row)) {
4389+ if base == "width" && attr.value.as_deref() == Some("fill")
4390+ && !matches!(parent_kind, Some(ElementKind::Row)) {
44144391 diagnostics.push(Diagnostic {
44154392 line: elem.line_num,
44164393 column: None,
@@ -4420,9 +4397,8 @@ fn validate_tree(
44204397 source_line: None,
44214398 });
44224399 }
4423− }
4424− if base == "height" && attr.value.as_deref() == Some("fill") {
4425− if !matches!(parent_kind, Some(ElementKind::Column)) {
4400+ if base == "height" && attr.value.as_deref() == Some("fill")
4401+ && !matches!(parent_kind, Some(ElementKind::Column)) {
44264402 diagnostics.push(Diagnostic {
44274403 line: elem.line_num,
44284404 column: None,
@@ -4433,7 +4409,6 @@ fn validate_tree(
44334409 source_line: None,
44344410 });
44354411 }
4436− }
44374412
44384413 // Container-only attributes on non-container elements
44394414 if CONTAINER_ONLY_ATTRS.contains(&base) && !is_container(&elem.kind) {
@@ -4526,8 +4501,8 @@ fn validate_tree(
45264501 }
45274502 }
45284503 // Missing alt text on @image
4529− if matches!(elem.kind, ElementKind::Image) {
4530− if !elem.attrs.iter().any(|a| a.key == "alt") {
4504+ if matches!(elem.kind, ElementKind::Image)
4505+ && !elem.attrs.iter().any(|a| a.key == "alt") {
45314506 diagnostics.push(Diagnostic {
45324507 line: elem.line_num,
45334508 column: None,
@@ -4536,9 +4511,8 @@ fn validate_tree(
45364511 source_line: None,
45374512 });
45384513 }
4539− }
4540− if matches!(elem.kind, ElementKind::Input) {
4541− if !elem.attrs.iter().any(|a| a.key == "type") {
4514+ if matches!(elem.kind, ElementKind::Input)
4515+ && !elem.attrs.iter().any(|a| a.key == "type") {
45424516 diagnostics.push(Diagnostic {
45434517 line: elem.line_num,
45444518 column: None,
@@ -4547,7 +4521,6 @@ fn validate_tree(
45474521 source_line: None,
45484522 });
45494523 }
4550− }
45514524 if matches!(elem.kind, ElementKind::Link) {
45524525 // For @link, argument is the URL, not text content
45534526 let has_text = !elem.children.is_empty();
@@ -4570,8 +4543,8 @@ fn validate_tree(
45704543 let fg_color = elem.attrs.iter()
45714544 .find(|a| strip_all_prefixes(&a.key) == "color")
45724545 .and_then(|a| a.value.as_deref());
4573− 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)) {
4546+ if let (Some(bg), Some(fg)) = (bg_color, fg_color)
4547+ && let (Some(bg_rgb), Some(fg_rgb)) = (parse_hex_rgb(bg), parse_hex_rgb(fg)) {
45754548 let ratio = contrast_ratio(bg_rgb, fg_rgb);
45764549 if ratio < 4.5 {
45774550 diagnostics.push(Diagnostic {
@@ -4586,7 +4559,6 @@ fn validate_tree(
45864559 });
45874560 }
45884561 }
4589− }
45904562 }
45914563
45924564 // @form inputs should have associated @label
@@ -4610,8 +4582,8 @@ fn validate_tree(
46104582 }
46114583
46124584 // @iframe should have title attribute
4613− if matches!(elem.kind, ElementKind::Iframe) {
4614− if !elem.attrs.iter().any(|a| a.key == "title") {
4585+ if matches!(elem.kind, ElementKind::Iframe)
4586+ && !elem.attrs.iter().any(|a| a.key == "title") {
46154587 diagnostics.push(Diagnostic {
46164588 line: elem.line_num,
46174589 column: None,
@@ -4620,7 +4592,6 @@ fn validate_tree(
46204592 source_line: None,
46214593 });
46224594 }
4623− }
46244595
46254596 // @button should have accessible text
46264597 if matches!(elem.kind, ElementKind::Button) {
@@ -4655,10 +4626,10 @@ fn validate_tree(
46554626 }
46564627
46574628 // Tabindex > 0 is an anti-pattern
4658− if let Some(tabindex_attr) = elem.attrs.iter().find(|a| a.key == "tabindex") {
4659− if let Some(ref val) = tabindex_attr.value {
4660− if let Ok(n) = val.parse::<i32>() {
4661− if n > 0 {
4629+ if let Some(tabindex_attr) = elem.attrs.iter().find(|a| a.key == "tabindex")
4630+ && let Some(ref val) = tabindex_attr.value
4631+ && let Ok(n) = val.parse::<i32>()
4632+ && n > 0 {
46624633 diagnostics.push(Diagnostic {
46634634 line: elem.line_num,
46644635 column: None,
@@ -4667,9 +4638,6 @@ fn validate_tree(
46674638 source_line: None,
46684639 });
46694640 }
4670− }
4671− }
4672− }
46734641
46744642 validate_tree(&elem.children, Some(&elem.kind), diagnostics);
46754643 }
@@ -4875,11 +4843,10 @@ fn replace_extends_slots(
48754843
48764844fn apply_filter(value: &str, filter: &str) -> String {
48774845 if let Some(arg) = filter.strip_prefix("truncate:") {
4878− if let Ok(n) = arg.parse::<usize>() {
4879− if value.len() > n {
4846+ if let Ok(n) = arg.parse::<usize>()
4847+ && value.len() > n {
48804848 return format!("{}...", &value[..n]);
48814849 }
4882− }
48834850 return value.to_string();
48844851 }
48854852 if let Some(rest) = filter.strip_prefix("replace:") {
@@ -4896,37 +4863,34 @@ fn apply_filter(value: &str, filter: &str) -> String {
48964863 }
48974864 // Color functions: lighten:N, darken:N, alpha:N, mix:COLOR:N
48984865 if let Some(arg) = filter.strip_prefix("lighten:") {
4899− if let Ok(amount) = arg.parse::<f64>() {
4900− if let Some(rgb) = parse_hex_rgb(value) {
4866+ if let Ok(amount) = arg.parse::<f64>()
4867+ && let Some(rgb) = parse_hex_rgb(value) {
49014868 let (r, g, b) = lighten_color(rgb, amount / 100.0);
49024869 return format!("#{:02x}{:02x}{:02x}", r, g, b);
49034870 }
4904− }
49054871 return value.to_string();
49064872 }
49074873 if let Some(arg) = filter.strip_prefix("darken:") {
4908− if let Ok(amount) = arg.parse::<f64>() {
4909− if let Some(rgb) = parse_hex_rgb(value) {
4874+ if let Ok(amount) = arg.parse::<f64>()
4875+ && let Some(rgb) = parse_hex_rgb(value) {
49104876 let (r, g, b) = darken_color(rgb, amount / 100.0);
49114877 return format!("#{:02x}{:02x}{:02x}", r, g, b);
49124878 }
4913− }
49144879 return value.to_string();
49154880 }
49164881 if let Some(arg) = filter.strip_prefix("alpha:") {
4917− if let Ok(a) = arg.parse::<f64>() {
4918− if let Some((r, g, b)) = parse_hex_rgb(value) {
4882+ if let Ok(a) = arg.parse::<f64>()
4883+ && let Some((r, g, b)) = parse_hex_rgb(value) {
49194884 let a8 = (a.clamp(0.0, 1.0) * 255.0) as u8;
49204885 return format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a8);
49214886 }
4922− }
49234887 return value.to_string();
49244888 }
49254889 if let Some(arg) = filter.strip_prefix("mix:") {
49264890 // mix:COLOR:PERCENTAGE (e.g., mix:#ffffff:50)
49274891 let parts: Vec<&str> = arg.splitn(2, ':').collect();
4928− if parts.len() == 2 {
4929− if let (Some(c1), Some(c2), Ok(pct)) = (
4892+ if parts.len() == 2
4893+ && let (Some(c1), Some(c2), Ok(pct)) = (
49304894 parse_hex_rgb(value),
49314895 parse_hex_rgb(parts[0]),
49324896 parts[1].parse::<f64>(),
@@ -4934,7 +4898,6 @@ fn apply_filter(value: &str, filter: &str) -> String {
49344898 let (r, g, b) = mix_colors(c1, c2, pct / 100.0);
49354899 return format!("#{:02x}{:02x}{:02x}", r, g, b);
49364900 }
4937− }
49384901 return value.to_string();
49394902 }
49404903 match filter {
@@ -5309,7 +5272,7 @@ fn flatten_json(prefix: &str, value: &JsonValue, vars: &mut HashMap<String, Stri
53095272 vars.insert(prefix.to_string(), csv.join(","));
53105273 } else {
53115274 // 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();
53135276 vars.insert(prefix.to_string(), csv.join(","));
53145277 }
53155278 // Also set indexed access: prefix.0, prefix.1, etc.
@@ -5390,14 +5353,13 @@ fn fetch_url_blocking(url: &str) -> Result<String, String> {
53905353 let body = &response_str[body_start + 4..];
53915354
53925355 // Check status code
5393− if let Some(first_line) = headers.lines().next() {
5394− if let Some(code_str) = first_line.split_whitespace().nth(1) {
5356+ if let Some(first_line) = headers.lines().next()
5357+ && let Some(code_str) = first_line.split_whitespace().nth(1) {
53955358 let code: u16 = code_str.parse().unwrap_or(0);
53965359 if code >= 400 {
53975360 return Err(format!("HTTP {}", code));
53985361 }
53995362 }
5400− }
54015363
54025364 // Handle chunked transfer encoding
54035365 if headers.to_lowercase().contains("transfer-encoding: chunked") {
@@ -5503,7 +5465,7 @@ fn markdown_to_html(lines: &[String]) -> String {
55035465 let trimmed = line.trim();
55045466
55055467 // Fenced code blocks
5506− if trimmed.starts_with("```") {
5468+ if let Some(after_fence) = trimmed.strip_prefix("```") {
55075469 if in_code_block {
55085470 html.push_str("<pre><code");
55095471 if !code_lang.is_empty() {
@@ -5517,7 +5479,7 @@ fn markdown_to_html(lines: &[String]) -> String {
55175479 in_code_block = false;
55185480 } else {
55195481 flush_para(&mut para_buf, &mut html);
5520− code_lang = trimmed[3..].trim().to_string();
5482+ code_lang = after_fence.trim().to_string();
55215483 in_code_block = true;
55225484 }
55235485 continue;
@@ -5565,15 +5527,14 @@ fn markdown_to_html(lines: &[String]) -> String {
55655527 }
55665528
55675529 // Ordered list items
5568− if let Some(dot_pos) = trimmed.find(". ") {
5569− if dot_pos <= 3 && trimmed[..dot_pos].chars().all(|c| c.is_ascii_digit()) {
5530+ if let Some(dot_pos) = trimmed.find(". ")
5531+ && dot_pos <= 3 && trimmed[..dot_pos].chars().all(|c| c.is_ascii_digit()) {
55705532 flush_para(&mut para_buf, &mut html);
55715533 if in_list { html.push_str("</ul>\n"); in_list = false; }
55725534 // We use <ol> but just emit <li> for simplicity
55735535 html.push_str(&format!("<li>{}</li>\n", md_inline(&trimmed[dot_pos + 2..])));
55745536 continue;
55755537 }
5576− }
55775538
55785539 // Horizontal rule
55795540 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
@@ -5584,9 +5545,9 @@ fn markdown_to_html(lines: &[String]) -> String {
55845545 }
55855546
55865547 // Blockquote
5587− if trimmed.starts_with("> ") {
5548+ if let Some(quote_content) = trimmed.strip_prefix("> ") {
55885549 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)));
55905551 continue;
55915552 }
55925553
@@ -5635,8 +5596,8 @@ fn md_inline(text: &str) -> String {
56355596 }
56365597 }
56375598 // Inline code: `text`
5638− if chars[i] == '`' {
5639− if let Some(end) = chars[i+1..].iter().position(|&c| c == '`') {
5599+ if chars[i] == '`'
5600+ && let Some(end) = chars[i+1..].iter().position(|&c| c == '`') {
56405601 let inner: String = chars[i+1..i+1+end].iter().collect();
56415602 result.push_str("<code>");
56425603 result.push_str(&html_escape_md(&inner));
@@ -5644,22 +5605,19 @@ fn md_inline(text: &str) -> String {
56445605 i = i + 2 + end;
56455606 continue;
56465607 }
5647− }
56485608 // Links: [text](url)
5649− if chars[i] == '[' {
5650− if let Some(close_bracket) = chars[i+1..].iter().position(|&c| c == ']') {
5609+ if chars[i] == '['
5610+ && let Some(close_bracket) = chars[i+1..].iter().position(|&c| c == ']') {
56515611 let after = i + 1 + close_bracket + 1;
5652− if after < chars.len() && chars[after] == '(' {
5653− if let Some(close_paren) = chars[after+1..].iter().position(|&c| c == ')') {
5612+ if after < chars.len() && chars[after] == '('
5613+ && let Some(close_paren) = chars[after+1..].iter().position(|&c| c == ')') {
56545614 let text: String = chars[i+1..i+1+close_bracket].iter().collect();
56555615 let url: String = chars[after+1..after+1+close_paren].iter().collect();
56565616 result.push_str(&format!("<a href=\"{}\">{}</a>", html_escape_md(&url), md_inline(&text)));
56575617 i = after + 2 + close_paren;
56585618 continue;
56595619 }
5660− }
56615620 }
5662− }
56635621 result.push(chars[i]);
56645622 i += 1;
56655623 }
@@ -5679,12 +5637,7 @@ fn find_closing_double(chars: &[char], start: usize, marker: char) -> Option<usi
56795637}
56805638
56815639fn find_closing_single(chars: &[char], start: usize, marker: char) -> Option<usize> {
5682− for i in start..chars.len() {
5683− if chars[i] == marker {
5684− return Some(i);
5685− }
5686− }
5687− None
5640+ (start..chars.len()).find(|&i| chars[i] == marker)
56885641}
56895642
56905643/// Simple glob matching supporting `*` as wildcard for any characters and `?` for a single character.
src/bin/htmlang_lsp/analysis.rs+30−43
@@ -37,8 +37,8 @@ pub(crate) fn document_symbols(text: &str) -> Vec<SymbolInformation> {
3737 }
3838
3939 // @let definitions
40− if let Some(rest) = trimmed.strip_prefix("@let ") {
41− if let Some((name, value)) = rest.trim().split_once(' ') {
40+ if let Some(rest) = trimmed.strip_prefix("@let ")
41+ && let Some((name, value)) = rest.trim().split_once(' ') {
4242 symbols.push(SymbolInformation {
4343 name: format!("${}", name),
4444 kind: SymbolKind::VARIABLE,
@@ -51,11 +51,10 @@ pub(crate) fn document_symbols(text: &str) -> Vec<SymbolInformation> {
5151 container_name: Some(format!("= {}", value.trim())),
5252 });
5353 }
54− }
5554
5655 // @define definitions
57− if let Some(rest) = trimmed.strip_prefix("@define ") {
58− if let Some(bracket) = rest.find('[') {
56+ if let Some(rest) = trimmed.strip_prefix("@define ")
57+ && let Some(bracket) = rest.find('[') {
5958 let name = rest[..bracket].trim();
6059 symbols.push(SymbolInformation {
6160 name: format!("${}", name),
@@ -69,7 +68,6 @@ pub(crate) fn document_symbols(text: &str) -> Vec<SymbolInformation> {
6968 container_name: Some("attribute bundle".to_string()),
7069 });
7170 }
72− }
7371
7472 // @keyframes definitions
7573 if let Some(rest) = trimmed.strip_prefix("@keyframes ") {
@@ -169,7 +167,7 @@ pub(crate) fn code_actions(
169167 let trimmed = source_line.trim_start();
170168 if trimmed.starts_with("@let ") {
171169 let var_name = trimmed.strip_prefix("@let ")
172− .and_then(|r| r.trim().split_whitespace().next())
170+ .and_then(|r| r.split_whitespace().next())
173171 .unwrap_or("?");
174172 let edit = TextEdit {
175173 range: Range::new(
@@ -396,8 +394,8 @@ pub(crate) fn code_actions(
396394 }),
397395 ..Default::default()
398396 }));
399− } else if source_line.contains("@input") {
400− if let Some(pos) = source_line.find("@input") {
397+ } else if source_line.contains("@input")
398+ && let Some(pos) = source_line.find("@input") {
401399 let after = pos + "@input".len();
402400 let edit = TextEdit {
403401 range: Range::new(
@@ -419,7 +417,6 @@ pub(crate) fn code_actions(
419417 ..Default::default()
420418 }));
421419 }
422− }
423420 }
424421 }
425422
@@ -436,19 +433,19 @@ pub(crate) fn code_actions(
436433
437434 // Quick-fix: auto-import suggestion for unknown element @name
438435 // Searches current directory and subdirectories for @fn definitions
439− if msg.contains("unknown element @") {
440− if let Some(fn_name) = extract_between(msg, "unknown element @", ",")
436+ if msg.contains("unknown element @")
437+ && let Some(fn_name) = extract_between(msg, "unknown element @", ",")
441438 .or_else(|| extract_between(msg, "unknown element @", ""))
442439 {
443440 let fn_name = fn_name.trim();
444− if !fn_name.is_empty() {
445− if let Ok(file_path) = uri.to_file_path() {
446− if let Some(dir) = file_path.parent() {
441+ if !fn_name.is_empty()
442+ && let Ok(file_path) = uri.to_file_path()
443+ && let Some(dir) = file_path.parent() {
447444 // Search current dir and subdirs for .hl files defining this function
448445 let mut search_dirs = vec![dir.to_path_buf()];
449446 // Also search parent dir's subdirs (for project-wide imports)
450− if let Some(parent) = dir.parent() {
451− if let Ok(entries) = std::fs::read_dir(parent) {
447+ if let Some(parent) = dir.parent()
448+ && let Ok(entries) = std::fs::read_dir(parent) {
452449 for entry in entries.flatten() {
453450 let p = entry.path();
454451 if p.is_dir() && p != dir {
@@ -456,7 +453,6 @@ pub(crate) fn code_actions(
456453 }
457454 }
458455 }
459− }
460456 for search_dir in &search_dirs {
461457 if let Ok(entries) = std::fs::read_dir(search_dir) {
462458 for entry in entries.flatten() {
@@ -539,10 +535,7 @@ pub(crate) fn code_actions(
539535 }
540536 }
541537 }
542− }
543− }
544538 }
545− }
546539 }
547540
548541 // Refactoring: extract selection to @fn
@@ -612,8 +605,8 @@ pub(crate) fn code_actions(
612605 let line_idx = selection.start.line as usize;
613606 if line_idx < lines.len() {
614607 let line = lines[line_idx];
615− if let Some(bracket_start) = line.find('[') {
616− if let Some(bracket_end) = line[bracket_start..].find(']') {
608+ if let Some(bracket_start) = line.find('[')
609+ && let Some(bracket_end) = line[bracket_start..].find(']') {
617610 let attrs_str = &line[bracket_start + 1..bracket_start + bracket_end];
618611 // Only offer if there are at least 2 attributes
619612 let attr_count = attrs_str.split(',').count();
@@ -656,7 +649,6 @@ pub(crate) fn code_actions(
656649 }));
657650 }
658651 }
659− }
660652 }
661653 }
662654
@@ -709,8 +701,8 @@ pub(crate) fn find_colors(text: &str) -> Vec<ColorInformation> {
709701 .unwrap_or(line.len());
710702 let hex = &line[hex_start..hex_end];
711703 let len = hex.len();
712− if len == 3 || len == 6 || len == 8 {
713− if let Some((r, g, b, a)) = parse_hex_color(hex) {
704+ if (len == 3 || len == 6 || len == 8)
705+ && let Some((r, g, b, a)) = parse_hex_color(hex) {
714706 colors.push(ColorInformation {
715707 range: Range::new(
716708 Position::new(line_idx as u32, abs_pos as u32),
@@ -724,7 +716,6 @@ pub(crate) fn find_colors(text: &str) -> Vec<ColorInformation> {
724716 },
725717 });
726718 }
727− }
728719 start = hex_end;
729720 }
730721 // Detect named CSS colors
@@ -911,38 +902,34 @@ pub(crate) fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
911902 let result = htmlang::parser::parse(text);
912903 let mut unused_vars: std::collections::HashSet<String> = std::collections::HashSet::new();
913904 for d in &result.diagnostics {
914− if d.message.contains("unused variable '$") {
915− if let Some(start) = d.message.find("'$") {
905+ if d.message.contains("unused variable '$")
906+ && let Some(start) = d.message.find("'$") {
916907 let rest = &d.message[start + 2..];
917908 if let Some(end) = rest.find('\'') {
918909 unused_vars.insert(rest[..end].to_string());
919910 }
920911 }
921− }
922− if d.message.contains("unused function '@") {
923− if let Some(start) = d.message.find("'@") {
912+ if d.message.contains("unused function '@")
913+ && let Some(start) = d.message.find("'@") {
924914 let rest = &d.message[start + 2..];
925915 if let Some(end) = rest.find('\'') {
926916 unused_vars.insert(format!("@{}", &rest[..end]));
927917 }
928918 }
929− }
930− if d.message.contains("unused define '$") {
931− if let Some(start) = d.message.find("'$") {
919+ if d.message.contains("unused define '$")
920+ && let Some(start) = d.message.find("'$") {
932921 let rest = &d.message[start + 2..];
933922 if let Some(end) = rest.find('\'') {
934923 unused_vars.insert(rest[..end].to_string());
935924 }
936925 }
937− }
938− if d.message.contains("unused mixin '") {
939− if let Some(start) = d.message.find("unused mixin '") {
926+ if d.message.contains("unused mixin '")
927+ && let Some(start) = d.message.find("unused mixin '") {
940928 let rest = &d.message[start + 14..];
941929 if let Some(end) = rest.find('\'') {
942930 unused_vars.insert(rest[..end].to_string());
943931 }
944932 }
945− }
946933 }
947934
948935 for (line_idx, line) in text.lines().enumerate() {
@@ -1023,6 +1010,7 @@ fn is_builtin_element(word: &str) -> bool {
10231010 )
10241011}
10251012
1013+#[allow(clippy::too_many_arguments)]
10261014fn push_token(
10271015 tokens: &mut Vec<SemanticToken>,
10281016 prev_line: &mut u32,
@@ -1059,11 +1047,10 @@ pub(crate) fn inlay_hints(text: &str) -> Vec<InlayHint> {
10591047 let mut vars: HashMap<&str, &str> = HashMap::new();
10601048 for line in text.lines() {
10611049 let trimmed = line.trim();
1062− if let Some(rest) = trimmed.strip_prefix("@let ") {
1063− if let Some((name, value)) = rest.trim().split_once(' ') {
1050+ if let Some(rest) = trimmed.strip_prefix("@let ")
1051+ && let Some((name, value)) = rest.trim().split_once(' ') {
10641052 vars.insert(name, value.trim());
10651053 }
1066− }
10671054 }
10681055
10691056 let mut hints = Vec::new();
@@ -1168,7 +1155,7 @@ pub(crate) fn get_signature_help(text: &str, position: Position) -> Option<Signa
11681155 };
11691156 ParameterInformation {
11701157 label: ParameterLabel::Simple(label),
1171− documentation: doc.map(|d| Documentation::String(d)),
1158+ documentation: doc.map(Documentation::String),
11721159 }
11731160 }).collect();
11741161
src/bin/htmlang_lsp/completion.rs+11−14
@@ -386,7 +386,7 @@ pub(crate) fn path_completions(uri: &Url, position: Position) -> Vec<CompletionI
386386 None => return vec![],
387387 };
388388
389− let col = position.character as u32;
389+ let col = position.character;
390390 let edit_range = Range::new(Position::new(position.line, col), Position::new(position.line, col));
391391
392392 let entries = match std::fs::read_dir(dir) {
@@ -397,8 +397,8 @@ pub(crate) fn path_completions(uri: &Url, position: Position) -> Vec<CompletionI
397397 let mut items = Vec::new();
398398 for entry in entries.flatten() {
399399 let path = entry.path();
400− if path.extension().and_then(|e| e.to_str()) == Some("hl") {
401− if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
400+ if path.extension().and_then(|e| e.to_str()) == Some("hl")
401+ && let Some(name) = path.file_name().and_then(|n| n.to_str()) {
402402 // Skip the current file itself
403403 if Some(name) == file_path.file_name().and_then(|n| n.to_str()) {
404404 continue;
@@ -414,7 +414,6 @@ pub(crate) fn path_completions(uri: &Url, position: Position) -> Vec<CompletionI
414414 ..Default::default()
415415 });
416416 }
417− }
418417 }
419418 items
420419}
@@ -432,9 +431,9 @@ pub(crate) fn use_symbol_completions(uri: &Url, line: &str, position: Position)
432431
433432 // Extract the filename from @use "file.hl" or @use file.hl
434433 let after_use = &line.trim_start()[5..]; // skip "@use "
435− let filename = if after_use.starts_with('"') {
436− let end = after_use[1..].find('"').map(|i| i + 1).unwrap_or(after_use.len());
437− &after_use[1..end]
434+ let filename = if let Some(after_quote) = after_use.strip_prefix('"') {
435+ let end = after_quote.find('"').unwrap_or(after_quote.len());
436+ &after_quote[..end]
438437 } else {
439438 after_use.split_whitespace().next().unwrap_or("")
440439 };
@@ -449,7 +448,7 @@ pub(crate) fn use_symbol_completions(uri: &Url, line: &str, position: Position)
449448 Err(_) => return vec![],
450449 };
451450
452− let col = position.character as u32;
451+ let col = position.character;
453452 let edit_range = Range::new(Position::new(position.line, col), Position::new(position.line, col));
454453
455454 let mut items = Vec::new();
@@ -489,8 +488,8 @@ pub(crate) fn use_symbol_completions(uri: &Url, line: &str, position: Position)
489488 ..Default::default()
490489 });
491490 }
492− } else if let Some(rest) = trimmed.strip_prefix("@component ") {
493− if let Some(name) = rest.split_whitespace().next() {
491+ } else if let Some(rest) = trimmed.strip_prefix("@component ")
492+ && let Some(name) = rest.split_whitespace().next() {
494493 items.push(CompletionItem {
495494 label: name.to_string(),
496495 kind: Some(CompletionItemKind::CLASS),
@@ -502,7 +501,6 @@ pub(crate) fn use_symbol_completions(uri: &Url, line: &str, position: Position)
502501 ..Default::default()
503502 });
504503 }
505− }
506504 }
507505
508506 items
@@ -986,8 +984,8 @@ fn variable_completions(text: &str, range: Range) -> Vec<CompletionItem> {
986984 range,
987985 ));
988986 }
989− } else if let Some(rest) = trimmed.strip_prefix("@define ") {
990− if let Some(bracket) = rest.find('[') {
987+ } else if let Some(rest) = trimmed.strip_prefix("@define ")
988+ && let Some(bracket) = rest.find('[') {
991989 let name = rest[..bracket].trim();
992990 items.push(item(
993991 &format!("${}", name),
@@ -997,7 +995,6 @@ fn variable_completions(text: &str, range: Range) -> Vec<CompletionItem> {
997995 range,
998996 ));
999997 }
1000− }
1001998 }
1002999
10031000 items
src/bin/htmlang_lsp/hover.rs+11−14
@@ -6,10 +6,10 @@ pub(crate) fn hover_at(text: &str, position: Position) -> Option<Hover> {
66 let col = (position.character as usize).min(line.len());
77 let word = word_at(line, col)?;
88
9− let doc = if word.starts_with('$') {
10− hover_variable(text, &word[1..])
11− } else if word.starts_with('@') {
12− hover_user_fn(text, &word[1..]).or_else(|| hover_builtin(&word))
9+ let doc = if let Some(var_name) = word.strip_prefix('$') {
10+ hover_variable(text, var_name)
11+ } else if let Some(fn_name) = word.strip_prefix('@') {
12+ hover_user_fn(text, fn_name).or_else(|| hover_builtin(&word))
1313 } else {
1414 hover_builtin(&word)
1515 }?;
@@ -46,27 +46,24 @@ pub(crate) fn is_word_byte(c: u8) -> bool {
4646fn hover_variable(text: &str, name: &str) -> Option<String> {
4747 for line in text.lines() {
4848 let trimmed = line.trim();
49− if let Some(rest) = trimmed.strip_prefix("@let ") {
50− if let Some((n, v)) = rest.trim().split_once(' ') {
51− if n == name {
49+ if let Some(rest) = trimmed.strip_prefix("@let ")
50+ && let Some((n, v)) = rest.trim().split_once(' ')
51+ && n == name {
5252 return Some(format!("**${}** = `{}`", name, v.trim()));
5353 }
54− }
55− }
5654 }
5755
5856 for line in text.lines() {
5957 let trimmed = line.trim();
6058 if let Some(rest) = trimmed.strip_prefix("@define ") {
6159 let rest = rest.trim();
62− if let Some(bracket) = rest.find('[') {
63− if rest[..bracket].trim() == name {
60+ if let Some(bracket) = rest.find('[')
61+ && rest[..bracket].trim() == name {
6462 return Some(format!(
6563 "**${}** \u{2014} Attribute bundle\n\n`{}`",
6664 name, trimmed
6765 ));
6866 }
69− }
7067 }
7168 }
7269
@@ -108,8 +105,8 @@ fn hover_user_fn(text: &str, name: &str) -> Option<String> {
108105 let prev = lines[j].trim();
109106 if let Some(comment) = prev.strip_prefix("-- ") {
110107 doc_lines.push(comment);
111− } else if prev.starts_with("--") {
112− doc_lines.push(&prev[2..]);
108+ } else if let Some(comment) = prev.strip_prefix("--") {
109+ doc_lines.push(comment);
113110 } else {
114111 break;
115112 }
src/bin/htmlang_lsp/main.rs+7−10
@@ -203,8 +203,7 @@ impl LanguageServer for Backend {
203203 }
204204 }
205205 // @use "file.hl" fn1, fn2 -- after the filename, suggest exported @fn names
206− if trimmed.starts_with("@use ") {
207− let after_use = &trimmed[5..];
206+ if let Some(after_use) = trimmed.strip_prefix("@use ") {
208207 // If we already have a filename (quoted or unquoted), suggest symbols from that file
209208 let has_file = after_use.contains(".hl");
210209 if has_file {
@@ -462,20 +461,19 @@ impl LanguageServer for Backend {
462461 }
463462 let path = entry.path();
464463 // Skip hidden / vendor / build directories.
465− if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
466− if name.starts_with('.')
464+ if let Some(name) = path.file_name().and_then(|n| n.to_str())
465+ && (name.starts_with('.')
467466 || name == "target"
468467 || name == "node_modules"
469− || name == "dist"
468+ || name == "dist")
470469 {
471470 continue;
472471 }
473− }
474472 if path.is_dir() {
475473 stack.push(path);
476474 continue;
477475 }
478− if path.extension().map_or(false, |e| e == "hl")
476+ if path.extension().is_some_and(|e| e == "hl")
479477 && !covered_files.contains(&path)
480478 {
481479 scanned += 1;
@@ -691,14 +689,13 @@ impl LanguageServer for Backend {
691689 if let Some((n, _)) = rest.trim().split_once(' ') {
692690 defs.push(Def { line: i as u32, name: n.to_string(), kind: "let" });
693691 }
694− } else if let Some(rest) = trimmed.strip_prefix("@define ") {
695− if let Some(bracket) = rest.find('[') {
692+ } else if let Some(rest) = trimmed.strip_prefix("@define ")
693+ && let Some(bracket) = rest.find('[') {
696694 let n = rest[..bracket].trim();
697695 if !n.is_empty() {
698696 defs.push(Def { line: i as u32, name: n.to_string(), kind: "define" });
699697 }
700698 }
701− }
702699 }
703700
704701 let mut lenses = Vec::with_capacity(defs.len());
src/bin/htmlang_lsp/navigation.rs+15−25
@@ -37,11 +37,9 @@ pub(crate) fn definition_at(text: &str, position: Position, uri: &Url) -> Option
3737 let word = word_at(line, col)?;
3838
3939 // Find definition location
40− let (def_line, def_col, def_len) = if word.starts_with('$') {
41− let name = &word[1..];
40+ let (def_line, def_col, def_len) = if let Some(name) = word.strip_prefix('$') {
4241 find_definition(text, name)?
43− } else if word.starts_with('@') {
44− let name = &word[1..];
42+ } else if let Some(name) = word.strip_prefix('@') {
4543 find_fn_definition(text, name)?
4644 } else {
4745 return None;
@@ -62,14 +60,12 @@ pub(crate) fn find_definition(text: &str, name: &str) -> Option<(u32, u32, u32)>
6260 let trimmed = line.trim();
6361 let offset = (line.len() - trimmed.len()) as u32;
6462
65− if let Some(rest) = trimmed.strip_prefix("@let ") {
66− if let Some((n, _)) = rest.trim().split_once(' ') {
67− if n == name {
63+ if let Some(rest) = trimmed.strip_prefix("@let ")
64+ && let Some((n, _)) = rest.trim().split_once(' ')
65+ && n == name {
6866 let col = offset + "@let ".len() as u32;
6967 return Some((i as u32, col, n.len() as u32));
7068 }
71− }
72− }
7369
7470 if let Some(rest) = trimmed.strip_prefix("@define ") {
7571 let rest = rest.trim();
@@ -179,10 +175,10 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
179175
180176 if is_var {
181177 // Rename @let definition
182− if let Some(rest) = trimmed.strip_prefix("@let ") {
183− if let Some((n, _)) = rest.trim().split_once(' ') {
184− if n == name {
185− if let Some(pos) = line.find(n) {
178+ if let Some(rest) = trimmed.strip_prefix("@let ")
179+ && let Some((n, _)) = rest.trim().split_once(' ')
180+ && n == name
181+ && let Some(pos) = line.find(n) {
186182 edits.push(TextEdit {
187183 range: Range::new(
188184 Position::new(line_num, pos as u32),
@@ -191,17 +187,14 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
191187 new_text: new_base.to_string(),
192188 });
193189 }
194− }
195− }
196− }
197190
198191 // Rename @define definition
199192 if let Some(rest) = trimmed.strip_prefix("@define ") {
200193 let rest_trimmed = rest.trim();
201194 if let Some(bracket) = rest_trimmed.find('[') {
202195 let n = rest_trimmed[..bracket].trim();
203− if n == name {
204− if let Some(pos) = line.find(n) {
196+ if n == name
197+ && let Some(pos) = line.find(n) {
205198 edits.push(TextEdit {
206199 range: Range::new(
207200 Position::new(line_num, pos as u32),
@@ -211,7 +204,6 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
211204 });
212205 continue; // Don't also match $ references on this line
213206 }
214− }
215207 }
216208 }
217209
@@ -220,8 +212,8 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
220212 let parts: Vec<&str> = rest.split_whitespace().collect();
221213 for param in &parts[1..] {
222214 let p = param.strip_prefix('$').unwrap_or(param);
223− if p == name {
224− if let Some(pos) = line.find(param) {
215+ if p == name
216+ && let Some(pos) = line.find(param) {
225217 let prefix = if param.starts_with('$') { "$" } else { "" };
226218 edits.push(TextEdit {
227219 range: Range::new(
@@ -231,7 +223,6 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
231223 new_text: format!("{}{}", prefix, new_base),
232224 });
233225 }
234− }
235226 }
236227 }
237228
@@ -262,8 +253,8 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
262253 // Rename @fn definition
263254 if let Some(rest) = trimmed.strip_prefix("@fn ") {
264255 let parts: Vec<&str> = rest.split_whitespace().collect();
265− if parts.first() == Some(&name) {
266− if let Some(pos) = line.find(&format!("@fn {}", name)) {
256+ if parts.first() == Some(&name)
257+ && let Some(pos) = line.find(&format!("@fn {}", name)) {
267258 let start = pos + 4; // skip "@fn "
268259 edits.push(TextEdit {
269260 range: Range::new(
@@ -273,7 +264,6 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
273264 new_text: new_base.to_string(),
274265 });
275266 }
276− }
277267 }
278268
279269 // Rename @name function calls
src/convert.rs+37−55
@@ -86,8 +86,8 @@ impl<'a> HtmlParser<'a> {
8686 break;
8787 }
8888 // Check for closing tag
89− if let Some(stop) = stop_tag {
90− if self.starts_with("</") {
89+ if let Some(stop) = stop_tag
90+ && self.starts_with("</") {
9191 let saved = self.pos;
9292 self.pos += 2;
9393 let tag = self.parse_tag_name();
@@ -102,7 +102,6 @@ impl<'a> HtmlParser<'a> {
102102 // parent handle it.
103103 break;
104104 }
105− }
106105
107106 if self.starts_with("<!--") {
108107 self.pos += 4;
@@ -356,7 +355,10 @@ fn strip_px_multi(v: &str) -> String {
356355/// Converts CSS properties to htmlang attribute key-value pairs.
357356/// Returns (attrs, leftover_css) where leftover_css is anything we
358357/// could not convert to a known htmlang attribute.
359−fn css_to_hl_attrs(styles: &[(String, String)]) -> (Vec<(String, Option<String>)>, Vec<(String, String)>) {
358+type HlAttr = (String, Option<String>);
359+type CssDecl = (String, String);
360+
361+fn css_to_hl_attrs(styles: &[CssDecl]) -> (Vec<HlAttr>, Vec<CssDecl>) {
360362 let mut attrs: Vec<(String, Option<String>)> = Vec::new();
361363 let mut leftover: Vec<(String, String)> = Vec::new();
362364
@@ -387,36 +389,30 @@ fn css_to_hl_attrs(styles: &[(String, String)]) -> (Vec<(String, Option<String>)
387389 }
388390 "padding-left" | "padding-right" => {
389391 // Check for padding-x pattern
390− if key == "padding-left" {
391− if let Some(&pr) = style_map.get("padding-right") {
392− if pr == val {
392+ if key == "padding-left"
393+ && let Some(&pr) = style_map.get("padding-right")
394+ && pr == val {
393395 attrs.push(("padding-x".into(), Some(strip_px(val).into())));
394396 continue;
395397 }
396− }
397− }
398− if key == "padding-right" {
399− if style_map.get("padding-left") == Some(&val.as_str()) {
398+ if key == "padding-right"
399+ && style_map.get("padding-left") == Some(&val.as_str()) {
400400 // Already emitted as padding-x
401401 continue;
402402 }
403− }
404403 leftover.push((key.clone(), val.clone()));
405404 }
406405 "padding-top" | "padding-bottom" => {
407− if key == "padding-top" {
408− if let Some(&pb) = style_map.get("padding-bottom") {
409− if pb == val {
406+ if key == "padding-top"
407+ && let Some(&pb) = style_map.get("padding-bottom")
408+ && pb == val {
410409 attrs.push(("padding-y".into(), Some(strip_px(val).into())));
411410 continue;
412411 }
413− }
414− }
415− if key == "padding-bottom" {
416− if style_map.get("padding-top") == Some(&val.as_str()) {
412+ if key == "padding-bottom"
413+ && style_map.get("padding-top") == Some(&val.as_str()) {
417414 continue;
418415 }
419− }
420416 leftover.push((key.clone(), val.clone()));
421417 }
422418 "padding-inline" => {
@@ -430,35 +426,29 @@ fn css_to_hl_attrs(styles: &[(String, String)]) -> (Vec<(String, Option<String>)
430426 attrs.push(("margin".into(), Some(strip_px_multi(val))));
431427 }
432428 "margin-left" | "margin-right" => {
433− if key == "margin-left" {
434− if let Some(&mr) = style_map.get("margin-right") {
435− if mr == val {
429+ if key == "margin-left"
430+ && let Some(&mr) = style_map.get("margin-right")
431+ && mr == val {
436432 attrs.push(("margin-x".into(), Some(strip_px(val).into())));
437433 continue;
438434 }
439− }
440− }
441− if key == "margin-right" {
442− if style_map.get("margin-left") == Some(&val.as_str()) {
435+ if key == "margin-right"
436+ && style_map.get("margin-left") == Some(&val.as_str()) {
443437 continue;
444438 }
445− }
446439 leftover.push((key.clone(), val.clone()));
447440 }
448441 "margin-top" | "margin-bottom" => {
449− if key == "margin-top" {
450− if let Some(&mb) = style_map.get("margin-bottom") {
451− if mb == val {
442+ if key == "margin-top"
443+ && let Some(&mb) = style_map.get("margin-bottom")
444+ && mb == val {
452445 attrs.push(("margin-y".into(), Some(strip_px(val).into())));
453446 continue;
454447 }
455− }
456− }
457− if key == "margin-bottom" {
458− if style_map.get("margin-top") == Some(&val.as_str()) {
448+ if key == "margin-bottom"
449+ && style_map.get("margin-top") == Some(&val.as_str()) {
459450 continue;
460451 }
461− }
462452 leftover.push((key.clone(), val.clone()));
463453 }
464454 "margin-inline" => {
@@ -677,16 +667,13 @@ fn unwrap_boilerplate(nodes: &[HtmlNode]) -> Vec<&HtmlNode> {
677667 .collect();
678668
679669 // If there's a single <html> element, unwrap it
680− if meaningful.len() == 1 {
681− if let HtmlNode::Element {
670+ if meaningful.len() == 1
671+ && let HtmlNode::Element {
682672 tag, children, ..
683673 } = meaningful[0]
684− {
685− if tag == "html" {
674+ && tag == "html" {
686675 return unwrap_html_children(children);
687676 }
688− }
689− }
690677
691678 // Otherwise, check if there are any html/head/body tags mixed in
692679 let mut result = Vec::new();
@@ -950,13 +937,11 @@ fn emit_element(
950937 }
951938
952939 // Alt text for images
953− if tag == "img" {
954− if let Some(alt_text) = &alt {
955− if !alt_text.is_empty() {
940+ if tag == "img"
941+ && let Some(alt_text) = &alt
942+ && !alt_text.is_empty() {
956943 hl_attrs.push(("alt".into(), Some(alt_text.clone())));
957944 }
958− }
959− }
960945
961946 // Passthrough HTML attributes that htmlang supports
962947 for (key, val) in attrs {
@@ -996,12 +981,11 @@ fn emit_element(
996981 out.push(' ');
997982 out.push_str(url);
998983 }
999− } else if tag == "img" {
1000− if let Some(src_val) = &src {
984+ } else if tag == "img"
985+ && let Some(src_val) = &src {
1001986 out.push(' ');
1002987 out.push_str(src_val);
1003988 }
1004− }
1005989
1006990 // Attributes block
1007991 if !hl_attrs.is_empty() {
@@ -1041,17 +1025,15 @@ fn emit_element(
10411025 }
10421026
10431027 // Alt text for images
1044− if tag == "img" {
1045− if let Some(alt_text) = &alt {
1046− if !alt_text.is_empty() {
1028+ if tag == "img"
1029+ && let Some(alt_text) = &alt
1030+ && !alt_text.is_empty() {
10471031 // Alt is emitted as an attribute already? No -- let's add it.
10481032 // Actually we should add it to hl_attrs above. But since we
10491033 // already flushed the line, add a comment.
10501034 // Re-check: the image alt is typically an attribute.
10511035 // We'll add it inline. Let's fix this by checking above.
10521036 }
1053− }
1054− }
10551037
10561038 // Emit children
10571039 for child in children {
src/fmt.rs+5−7
@@ -92,7 +92,7 @@ fn sort_attrs(attrs_str: &str) -> String {
9292 let mut parts: Vec<&str> = attrs_str.split(',').collect();
9393 // Preserve order within same category (stable sort)
9494 parts.sort_by_key(|part| {
95− let key = part.trim().split_whitespace().next().unwrap_or("");
95+ let key = part.split_whitespace().next().unwrap_or("");
9696 attr_category(key)
9797 });
9898 parts.iter()
@@ -224,13 +224,12 @@ pub fn format(input: &str) -> String {
224224 || (bracket_was_wrapped
225225 && attrs_count > 1
226226 && indented_len > WRAP_MIN_WIDTH);
227− if should_wrap {
228− if let Some(wrapped) = wrap_attrs(&formatted, level) {
227+ if should_wrap
228+ && let Some(wrapped) = wrap_attrs(&formatted, level) {
229229 output.push_str(&wrapped);
230230 bracket_was_wrapped = false;
231231 continue;
232232 }
233− }
234233 output.push_str(&" ".repeat(level));
235234 output.push_str(&formatted);
236235 if let Some(cmt) = pending_comment.take() {
@@ -275,8 +274,8 @@ pub fn format(input: &str) -> String {
275274
276275 let indented_len = level * 2 + formatted.len();
277276 // Single-line emission — only wrap when we strictly exceed the ceiling.
278− if indented_len > MAX_LINE_WIDTH && formatted.contains('[') {
279− if let Some(mut wrapped) = wrap_attrs(&formatted, level) {
277+ if indented_len > MAX_LINE_WIDTH && formatted.contains('[')
278+ && let Some(mut wrapped) = wrap_attrs(&formatted, level) {
280279 if let Some(cmt) = &trailing {
281280 // Reattach comment on the final `]` line.
282281 if wrapped.ends_with('\n') {
@@ -290,7 +289,6 @@ pub fn format(input: &str) -> String {
290289 indent_stack.push(raw_indent);
291290 continue;
292291 }
293− }
294292 let _ = attrs_count;
295293
296294 output.push_str(&" ".repeat(level));
src/main.rs+65−92
@@ -218,8 +218,8 @@ fn bundle_assets(html: &str, base_dir: &Path) -> String {
218218 && !path_str.is_empty()
219219 {
220220 let asset_path = base_dir.join(path_str);
221− if asset_path.exists() {
222− if let Ok(mut file) = std::fs::File::open(&asset_path) {
221+ if asset_path.exists()
222+ && let Ok(mut file) = std::fs::File::open(&asset_path) {
223223 let mut buf = Vec::new();
224224 if file.read_to_end(&mut buf).is_ok() {
225225 let mime = match asset_path.extension().and_then(|e| e.to_str()) {
@@ -246,7 +246,6 @@ fn bundle_assets(html: &str, base_dir: &Path) -> String {
246246 continue;
247247 }
248248 }
249− }
250249 }
251250 // If we couldn't inline, keep original path
252251 output.push_str(&remaining[..end]);
@@ -261,7 +260,7 @@ fn bundle_assets(html: &str, base_dir: &Path) -> String {
261260
262261fn base64_encode(data: &[u8]) -> String {
263262 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
264− let mut result = String::with_capacity((data.len() + 2) / 3 * 4);
263+ let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
265264 for chunk in data.chunks(3) {
266265 let b0 = chunk[0] as u32;
267266 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
@@ -293,7 +292,7 @@ fn copy_non_hl_recursive(base: &Path, dir: &Path, out_dir: &Path) {
293292 let path = entry.path();
294293 if path.is_dir() {
295294 copy_non_hl_recursive(base, &path, out_dir);
296− } else if path.is_file() && path.extension().map_or(true, |e| e != "hl") {
295+ } else if path.is_file() && path.extension().is_none_or(|e| e != "hl") {
297296 let rel = path.strip_prefix(base).unwrap_or(&path);
298297 let dest = out_dir.join(rel);
299298 if let Some(parent) = dest.parent() {
@@ -362,12 +361,11 @@ h1{{color:#ff6b6b;margin-bottom:1rem;font-size:1.5rem}}
362361
363362fn init_project(dir: &str, template_name: Option<&str>) {
364363 let dir = Path::new(dir);
365− if dir.to_str() != Some(".") {
366− if let Err(e) = fs::create_dir_all(dir) {
364+ if dir.to_str() != Some(".")
365+ && let Err(e) = fs::create_dir_all(dir) {
367366 eprintln!("error: cannot create directory '{}': {}", dir.display(), e);
368367 process::exit(1);
369368 }
370− }
371369
372370 let index_path = dir.join("index.hl");
373371 if index_path.exists() {
@@ -501,7 +499,7 @@ fn collect_hl_recursive_inner(dir: &Path, files: &mut Vec<PathBuf>) {
501499 let path = entry.path();
502500 if path.is_dir() {
503501 collect_hl_recursive_inner(&path, files);
504− } else if path.is_file() && path.extension().map_or(false, |e| e == "hl") {
502+ } else if path.is_file() && path.extension().is_some_and(|e| e == "hl") {
505503 files.push(path);
506504 }
507505 }
@@ -627,28 +625,26 @@ fn lint_nodes(nodes: &[htmlang::ast::Node], path: &str, depth: usize, warnings:
627625 }
628626
629627 // @image without alt
630− if elem.kind == htmlang::ast::ElementKind::Image {
631− if !elem.attrs.iter().any(|a| a.key == "alt") {
628+ if elem.kind == htmlang::ast::ElementKind::Image
629+ && !elem.attrs.iter().any(|a| a.key == "alt") {
632630 warnings.push(format!("{}:{}:lint: @image missing 'alt' attribute (accessibility)", path, elem.line_num));
633631 }
634− }
635632
636633 // @link without content or aria-label
637634 if elem.kind == htmlang::ast::ElementKind::Link {
638635 let has_aria = elem.attrs.iter().any(|a| a.key == "aria-label");
639636 let has_children = !elem.children.is_empty();
640− let has_arg_text = elem.argument.as_ref().map_or(false, |_| false);
637+ let has_arg_text = elem.argument.as_ref().is_some_and(|_| false);
641638 if !has_aria && !has_children && !has_arg_text {
642639 warnings.push(format!("{}:{}:lint: @link has no visible text or aria-label (accessibility)", path, elem.line_num));
643640 }
644641 }
645642
646643 // @input without type
647− if elem.kind == htmlang::ast::ElementKind::Input {
648− if !elem.attrs.iter().any(|a| a.key == "type") {
644+ if elem.kind == htmlang::ast::ElementKind::Input
645+ && !elem.attrs.iter().any(|a| a.key == "type") {
649646 warnings.push(format!("{}:{}:lint: @input missing 'type' attribute", path, elem.line_num));
650647 }
651− }
652648
653649 // Empty containers (no children, no text)
654650 if matches!(elem.kind,
@@ -664,11 +660,10 @@ fn lint_nodes(nodes: &[htmlang::ast::Node], path: &str, depth: usize, warnings:
664660 }
665661
666662 // @button without type
667− if elem.kind == htmlang::ast::ElementKind::Button {
668− if !elem.attrs.iter().any(|a| a.key == "type") {
663+ if elem.kind == htmlang::ast::ElementKind::Button
664+ && !elem.attrs.iter().any(|a| a.key == "type") {
669665 warnings.push(format!("{}:{}:lint: @button missing 'type' attribute (defaults to submit)", path, elem.line_num));
670666 }
671− }
672667
673668 lint_nodes(&elem.children, path, depth + 1, warnings);
674669 }
@@ -736,17 +731,15 @@ fn count_elements(
736731 for attr in &elem.attrs {
737732 let key = attr.key.as_str();
738733 // Strip pseudo/media prefixes for color/font detection
739− let base_key = key.split(':').last().unwrap_or(key);
740− if matches!(base_key, "color" | "background") {
741− if let Some(ref v) = attr.value {
734+ let base_key = key.split(':').next_back().unwrap_or(key);
735+ if matches!(base_key, "color" | "background")
736+ && let Some(ref v) = attr.value {
742737 colors.insert(v.clone());
743738 }
744− }
745− if base_key == "font" {
746− if let Some(ref v) = attr.value {
739+ if base_key == "font"
740+ && let Some(ref v) = attr.value {
747741 fonts.insert(v.clone());
748742 }
749− }
750743 }
751744 count_elements(&elem.children, count, colors, fonts);
752745 }
@@ -761,8 +754,8 @@ fn extract_shared_css(html_files: &[PathBuf], out_dir: &Path) {
761754 for file in html_files {
762755 if let Ok(html) = fs::read_to_string(file) {
763756 // Extract CSS between <style> and </style>
764− if let Some(start) = html.find("<style>") {
765− if let Some(end) = html[start..].find("</style>") {
757+ if let Some(start) = html.find("<style>")
758+ && let Some(end) = html[start..].find("</style>") {
766759 let css = &html[start + 7..start + end];
767760 // Extract individual rules (class-based)
768761 let mut seen_in_file = std::collections::HashSet::new();
@@ -792,7 +785,6 @@ fn extract_shared_css(html_files: &[PathBuf], out_dir: &Path) {
792785 }
793786 }
794787 }
795− }
796788 }
797789 }
798790 // Rules appearing in ALL files are shared
@@ -813,9 +805,9 @@ fn extract_shared_css(html_files: &[PathBuf], out_dir: &Path) {
813805
814806 // Remove shared rules from individual files and inject <link> tag
815807 for file in html_files {
816− if let Ok(html) = fs::read_to_string(file) {
817− if let Some(style_start) = html.find("<style>") {
818− if let Some(style_end_rel) = html[style_start..].find("</style>") {
808+ if let Ok(html) = fs::read_to_string(file)
809+ && let Some(style_start) = html.find("<style>")
810+ && let Some(style_end_rel) = html[style_start..].find("</style>") {
819811 let css = &html[style_start + 7..style_start + style_end_rel];
820812 // Rebuild CSS without shared rules
821813 let mut filtered = String::new();
@@ -853,8 +845,6 @@ fn extract_shared_css(html_files: &[PathBuf], out_dir: &Path) {
853845 );
854846 let _ = fs::write(file, new_html);
855847 }
856− }
857− }
858848 }
859849}
860850
@@ -1144,8 +1134,8 @@ fn main() {
11441134 let mut hasher = std::collections::hash_map::DefaultHasher::new();
11451135 content.hash(&mut hasher);
11461136 let current_hash = hasher.finish().to_string();
1147− if let Ok(cached_hash) = fs::read_to_string(&hash_path) {
1148− if cached_hash.trim() == current_hash {
1137+ if let Ok(cached_hash) = fs::read_to_string(&hash_path)
1138+ && cached_hash.trim() == current_hash {
11491139 // Also verify output exists
11501140 let out_exists = effective_out.as_ref().map_or(
11511141 file.with_extension("html").exists(),
@@ -1156,7 +1146,6 @@ fn main() {
11561146 return;
11571147 }
11581148 }
1159− }
11601149 // Update hash cache after compilation
11611150 let _ = fs::write(&hash_path, &current_hash);
11621151 }
@@ -1301,7 +1290,7 @@ fn main() {
13011290 match args[ci].as_str() {
13021291 "--format" => {
13031292 ci += 1;
1304− if args.get(ci).map_or(false, |v| v == "json") {
1293+ if args.get(ci).is_some_and(|v| v == "json") {
13051294 check_format_json = true;
13061295 }
13071296 }
@@ -1340,11 +1329,10 @@ fn main() {
13401329 });
13411330 if has_errors { any_errors = true; }
13421331 }
1343− if check_format_json {
1344− if let Some(collector) = json_collector {
1332+ if check_format_json
1333+ && let Some(collector) = json_collector {
13451334 print_json_diagnostics(&collector.lock().unwrap());
13461335 }
1347− }
13481336 if any_errors { process::exit(1); }
13491337 return;
13501338 }
@@ -1683,7 +1671,7 @@ fn main() {
16831671 });
16841672 }
16851673 let (tx, _) = tokio::sync::broadcast::channel::<()>(16);
1686− let serve_dir = config.output.as_ref().map(|o| PathBuf::from(o)).unwrap_or_else(|| target_path.to_path_buf());
1674+ let serve_dir = config.output.as_ref().map(PathBuf::from).unwrap_or_else(|| target_path.to_path_buf());
16871675 let index_path = serve_dir.join("index.html");
16881676 let server_tx = tx.clone();
16891677 let tls_for_thread = tls_config;
@@ -2036,11 +2024,10 @@ fn main() {
20362024 let name = name.trim_end_matches('[');
20372025 all_def_defs.push((name.to_string(), rel.clone(), line_num + 1));
20382026 }
2039− } else if let Some(rest) = trimmed.strip_prefix("@let ") {
2040− if let Some(name) = rest.split_whitespace().next() {
2027+ } else if let Some(rest) = trimmed.strip_prefix("@let ")
2028+ && let Some(name) = rest.split_whitespace().next() {
20412029 all_let_defs.push((name.to_string(), rel.clone(), line_num + 1));
20422030 }
2043− }
20442031 // Collect references: @name calls and $name usages
20452032 if trimmed.starts_with('@') && !trimmed.starts_with("@fn ")
20462033 && !trimmed.starts_with("@let ") && !trimmed.starts_with("@define ")
@@ -2053,11 +2040,9 @@ fn main() {
20532040 && !trimmed.starts_with("@use ") && !trimmed.starts_with("@slot ")
20542041 && !trimmed.starts_with("@lang ") && !trimmed.starts_with("@favicon ")
20552042 && !trimmed.starts_with("--")
2056− {
2057− if let Some(name) = trimmed[1..].split(|c: char| c == ' ' || c == '[').next() {
2043+ && let Some(name) = trimmed[1..].split([' ', '[']).next() {
20582044 all_refs.insert(name.to_string());
20592045 }
2060− }
20612046 // Collect $var references
20622047 let mut rest = trimmed;
20632048 while let Some(pos) = rest.find('$') {
@@ -2184,7 +2169,7 @@ fn main() {
21842169 "netlify" => {
21852170 eprintln!("deploying to Netlify...");
21862171 let status = process::Command::new("npx")
2187− .args(&["netlify-cli", "deploy", "--prod", "--dir", &deploy_dir.to_string_lossy()])
2172+ .args(["netlify-cli", "deploy", "--prod", "--dir", &deploy_dir.to_string_lossy()])
21882173 .status();
21892174 match status {
21902175 Ok(s) if s.success() => eprintln!("deployed to Netlify!"),
@@ -2198,7 +2183,7 @@ fn main() {
21982183 "vercel" => {
21992184 eprintln!("deploying to Vercel...");
22002185 let status = process::Command::new("npx")
2201− .args(&["vercel", "--prod", &deploy_dir.to_string_lossy().to_string()])
2186+ .args(["vercel", "--prod", deploy_dir.to_string_lossy().as_ref()])
22022187 .status();
22032188 match status {
22042189 Ok(s) if s.success() => eprintln!("deployed to Vercel!"),
@@ -2212,7 +2197,7 @@ fn main() {
22122197 "cloudflare" => {
22132198 eprintln!("deploying to Cloudflare Pages...");
22142199 let status = process::Command::new("npx")
2215− .args(&["wrangler", "pages", "deploy", &deploy_dir.to_string_lossy().to_string()])
2200+ .args(["wrangler", "pages", "deploy", deploy_dir.to_string_lossy().as_ref()])
22162201 .status();
22172202 match status {
22182203 Ok(s) if s.success() => eprintln!("deployed to Cloudflare Pages!"),
@@ -2229,7 +2214,7 @@ fn main() {
22292214 // Deploy via gh-pages push
22302215 let init_ok = matches!(
22312216 process::Command::new("git")
2232− .args(&["init"])
2217+ .args(["init"])
22332218 .current_dir(&deploy_dir)
22342219 .status(),
22352220 Ok(s) if s.success()
@@ -2239,16 +2224,16 @@ fn main() {
22392224 process::exit(1);
22402225 }
22412226 let _ = process::Command::new("git")
2242− .args(&["checkout", "-b", "gh-pages"])
2227+ .args(["checkout", "-b", "gh-pages"])
22432228 .current_dir(&deploy_dir)
22442229 .status();
22452230 let _ = process::Command::new("git")
2246− .args(&["add", "."])
2231+ .args(["add", "."])
22472232 .current_dir(&deploy_dir)
22482233 .status();
22492234 let commit_ok = matches!(
22502235 process::Command::new("git")
2251− .args(&["commit", "-m", "deploy"])
2236+ .args(["commit", "-m", "deploy"])
22522237 .current_dir(&deploy_dir)
22532238 .status(),
22542239 Ok(s) if s.success()
@@ -2259,19 +2244,19 @@ fn main() {
22592244 }
22602245 // Check if remote origin exists in parent
22612246 let remote_output = process::Command::new("git")
2262− .args(&["remote", "get-url", "origin"])
2247+ .args(["remote", "get-url", "origin"])
22632248 .output();
22642249 if let Ok(output) = remote_output {
22652250 if output.status.success() {
22662251 let remote_url = String::from_utf8_lossy(&output.stdout).trim().to_string();
22672252 let _ = process::Command::new("git")
2268− .args(&["remote", "add", "origin", &remote_url])
2253+ .args(["remote", "add", "origin", &remote_url])
22692254 .current_dir(&deploy_dir)
22702255 .status();
22712256 eprintln!("pushing to gh-pages branch at {}...", remote_url);
22722257 let push_ok = matches!(
22732258 process::Command::new("git")
2274− .args(&["push", "-f", "origin", "gh-pages"])
2259+ .args(["push", "-f", "origin", "gh-pages"])
22752260 .current_dir(&deploy_dir)
22762261 .status(),
22772262 Ok(s) if s.success()
@@ -2612,12 +2597,11 @@ compile();
26122597 }
26132598 // Also remove sitemap.xml if present
26142599 let sitemap = dir.join("sitemap.xml");
2615− if sitemap.exists() {
2616− if let Ok(()) = fs::remove_file(&sitemap) {
2600+ if sitemap.exists()
2601+ && let Ok(()) = fs::remove_file(&sitemap) {
26172602 eprintln!("removed {}", sitemap.display());
26182603 removed += 1;
26192604 }
2620− }
26212605 eprintln!("cleaned {} file{}", removed, if removed == 1 { "" } else { "s" });
26222606 return;
26232607 }
@@ -2973,8 +2957,8 @@ compile();
29732957 let html = htmlang::codegen::generate_dev(&result.document);
29742958 // Extract CSS class mappings from <style> block
29752959 let mut class_css: HashMap<String, String> = HashMap::new();
2976− if let Some(style_start) = html.find("<style>") {
2977− if let Some(style_end) = html[style_start..].find("</style>") {
2960+ if let Some(style_start) = html.find("<style>")
2961+ && let Some(style_end) = html[style_start..].find("</style>") {
29782962 let css_block = &html[style_start + 7..style_start + style_end];
29792963 // Parse each CSS rule: .className { rules }
29802964 let mut pos = 0;
@@ -3016,7 +3000,6 @@ compile();
30163000 }
30173001 }
30183002 }
3019− }
30203003 // Extract data-hl-line attributes from dev HTML to map classes to source lines
30213004 let mut line_classes: HashMap<usize, Vec<String>> = HashMap::new();
30223005 let html_body = if let Some(body_start) = html.find("<body") {
@@ -3036,8 +3019,8 @@ compile();
30363019 // Extract data-hl-line
30373020 if let Some(hl_pos) = tag.find("data-hl-line=\"") {
30383021 let after = &tag[hl_pos + 14..];
3039− if let Some(end) = after.find('"') {
3040− if let Ok(ln) = after[..end].parse::<usize>() {
3022+ if let Some(end) = after.find('"')
3023+ && let Ok(ln) = after[..end].parse::<usize>() {
30413024 // Extract class
30423025 if let Some(cls_pos) = tag.find("class=\"") {
30433026 let cls_after = &tag[cls_pos + 7..];
@@ -3048,7 +3031,6 @@ compile();
30483031 }
30493032 }
30503033 }
3051− }
30523034 }
30533035 scan_pos = tag_end;
30543036 } else {
@@ -3444,11 +3426,10 @@ compile();
34443426 all_included.extend(included);
34453427 }
34463428
3447− if format_json {
3448− if let Some(collector) = json_collector {
3429+ if format_json
3430+ && let Some(collector) = json_collector {
34493431 print_json_diagnostics(&collector.lock().unwrap());
34503432 }
3451− }
34523433
34533434 if !watch {
34543435 if any_errors {
@@ -3487,11 +3468,10 @@ compile();
34873468 compat, strict, partial,
34883469 ..Default::default()
34893470 });
3490− if format_json {
3491− if let Some(ref collector) = json_collector_single {
3471+ if format_json
3472+ && let Some(ref collector) = json_collector_single {
34923473 print_json_diagnostics(&collector.lock().unwrap());
34933474 }
3494− }
34953475
34963476 if !watch {
34973477 if has_errors {
@@ -3540,7 +3520,7 @@ fn collect_hl_files(dir: &Path) -> Vec<PathBuf> {
35403520 if let Ok(entries) = fs::read_dir(dir) {
35413521 for entry in entries.flatten() {
35423522 let path = entry.path();
3543− if path.is_file() && path.extension().map_or(false, |e| e == "hl") {
3523+ if path.is_file() && path.extension().is_some_and(|e| e == "hl") {
35443524 files.push(path);
35453525 }
35463526 }
@@ -3549,6 +3529,7 @@ fn collect_hl_files(dir: &Path) -> Vec<PathBuf> {
35493529 files
35503530}
35513531
3532+#[allow(clippy::too_many_arguments)]
35523533fn watch_loop(
35533534 watch_dir: &Path,
35543535 source_files: &[PathBuf],
@@ -3639,24 +3620,21 @@ fn watch_loop(
36393620 }
36403621 }
36413622
3642− loop {
3643− match rx.recv() {
3644− Ok(_) => {
3645− // Drain additional events (debounce) with configurable delay
3646− std::thread::sleep(std::time::Duration::from_millis(debounce_ms));
3647− while rx.try_recv().is_ok() {}
3623+ while rx.recv().is_ok() {
3624+ // Drain additional events (debounce) with configurable delay
3625+ std::thread::sleep(std::time::Duration::from_millis(debounce_ms));
3626+ while rx.try_recv().is_ok() {}
36483627
36493628 // Collect which files actually changed (HashSet for O(1) lookups)
36503629 let mut changed_files: HashSet<PathBuf> = HashSet::new();
36513630 let check_path = |path: &Path,
36523631 hashes: &mut HashMap<PathBuf, u64>|
36533632 -> bool {
3654− if let Some(h) = hash_file(path) {
3655− if hashes.get(path) != Some(&h) {
3633+ if let Some(h) = hash_file(path)
3634+ && hashes.get(path) != Some(&h) {
36563635 hashes.insert(path.to_path_buf(), h);
36573636 return true;
36583637 }
3659− }
36603638 false
36613639 };
36623640
@@ -3668,7 +3646,7 @@ fn watch_loop(
36683646 }
36693647
36703648 // Check included files for changes
3671− for (_, deps) in &dep_map {
3649+ for deps in dep_map.values() {
36723650 for dep in deps {
36733651 if check_path(dep, &mut content_hashes) {
36743652 changed_files.insert(dep.clone());
@@ -3722,13 +3700,11 @@ fn watch_loop(
37223700 continue;
37233701 }
37243702 // Recompile if any of its dependencies changed
3725− if let Some(deps) = dep_map.get(source) {
3726− if deps.iter().any(|d| changed_files.contains(d)) {
3727− if !files_to_compile.contains(source) {
3703+ if let Some(deps) = dep_map.get(source)
3704+ && deps.iter().any(|d| changed_files.contains(d))
3705+ && !files_to_compile.contains(source) {
37283706 files_to_compile.push(source.clone());
37293707 }
3730− }
3731− }
37323708 }
37333709
37343710 // If no specific files identified (e.g., first run), compile all
@@ -3782,8 +3758,5 @@ fn watch_loop(
37823758 if let Some(ref tx) = reload_tx {
37833759 let _ = tx.send(());
37843760 }
3785− }
3786− Err(_) => break,
3787− }
37883761 }
37893762}
src/serve.rs+4−5
@@ -233,7 +233,7 @@ where
233233 Some(fp) => {
234234 let body = match std::fs::read(&fp) {
235235 Ok(content) => {
236− if fp.extension().map_or(false, |e| e == "html") {
236+ if fp.extension().is_some_and(|e| e == "html") {
237237 inject_reload_script(&content)
238238 } else {
239239 content
@@ -304,11 +304,10 @@ fn collect_html_pages(base: &Path, dir: &Path, pages: &mut Vec<String>) {
304304 let path = entry.path();
305305 if path.is_dir() {
306306 collect_html_pages(base, &path, pages);
307− } else if path.extension().map_or(false, |e| e == "html") {
308− if let Ok(rel) = path.strip_prefix(base) {
307+ } else if path.extension().is_some_and(|e| e == "html")
308+ && let Ok(rel) = path.strip_prefix(base) {
309309 pages.push(format!("/{}", rel.display()));
310310 }
311− }
312311 }
313312 }
314313}
@@ -416,7 +415,7 @@ where
416415
417416 let body = match std::fs::read(&file_path) {
418417 Ok(content) => {
419− if file_path.extension().map_or(false, |e| e == "html") {
418+ if file_path.extension().is_some_and(|e| e == "html") {
420419 inject_reload_script(&content)
421420 } else {
422421 content