on master help sign in
Apply clippy fixes: collapsible if statements, strip_prefix, iterators, and type simplifications
503750diko
crates/htmlang-core/src/codegen.rs+38−59
@@ -71,6 +71,7 @@ impl StyleCollector {
71 }
71 }
72
72
73 /// Returns a class name for this style combination, or None if all empty.
73 /// Returns a class name for this style combination, or None if all empty.
74+ #[allow(clippy::too_many_arguments)]
74 fn get_class(
75 fn get_class(
75 &mut self,
76 &mut self,
76 base: String,
77 base: String,
@@ -1348,10 +1349,7 @@ fn generate_element(
1348 match child {
1349 match child {
1349 Node::Text(segments) => {
1350 Node::Text(segments) => {
1350 for seg in segments {
1351 for seg in segments {
1351− match seg {
1352− TextSegment::Plain(text) => out.push_str(text),
1352+ if let TextSegment::Plain(text) = seg { out.push_str(text) }
1353− _ => {}
1354− }
1355 }
1353 }
1356 }
1354 }
1357 Node::Raw(content) => out.push_str(content),
1355 Node::Raw(content) => out.push_str(content),
@@ -1582,8 +1580,8 @@ fn generate_element(
1582 out.push_str(" id=\"hl-main\"");
1580 out.push_str(" id=\"hl-main\"");
1583 }
1581 }
1584
1582
1585− if elem.kind == ElementKind::Link {
1583+ if elem.kind == ElementKind::Link
1586− if let Some(url) = &elem.argument {
1584+ && let Some(url) = &elem.argument {
1587 out.push_str(" href=\"");
1585 out.push_str(" href=\"");
1588 out.push_str(&html_escape(url));
1586 out.push_str(&html_escape(url));
1589 out.push('"');
1587 out.push('"');
@@ -1600,54 +1598,47 @@ fn generate_element(
1600 }
1598 }
1601 }
1599 }
1602 }
1600 }
1603− }
1604
1601
1605 // Video/Audio src
1602 // Video/Audio src
1606− if matches!(elem.kind, ElementKind::Video | ElementKind::Audio) {
1603+ if matches!(elem.kind, ElementKind::Video | ElementKind::Audio)
1607− if let Some(src) = &elem.argument {
1604+ && let Some(src) = &elem.argument {
1608 out.push_str(" src=\"");
1605 out.push_str(" src=\"");
1609 out.push_str(&html_escape(src));
1606 out.push_str(&html_escape(src));
1610 out.push('"');
1607 out.push('"');
1611 }
1608 }
1612− }
1613
1609
1614 // Form action
1610 // Form action
1615− if elem.kind == ElementKind::Form {
1611+ if elem.kind == ElementKind::Form
1616− if let Some(action) = &elem.argument {
1612+ && let Some(action) = &elem.argument {
1617 out.push_str(" action=\"");
1613 out.push_str(" action=\"");
1618 out.push_str(&html_escape(action));
1614 out.push_str(&html_escape(action));
1619 out.push('"');
1615 out.push('"');
1620 }
1616 }
1621− }
1622
1617
1623 // Iframe src
1618 // Iframe src
1624− if elem.kind == ElementKind::Iframe {
1619+ if elem.kind == ElementKind::Iframe
1625− if let Some(src) = &elem.argument {
1620+ && let Some(src) = &elem.argument {
1626 out.push_str(" src=\"");
1621 out.push_str(" src=\"");
1627 out.push_str(&html_escape(src));
1622 out.push_str(&html_escape(src));
1628 out.push('"');
1623 out.push('"');
1629 }
1624 }
1630− }
1631
1625
1632 // Tooltip title
1626 // Tooltip title
1633− if elem.kind == ElementKind::Tooltip {
1627+ if elem.kind == ElementKind::Tooltip
1634− if let Some(text) = &elem.argument {
1628+ && let Some(text) = &elem.argument {
1635 out.push_str(" title=\"");
1629 out.push_str(" title=\"");
1636 out.push_str(&html_escape(text));
1630 out.push_str(&html_escape(text));
1637 out.push('"');
1631 out.push('"');
1638 }
1632 }
1639− }
1640
1633
1641 // Time datetime
1634 // Time datetime
1642− if elem.kind == ElementKind::Time {
1635+ if elem.kind == ElementKind::Time
1643− if let Some(dt) = elem.attrs.iter().find(|a| a.key == "datetime") {
1636+ && let Some(dt) = elem.attrs.iter().find(|a| a.key == "datetime")
1644− if let Some(val) = &dt.value {
1637+ && let Some(val) = &dt.value {
1645 out.push_str(" datetime=\"");
1638 out.push_str(" datetime=\"");
1646 out.push_str(&html_escape(val));
1639 out.push_str(&html_escape(val));
1647 out.push('"');
1640 out.push('"');
1648 }
1641 }
1649− }
1650− }
1651
1642
1652 // Critical CSS: inline styles directly instead of using a class
1643 // Critical CSS: inline styles directly instead of using a class
1653 let is_critical = elem.attrs.iter().any(|a| a.key == "critical");
1644 let is_critical = elem.attrs.iter().any(|a| a.key == "critical");
@@ -1659,7 +1650,7 @@ fn generate_element(
1659 out.push('"');
1650 out.push('"');
1660 }
1651 }
1661 // Still use class for non-base styles (pseudo, responsive, etc.)
1652 // 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()) {
1663 emit_class_attr(out, gen_class.as_deref(), user_class.as_deref());
1654 emit_class_attr(out, gen_class.as_deref(), user_class.as_deref());
1664 } else if let Some(ref uc) = user_class {
1655 } else if let Some(ref uc) = user_class {
1665 emit_class_attr(out, None, Some(uc));
1656 emit_class_attr(out, None, Some(uc));
@@ -1710,11 +1701,10 @@ fn generate_element(
1710 | ElementKind::Tooltip
1701 | ElementKind::Tooltip
1711 | ElementKind::Chip
1702 | ElementKind::Chip
1712 | ElementKind::Tag
1703 | ElementKind::Tag
1713− ) {
1704+ )
1714− if let Some(text) = &elem.argument {
1705+ && let Some(text) = &elem.argument {
1715 out.push_str(&html_escape(text));
1706 out.push_str(&html_escape(text));
1716 }
1707 }
1717− }
1718
1708
1719 // Children
1709 // Children
1720 ctx.depth += 1;
1710 ctx.depth += 1;
@@ -1809,10 +1799,10 @@ fn generate_self_closing(
1809 // Image optimization: auto-add loading="lazy" and decoding="async"
1799 // Image optimization: auto-add loading="lazy" and decoding="async"
1810 if elem.kind == ElementKind::Image {
1800 if elem.kind == ElementKind::Image {
1811 // SVG inlining: @image [inline] logo.svg
1801 // SVG inlining: @image [inline] logo.svg
1812− if elem.attrs.iter().any(|a| a.key == "inline") {
1802+ if elem.attrs.iter().any(|a| a.key == "inline")
1813− if let Some(src) = &elem.argument {
1803+ && let Some(src) = &elem.argument
1814− if src.ends_with(".svg") {
1804+ && src.ends_with(".svg")
1815− if let Ok(svg_content) = std::fs::read_to_string(src) {
1805+ && let Ok(svg_content) = std::fs::read_to_string(src) {
1816 // Close the tag we opened, then emit inline SVG instead
1806 // Close the tag we opened, then emit inline SVG instead
1817 out.truncate(out.rfind('<').unwrap_or(0));
1807 out.truncate(out.rfind('<').unwrap_or(0));
1818 out.push_str(&ctx.indent());
1808 out.push_str(&ctx.indent());
@@ -1820,13 +1810,10 @@ fn generate_self_closing(
1820 out.push_str(ctx.nl());
1810 out.push_str(ctx.nl());
1821 return;
1811 return;
1822 }
1812 }
1823− }
1824− }
1825− }
1826 // Responsive srcset: @image photo.jpg [responsive 400 800 1200]
1813 // Responsive srcset: @image photo.jpg [responsive 400 800 1200]
1827 let responsive_attr = elem.attrs.iter().find(|a| a.key == "responsive");
1814 let responsive_attr = elem.attrs.iter().find(|a| a.key == "responsive");
1828− if let Some(resp) = responsive_attr {
1815+ if let Some(resp) = responsive_attr
1829− if let Some(ref sizes_str) = resp.value {
1816+ && let Some(ref sizes_str) = resp.value {
1830 let widths: Vec<&str> = sizes_str.split_whitespace().collect();
1817 let widths: Vec<&str> = sizes_str.split_whitespace().collect();
1831 if !widths.is_empty() {
1818 if !widths.is_empty() {
1832 let src = elem.argument.as_deref().unwrap_or("");
1819 let src = elem.argument.as_deref().unwrap_or("");
@@ -1849,15 +1836,14 @@ fn generate_self_closing(
1849 }
1836 }
1850 }
1837 }
1851 }
1838 }
1852− }
1853
1839
1854 // Auto image dimensions: read local image file to inject width/height + aspect-ratio
1840 // Auto image dimensions: read local image file to inject width/height + aspect-ratio
1855 let has_width = elem.attrs.iter().any(|a| a.key == "width");
1841 let has_width = elem.attrs.iter().any(|a| a.key == "width");
1856 let has_height = elem.attrs.iter().any(|a| a.key == "height");
1842 let has_height = elem.attrs.iter().any(|a| a.key == "height");
1857− if !has_width || !has_height {
1843+ if (!has_width || !has_height)
1858− if let Some(ref src) = elem.argument {
1844+ && let Some(ref src) = elem.argument
1859− if !src.starts_with("http://") && !src.starts_with("https://") && !src.starts_with("data:") {
1845+ && !src.starts_with("http://") && !src.starts_with("https://") && !src.starts_with("data:")
1860− if let Some((w, h)) = read_image_dimensions(src) {
1846+ && let Some((w, h)) = read_image_dimensions(src) {
1861 if !has_width {
1847 if !has_width {
1862 out.push_str(&format!(" width=\"{}\"", w));
1848 out.push_str(&format!(" width=\"{}\"", w));
1863 }
1849 }
@@ -1869,9 +1855,6 @@ fn generate_self_closing(
1869 out.push_str(&format!(" style=\"aspect-ratio:{}/{}\"", w, h));
1855 out.push_str(&format!(" style=\"aspect-ratio:{}/{}\"", w, h));
1870 }
1856 }
1871 }
1857 }
1872− }
1873− }
1874− }
1875
1858
1876 // Smart image loading: first 3 images get fetchpriority="high" (above the fold),
1859 // Smart image loading: first 3 images get fetchpriority="high" (above the fold),
1877 // subsequent images get loading="lazy" + decoding="async"
1860 // subsequent images get loading="lazy" + decoding="async"
@@ -1965,19 +1948,18 @@ fn compute_class(
1965 // Collect has(...): dynamic pseudo selectors
1948 // Collect has(...): dynamic pseudo selectors
1966 let mut has_prefixes: Vec<String> = Vec::new();
1949 let mut has_prefixes: Vec<String> = Vec::new();
1967 for attr in attrs {
1950 for attr in attrs {
1968− if attr.key.starts_with("has(") {
1951+ if attr.key.starts_with("has(")
1969− if let Some(close) = attr.key.find("):") {
1952+ && let Some(close) = attr.key.find("):") {
1970 let prefix = format!("{}:", &attr.key[..close + 1]);
1953 let prefix = format!("{}:", &attr.key[..close + 1]);
1971 if !has_prefixes.contains(&prefix) {
1954 if !has_prefixes.contains(&prefix) {
1972 has_prefixes.push(prefix);
1955 has_prefixes.push(prefix);
1973 }
1956 }
1974 }
1957 }
1975− }
1976 }
1958 }
1977 for prefix in &has_prefixes {
1959 for prefix in &has_prefixes {
1978 let inner = &prefix[4..prefix.len() - 2]; // extract selector from has(selector):
1960 let inner = &prefix[4..prefix.len() - 2]; // extract selector from has(selector):
1979 let selector = format!(":has({})", inner);
1961 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);
1981 if !css.is_empty() {
1963 if !css.is_empty() {
1982 pseudo.push((selector, css));
1964 pseudo.push((selector, css));
1983 }
1965 }
@@ -3143,7 +3125,7 @@ fn css_px(value: &str) -> String {
3143fn css_px_multi(value: &str) -> String {
3125fn css_px_multi(value: &str) -> String {
3144 value
3126 value
3145 .split_whitespace()
3127 .split_whitespace()
3146− .map(|p| css_px(p))
3128+ .map(css_px)
3147 .collect::<Vec<_>>()
3129 .collect::<Vec<_>>()
3148 .join(" ")
3130 .join(" ")
3149}
3131}
@@ -3246,13 +3228,11 @@ fn read_svg_dimensions(text: &str) -> Option<(u32, u32)> {
3246 let rest = &text[vb_start + 9..];
3228 let rest = &text[vb_start + 9..];
3247 if let Some(end) = rest.find('"') {
3229 if let Some(end) = rest.find('"') {
3248 let parts: Vec<&str> = rest[..end].split_whitespace().collect();
3230 let parts: Vec<&str> = rest[..end].split_whitespace().collect();
3249− if parts.len() == 4 {
3231+ if parts.len() == 4
3250− if let (Ok(w), Ok(h)) = (parts[2].parse::<f64>(), parts[3].parse::<f64>()) {
3232+ && let (Ok(w), Ok(h)) = (parts[2].parse::<f64>(), parts[3].parse::<f64>())
3251− if w > 0.0 && h > 0.0 {
3233+ && w > 0.0 && h > 0.0 {
3252 return Some((w.round() as u32, h.round() as u32));
3234 return Some((w.round() as u32, h.round() as u32));
3253 }
3235 }
3254− }
3255− }
3256 }
3236 }
3257 }
3237 }
3258 // Fall back to width/height attributes on <svg>
3238 // Fall back to width/height attributes on <svg>
@@ -3293,11 +3273,10 @@ fn read_avif_dimensions(data: &[u8]) -> Option<(u32, u32)> {
3293 // Recurse into container boxes (meta, iprp, ipco)
3273 // Recurse into container boxes (meta, iprp, ipco)
3294 if matches!(box_type, b"meta" | b"iprp" | b"ipco" | b"moov" | b"trak" | b"mdia") {
3274 if matches!(box_type, b"meta" | b"iprp" | b"ipco" | b"moov" | b"trak" | b"mdia") {
3295 let header_size = if box_type == b"meta" { 12 } else { 8 };
3275 let header_size = if box_type == b"meta" { 12 } else { 8 };
3296− if i + header_size < box_end {
3276+ if i + header_size < box_end
3297− if let Some(dims) = read_avif_dimensions(&data[i + header_size..box_end]) {
3277+ && let Some(dims) = read_avif_dimensions(&data[i + header_size..box_end]) {
3298 return Some(dims);
3278 return Some(dims);
3299 }
3279 }
3300− }
3301 }
3280 }
3302 i = box_end;
3281 i = box_end;
3303 }
3282 }
@@ -3435,7 +3414,7 @@ fn collect_dns_prefetch(html: &str, dev: bool) -> String {
3435 while let Some(pos) = rest.find("https://") {
3414 while let Some(pos) = rest.find("https://") {
3436 let start = pos + 8; // skip "https://"
3415 let start = pos + 8; // skip "https://"
3437 rest = &rest[start..];
3416 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());
3439 let domain = &rest[..end];
3418 let domain = &rest[..end];
3440 if !domain.is_empty() && domain.contains('.') && !domains.contains(&domain.to_string()) {
3419 if !domain.is_empty() && domain.contains('.') && !domains.contains(&domain.to_string()) {
3441 domains.push(domain.to_string());
3420 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
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('#')
238 {
238 {
@@ -242,8 +242,6 @@ fn collect_images_recursive(nodes: &[Node], hints: &mut Vec<crate::ast::PreloadH
242 crossorigin: false,
242 crossorigin: false,
243 });
243 });
244 }
244 }
245− }
246− }
247 collect_images_recursive(&elem.children, hints, max);
245 collect_images_recursive(&elem.children, hints, max);
248 }
246 }
249 }
247 }
@@ -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();
271
269
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("\"\"\"") {
277−
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 == "=" {
449 ""
445 ""
450 } else {
446 } else {
@@ -453,8 +449,7 @@ impl Parser {
453
449
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);
1030 }
1025 }
1031− }
1032 }
1026 }
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)
1064 };
1058 };
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| {
1060+
1066− let d = substitute_vars(d, &ctx.variables);
1061+ substitute_vars(d, &ctx.variables)
1067− d
1068 }));
1062 }));
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));
1537 }
1531 }
1538 }
1532 }
1539− }
1540 }
1533 }
1541
1534
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 {
1757
1750
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;
1767 }
1760 }
1768 }
1761 }
1769− }
1770− }
1771
1762
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 {
1817 }
1808 }
1818
1809
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(' ')));
1821
1812
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 {
2018 };
2009 };
2019
2010
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());
2023− ));
2024
2013
2025 return Ok(Some(vec![Node::Element(wrapper), script]));
2014 return Ok(Some(vec![Node::Element(wrapper), script]));
2026 }
2015 }
@@ -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);
2415 }
2404 }
2416 }
2405 }
@@ -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);
2444 }
2433 }
2445 }
2434 }
2446− }
2447− }
2448 return Ok(None);
2435 return Ok(None);
2449 }
2436 }
2450
2437
@@ -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;
2649 }
2636 }
2650− }
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;
2813 }
2799 }
2814− }
2815− }
2816 caller_children.push(child);
2800 caller_children.push(child);
2817 }
2801 }
2818
2802
@@ -2956,7 +2940,7 @@ fn replace_children_and_slots(
2956
2940
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,
2962 }
2946 }
@@ -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;
3314 }
3297 }
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()),
3324 });
3307 });
3325 }
3308 }
3326− }
3327 i = end;
3309 i = end;
3328 } else {
3310 } else {
3329 i += 1;
3311 i += 1;
@@ -3777,8 +3759,7 @@ fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validat
3777 }
3759 }
3778
3760
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
3792 }
3773 }
3793
3774
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
3877 }
3857 }
3878
3858
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,
3889 });
3869 });
3890 }
3870 }
3891− }
3892− }
3893 }
3871 }
3894
3872
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 {
4258 None
4235 None
@@ -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.)
4268+
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 _
4297− key
4298}
4275}
4299
4276
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,
4421 });
4398 });
4422 }
4399 }
4423− }
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,
4434 });
4410 });
4435 }
4411 }
4436− }
4437
4412
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(
4526 }
4501 }
4527 }
4502 }
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,
4537 });
4512 });
4538 }
4513 }
4539− }
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,
4548 });
4522 });
4549 }
4523 }
4550− }
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(
4586 });
4559 });
4587 }
4560 }
4588 }
4561 }
4589− }
4590 }
4562 }
4591
4563
4592 // @form inputs should have associated @label
4564 // @form inputs should have associated @label
@@ -4610,8 +4582,8 @@ fn validate_tree(
4610 }
4582 }
4611
4583
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,
4621 });
4593 });
4622 }
4594 }
4623− }
4624
4595
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(
4655 }
4626 }
4656
4627
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,
4668 });
4639 });
4669 }
4640 }
4670− }
4671− }
4672− }
4673
4641
4674 validate_tree(&elem.children, Some(&elem.kind), diagnostics);
4642 validate_tree(&elem.children, Some(&elem.kind), diagnostics);
4675 }
4643 }
@@ -4875,11 +4843,10 @@ fn replace_extends_slots(
4875
4843
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]);
4881 }
4849 }
4882− }
4883 return value.to_string();
4850 return value.to_string();
4884 }
4851 }
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 {
4896 }
4863 }
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);
4903 }
4870 }
4904− }
4905 return value.to_string();
4871 return value.to_string();
4906 }
4872 }
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);
4912 }
4878 }
4913− }
4914 return value.to_string();
4879 return value.to_string();
4915 }
4880 }
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);
4921 }
4886 }
4922− }
4923 return value.to_string();
4887 return value.to_string();
4924 }
4888 }
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);
4936 }
4900 }
4937− }
4938 return value.to_string();
4901 return value.to_string();
4939 }
4902 }
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(","));
5314 }
5277 }
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..];
5391
5354
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));
5398 }
5361 }
5399 }
5362 }
5400− }
5401
5363
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();
5504
5466
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;
5522 }
5484 }
5523 continue;
5485 continue;
@@ -5565,15 +5527,14 @@ fn markdown_to_html(lines: &[String]) -> String {
5565 }
5527 }
5566
5528
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;
5575 }
5537 }
5576− }
5577
5538
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 {
5584 }
5545 }
5585
5546
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;
5591 }
5552 }
5592
5553
@@ -5635,8 +5596,8 @@ fn md_inline(text: &str) -> String {
5635 }
5596 }
5636 }
5597 }
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;
5646 }
5607 }
5647− }
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;
5659 }
5619 }
5660− }
5661 }
5620 }
5662− }
5663 result.push(chars[i]);
5621 result.push(chars[i]);
5664 i += 1;
5622 i += 1;
5665 }
5623 }
@@ -5679,12 +5637,7 @@ fn find_closing_double(chars: &[char], start: usize, marker: char) -> Option<usi
5679}
5637}
5680
5638
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)
5684− return Some(i);
5685− }
5686− }
5687− None
5688}
5641}
5689
5642
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.
src/bin/htmlang_lsp/analysis.rs+30−43
@@ -37,8 +37,8 @@ pub(crate) fn document_symbols(text: &str) -> Vec<SymbolInformation> {
37 }
37 }
38
38
39 // @let definitions
39 // @let definitions
40− if let Some(rest) = trimmed.strip_prefix("@let ") {
40+ if let Some(rest) = trimmed.strip_prefix("@let ")
41− if let Some((name, value)) = rest.trim().split_once(' ') {
41+ && let Some((name, value)) = rest.trim().split_once(' ') {
42 symbols.push(SymbolInformation {
42 symbols.push(SymbolInformation {
43 name: format!("${}", name),
43 name: format!("${}", name),
44 kind: SymbolKind::VARIABLE,
44 kind: SymbolKind::VARIABLE,
@@ -51,11 +51,10 @@ pub(crate) fn document_symbols(text: &str) -> Vec<SymbolInformation> {
51 container_name: Some(format!("= {}", value.trim())),
51 container_name: Some(format!("= {}", value.trim())),
52 });
52 });
53 }
53 }
54− }
55
54
56 // @define definitions
55 // @define definitions
57− if let Some(rest) = trimmed.strip_prefix("@define ") {
56+ if let Some(rest) = trimmed.strip_prefix("@define ")
58− if let Some(bracket) = rest.find('[') {
57+ && let Some(bracket) = rest.find('[') {
59 let name = rest[..bracket].trim();
58 let name = rest[..bracket].trim();
60 symbols.push(SymbolInformation {
59 symbols.push(SymbolInformation {
61 name: format!("${}", name),
60 name: format!("${}", name),
@@ -69,7 +68,6 @@ pub(crate) fn document_symbols(text: &str) -> Vec<SymbolInformation> {
69 container_name: Some("attribute bundle".to_string()),
68 container_name: Some("attribute bundle".to_string()),
70 });
69 });
71 }
70 }
72− }
73
71
74 // @keyframes definitions
72 // @keyframes definitions
75 if let Some(rest) = trimmed.strip_prefix("@keyframes ") {
73 if let Some(rest) = trimmed.strip_prefix("@keyframes ") {
@@ -169,7 +167,7 @@ pub(crate) fn code_actions(
169 let trimmed = source_line.trim_start();
167 let trimmed = source_line.trim_start();
170 if trimmed.starts_with("@let ") {
168 if trimmed.starts_with("@let ") {
171 let var_name = trimmed.strip_prefix("@let ")
169 let var_name = trimmed.strip_prefix("@let ")
172− .and_then(|r| r.trim().split_whitespace().next())
170+ .and_then(|r| r.split_whitespace().next())
173 .unwrap_or("?");
171 .unwrap_or("?");
174 let edit = TextEdit {
172 let edit = TextEdit {
175 range: Range::new(
173 range: Range::new(
@@ -396,8 +394,8 @@ pub(crate) fn code_actions(
396 }),
394 }),
397 ..Default::default()
395 ..Default::default()
398 }));
396 }));
399− } else if source_line.contains("@input") {
397+ } else if source_line.contains("@input")
400− if let Some(pos) = source_line.find("@input") {
398+ && let Some(pos) = source_line.find("@input") {
401 let after = pos + "@input".len();
399 let after = pos + "@input".len();
402 let edit = TextEdit {
400 let edit = TextEdit {
403 range: Range::new(
401 range: Range::new(
@@ -419,7 +417,6 @@ pub(crate) fn code_actions(
419 ..Default::default()
417 ..Default::default()
420 }));
418 }));
421 }
419 }
422− }
423 }
420 }
424 }
421 }
425
422
@@ -436,19 +433,19 @@ pub(crate) fn code_actions(
436
433
437 // Quick-fix: auto-import suggestion for unknown element @name
434 // Quick-fix: auto-import suggestion for unknown element @name
438 // Searches current directory and subdirectories for @fn definitions
435 // Searches current directory and subdirectories for @fn definitions
439− if msg.contains("unknown element @") {
436+ if msg.contains("unknown element @")
440− if let Some(fn_name) = extract_between(msg, "unknown element @", ",")
437+ && let Some(fn_name) = extract_between(msg, "unknown element @", ",")
441 .or_else(|| extract_between(msg, "unknown element @", ""))
438 .or_else(|| extract_between(msg, "unknown element @", ""))
442 {
439 {
443 let fn_name = fn_name.trim();
440 let fn_name = fn_name.trim();
444− if !fn_name.is_empty() {
441+ if !fn_name.is_empty()
445− if let Ok(file_path) = uri.to_file_path() {
442+ && let Ok(file_path) = uri.to_file_path()
446− if let Some(dir) = file_path.parent() {
443+ && let Some(dir) = file_path.parent() {
447 // Search current dir and subdirs for .hl files defining this function
444 // Search current dir and subdirs for .hl files defining this function
448 let mut search_dirs = vec![dir.to_path_buf()];
445 let mut search_dirs = vec![dir.to_path_buf()];
449 // Also search parent dir's subdirs (for project-wide imports)
446 // Also search parent dir's subdirs (for project-wide imports)
450− if let Some(parent) = dir.parent() {
447+ if let Some(parent) = dir.parent()
451− if let Ok(entries) = std::fs::read_dir(parent) {
448+ && let Ok(entries) = std::fs::read_dir(parent) {
452 for entry in entries.flatten() {
449 for entry in entries.flatten() {
453 let p = entry.path();
450 let p = entry.path();
454 if p.is_dir() && p != dir {
451 if p.is_dir() && p != dir {
@@ -456,7 +453,6 @@ pub(crate) fn code_actions(
456 }
453 }
457 }
454 }
458 }
455 }
459− }
460 for search_dir in &search_dirs {
456 for search_dir in &search_dirs {
461 if let Ok(entries) = std::fs::read_dir(search_dir) {
457 if let Ok(entries) = std::fs::read_dir(search_dir) {
462 for entry in entries.flatten() {
458 for entry in entries.flatten() {
@@ -539,10 +535,7 @@ pub(crate) fn code_actions(
539 }
535 }
540 }
536 }
541 }
537 }
542− }
543− }
544 }
538 }
545− }
546 }
539 }
547
540
548 // Refactoring: extract selection to @fn
541 // Refactoring: extract selection to @fn
@@ -612,8 +605,8 @@ pub(crate) fn code_actions(
612 let line_idx = selection.start.line as usize;
605 let line_idx = selection.start.line as usize;
613 if line_idx < lines.len() {
606 if line_idx < lines.len() {
614 let line = lines[line_idx];
607 let line = lines[line_idx];
615− if let Some(bracket_start) = line.find('[') {
608+ if let Some(bracket_start) = line.find('[')
616− if let Some(bracket_end) = line[bracket_start..].find(']') {
609+ && let Some(bracket_end) = line[bracket_start..].find(']') {
617 let attrs_str = &line[bracket_start + 1..bracket_start + bracket_end];
610 let attrs_str = &line[bracket_start + 1..bracket_start + bracket_end];
618 // Only offer if there are at least 2 attributes
611 // Only offer if there are at least 2 attributes
619 let attr_count = attrs_str.split(',').count();
612 let attr_count = attrs_str.split(',').count();
@@ -656,7 +649,6 @@ pub(crate) fn code_actions(
656 }));
649 }));
657 }
650 }
658 }
651 }
659− }
660 }
652 }
661 }
653 }
662
654
@@ -709,8 +701,8 @@ pub(crate) fn find_colors(text: &str) -> Vec<ColorInformation> {
709 .unwrap_or(line.len());
701 .unwrap_or(line.len());
710 let hex = &line[hex_start..hex_end];
702 let hex = &line[hex_start..hex_end];
711 let len = hex.len();
703 let len = hex.len();
712− if len == 3 || len == 6 || len == 8 {
704+ if (len == 3 || len == 6 || len == 8)
713− if let Some((r, g, b, a)) = parse_hex_color(hex) {
705+ && let Some((r, g, b, a)) = parse_hex_color(hex) {
714 colors.push(ColorInformation {
706 colors.push(ColorInformation {
715 range: Range::new(
707 range: Range::new(
716 Position::new(line_idx as u32, abs_pos as u32),
708 Position::new(line_idx as u32, abs_pos as u32),
@@ -724,7 +716,6 @@ pub(crate) fn find_colors(text: &str) -> Vec<ColorInformation> {
724 },
716 },
725 });
717 });
726 }
718 }
727− }
728 start = hex_end;
719 start = hex_end;
729 }
720 }
730 // Detect named CSS colors
721 // Detect named CSS colors
@@ -911,38 +902,34 @@ pub(crate) fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
911 let result = htmlang::parser::parse(text);
902 let result = htmlang::parser::parse(text);
912 let mut unused_vars: std::collections::HashSet<String> = std::collections::HashSet::new();
903 let mut unused_vars: std::collections::HashSet<String> = std::collections::HashSet::new();
913 for d in &result.diagnostics {
904 for d in &result.diagnostics {
914− if d.message.contains("unused variable '$") {
905+ if d.message.contains("unused variable '$")
915− if let Some(start) = d.message.find("'$") {
906+ && let Some(start) = d.message.find("'$") {
916 let rest = &d.message[start + 2..];
907 let rest = &d.message[start + 2..];
917 if let Some(end) = rest.find('\'') {
908 if let Some(end) = rest.find('\'') {
918 unused_vars.insert(rest[..end].to_string());
909 unused_vars.insert(rest[..end].to_string());
919 }
910 }
920 }
911 }
921− }
922− if d.message.contains("unused function '@") {
912+ if d.message.contains("unused function '@")
923− if let Some(start) = d.message.find("'@") {
913+ && let Some(start) = d.message.find("'@") {
924 let rest = &d.message[start + 2..];
914 let rest = &d.message[start + 2..];
925 if let Some(end) = rest.find('\'') {
915 if let Some(end) = rest.find('\'') {
926 unused_vars.insert(format!("@{}", &rest[..end]));
916 unused_vars.insert(format!("@{}", &rest[..end]));
927 }
917 }
928 }
918 }
929− }
930− if d.message.contains("unused define '$") {
919+ if d.message.contains("unused define '$")
931− if let Some(start) = d.message.find("'$") {
920+ && let Some(start) = d.message.find("'$") {
932 let rest = &d.message[start + 2..];
921 let rest = &d.message[start + 2..];
933 if let Some(end) = rest.find('\'') {
922 if let Some(end) = rest.find('\'') {
934 unused_vars.insert(rest[..end].to_string());
923 unused_vars.insert(rest[..end].to_string());
935 }
924 }
936 }
925 }
937− }
938− if d.message.contains("unused mixin '") {
926+ if d.message.contains("unused mixin '")
939− if let Some(start) = d.message.find("unused mixin '") {
927+ && let Some(start) = d.message.find("unused mixin '") {
940 let rest = &d.message[start + 14..];
928 let rest = &d.message[start + 14..];
941 if let Some(end) = rest.find('\'') {
929 if let Some(end) = rest.find('\'') {
942 unused_vars.insert(rest[..end].to_string());
930 unused_vars.insert(rest[..end].to_string());
943 }
931 }
944 }
932 }
945− }
946 }
933 }
947
934
948 for (line_idx, line) in text.lines().enumerate() {
935 for (line_idx, line) in text.lines().enumerate() {
@@ -1023,6 +1010,7 @@ fn is_builtin_element(word: &str) -> bool {
1023 )
1010 )
1024}
1011}
1025
1012
1013+#[allow(clippy::too_many_arguments)]
1026fn push_token(
1014fn push_token(
1027 tokens: &mut Vec<SemanticToken>,
1015 tokens: &mut Vec<SemanticToken>,
1028 prev_line: &mut u32,
1016 prev_line: &mut u32,
@@ -1059,11 +1047,10 @@ pub(crate) fn inlay_hints(text: &str) -> Vec<InlayHint> {
1059 let mut vars: HashMap<&str, &str> = HashMap::new();
1047 let mut vars: HashMap<&str, &str> = HashMap::new();
1060 for line in text.lines() {
1048 for line in text.lines() {
1061 let trimmed = line.trim();
1049 let trimmed = line.trim();
1062− if let Some(rest) = trimmed.strip_prefix("@let ") {
1050+ if let Some(rest) = trimmed.strip_prefix("@let ")
1063− if let Some((name, value)) = rest.trim().split_once(' ') {
1051+ && let Some((name, value)) = rest.trim().split_once(' ') {
1064 vars.insert(name, value.trim());
1052 vars.insert(name, value.trim());
1065 }
1053 }
1066− }
1067 }
1054 }
1068
1055
1069 let mut hints = Vec::new();
1056 let mut hints = Vec::new();
@@ -1168,7 +1155,7 @@ pub(crate) fn get_signature_help(text: &str, position: Position) -> Option<Signa
1168 };
1155 };
1169 ParameterInformation {
1156 ParameterInformation {
1170 label: ParameterLabel::Simple(label),
1157 label: ParameterLabel::Simple(label),
1171− documentation: doc.map(|d| Documentation::String(d)),
1158+ documentation: doc.map(Documentation::String),
1172 }
1159 }
1173 }).collect();
1160 }).collect();
1174
1161
src/bin/htmlang_lsp/completion.rs+11−14
@@ -386,7 +386,7 @@ pub(crate) fn path_completions(uri: &Url, position: Position) -> Vec<CompletionI
386 None => return vec![],
386 None => return vec![],
387 };
387 };
388
388
389− let col = position.character as u32;
389+ let col = position.character;
390 let edit_range = Range::new(Position::new(position.line, col), Position::new(position.line, col));
390 let edit_range = Range::new(Position::new(position.line, col), Position::new(position.line, col));
391
391
392 let entries = match std::fs::read_dir(dir) {
392 let entries = match std::fs::read_dir(dir) {
@@ -397,8 +397,8 @@ pub(crate) fn path_completions(uri: &Url, position: Position) -> Vec<CompletionI
397 let mut items = Vec::new();
397 let mut items = Vec::new();
398 for entry in entries.flatten() {
398 for entry in entries.flatten() {
399 let path = entry.path();
399 let path = entry.path();
400− if path.extension().and_then(|e| e.to_str()) == Some("hl") {
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()) {
401+ && let Some(name) = path.file_name().and_then(|n| n.to_str()) {
402 // Skip the current file itself
402 // Skip the current file itself
403 if Some(name) == file_path.file_name().and_then(|n| n.to_str()) {
403 if Some(name) == file_path.file_name().and_then(|n| n.to_str()) {
404 continue;
404 continue;
@@ -414,7 +414,6 @@ pub(crate) fn path_completions(uri: &Url, position: Position) -> Vec<CompletionI
414 ..Default::default()
414 ..Default::default()
415 });
415 });
416 }
416 }
417− }
418 }
417 }
419 items
418 items
420}
419}
@@ -432,9 +431,9 @@ pub(crate) fn use_symbol_completions(uri: &Url, line: &str, position: Position)
432
431
433 // Extract the filename from @use "file.hl" or @use file.hl
432 // Extract the filename from @use "file.hl" or @use file.hl
434 let after_use = &line.trim_start()[5..]; // skip "@use "
433 let after_use = &line.trim_start()[5..]; // skip "@use "
435− let filename = if after_use.starts_with('"') {
434+ let filename = if let Some(after_quote) = after_use.strip_prefix('"') {
436− let end = after_use[1..].find('"').map(|i| i + 1).unwrap_or(after_use.len());
435+ let end = after_quote.find('"').unwrap_or(after_quote.len());
437− &after_use[1..end]
436+ &after_quote[..end]
438 } else {
437 } else {
439 after_use.split_whitespace().next().unwrap_or("")
438 after_use.split_whitespace().next().unwrap_or("")
440 };
439 };
@@ -449,7 +448,7 @@ pub(crate) fn use_symbol_completions(uri: &Url, line: &str, position: Position)
449 Err(_) => return vec![],
448 Err(_) => return vec![],
450 };
449 };
451
450
452− let col = position.character as u32;
451+ let col = position.character;
453 let edit_range = Range::new(Position::new(position.line, col), Position::new(position.line, col));
452 let edit_range = Range::new(Position::new(position.line, col), Position::new(position.line, col));
454
453
455 let mut items = Vec::new();
454 let mut items = Vec::new();
@@ -489,8 +488,8 @@ pub(crate) fn use_symbol_completions(uri: &Url, line: &str, position: Position)
489 ..Default::default()
488 ..Default::default()
490 });
489 });
491 }
490 }
492− } else if let Some(rest) = trimmed.strip_prefix("@component ") {
491+ } else if let Some(rest) = trimmed.strip_prefix("@component ")
493− if let Some(name) = rest.split_whitespace().next() {
492+ && let Some(name) = rest.split_whitespace().next() {
494 items.push(CompletionItem {
493 items.push(CompletionItem {
495 label: name.to_string(),
494 label: name.to_string(),
496 kind: Some(CompletionItemKind::CLASS),
495 kind: Some(CompletionItemKind::CLASS),
@@ -502,7 +501,6 @@ pub(crate) fn use_symbol_completions(uri: &Url, line: &str, position: Position)
502 ..Default::default()
501 ..Default::default()
503 });
502 });
504 }
503 }
505− }
506 }
504 }
507
505
508 items
506 items
@@ -986,8 +984,8 @@ fn variable_completions(text: &str, range: Range) -> Vec<CompletionItem> {
986 range,
984 range,
987 ));
985 ));
988 }
986 }
989− } else if let Some(rest) = trimmed.strip_prefix("@define ") {
987+ } else if let Some(rest) = trimmed.strip_prefix("@define ")
990− if let Some(bracket) = rest.find('[') {
988+ && let Some(bracket) = rest.find('[') {
991 let name = rest[..bracket].trim();
989 let name = rest[..bracket].trim();
992 items.push(item(
990 items.push(item(
993 &format!("${}", name),
991 &format!("${}", name),
@@ -997,7 +995,6 @@ fn variable_completions(text: &str, range: Range) -> Vec<CompletionItem> {
997 range,
995 range,
998 ));
996 ));
999 }
997 }
1000− }
1001 }
998 }
1002
999
1003 items
1000 items
src/bin/htmlang_lsp/hover.rs+11−14
@@ -6,10 +6,10 @@ pub(crate) fn hover_at(text: &str, position: Position) -> Option<Hover> {
6 let col = (position.character as usize).min(line.len());
6 let col = (position.character as usize).min(line.len());
7 let word = word_at(line, col)?;
7 let word = word_at(line, col)?;
8
8
9− let doc = if word.starts_with('$') {
9+ let doc = if let Some(var_name) = word.strip_prefix('$') {
10− hover_variable(text, &word[1..])
10+ hover_variable(text, var_name)
11− } else if word.starts_with('@') {
11+ } else if let Some(fn_name) = word.strip_prefix('@') {
12− hover_user_fn(text, &word[1..]).or_else(|| hover_builtin(&word))
12+ hover_user_fn(text, fn_name).or_else(|| hover_builtin(&word))
13 } else {
13 } else {
14 hover_builtin(&word)
14 hover_builtin(&word)
15 }?;
15 }?;
@@ -46,27 +46,24 @@ pub(crate) fn is_word_byte(c: u8) -> bool {
46fn hover_variable(text: &str, name: &str) -> Option<String> {
46fn hover_variable(text: &str, name: &str) -> Option<String> {
47 for line in text.lines() {
47 for line in text.lines() {
48 let trimmed = line.trim();
48 let trimmed = line.trim();
49− if let Some(rest) = trimmed.strip_prefix("@let ") {
49+ if let Some(rest) = trimmed.strip_prefix("@let ")
50− if let Some((n, v)) = rest.trim().split_once(' ') {
50+ && let Some((n, v)) = rest.trim().split_once(' ')
51− if n == name {
51+ && n == name {
52 return Some(format!("**${}** = `{}`", name, v.trim()));
52 return Some(format!("**${}** = `{}`", name, v.trim()));
53 }
53 }
54− }
55− }
56 }
54 }
57
55
58 for line in text.lines() {
56 for line in text.lines() {
59 let trimmed = line.trim();
57 let trimmed = line.trim();
60 if let Some(rest) = trimmed.strip_prefix("@define ") {
58 if let Some(rest) = trimmed.strip_prefix("@define ") {
61 let rest = rest.trim();
59 let rest = rest.trim();
62− if let Some(bracket) = rest.find('[') {
60+ if let Some(bracket) = rest.find('[')
63− if rest[..bracket].trim() == name {
61+ && rest[..bracket].trim() == name {
64 return Some(format!(
62 return Some(format!(
65 "**${}** \u{2014} Attribute bundle\n\n`{}`",
63 "**${}** \u{2014} Attribute bundle\n\n`{}`",
66 name, trimmed
64 name, trimmed
67 ));
65 ));
68 }
66 }
69− }
70 }
67 }
71 }
68 }
72
69
@@ -108,8 +105,8 @@ fn hover_user_fn(text: &str, name: &str) -> Option<String> {
108 let prev = lines[j].trim();
105 let prev = lines[j].trim();
109 if let Some(comment) = prev.strip_prefix("-- ") {
106 if let Some(comment) = prev.strip_prefix("-- ") {
110 doc_lines.push(comment);
107 doc_lines.push(comment);
111− } else if prev.starts_with("--") {
108+ } else if let Some(comment) = prev.strip_prefix("--") {
112− doc_lines.push(&prev[2..]);
109+ doc_lines.push(comment);
113 } else {
110 } else {
114 break;
111 break;
115 }
112 }
src/bin/htmlang_lsp/main.rs+7−10
@@ -203,8 +203,7 @@ impl LanguageServer for Backend {
203 }
203 }
204 }
204 }
205 // @use "file.hl" fn1, fn2 -- after the filename, suggest exported @fn names
205 // @use "file.hl" fn1, fn2 -- after the filename, suggest exported @fn names
206− if trimmed.starts_with("@use ") {
206+ if let Some(after_use) = trimmed.strip_prefix("@use ") {
207− let after_use = &trimmed[5..];
208 // If we already have a filename (quoted or unquoted), suggest symbols from that file
207 // If we already have a filename (quoted or unquoted), suggest symbols from that file
209 let has_file = after_use.contains(".hl");
208 let has_file = after_use.contains(".hl");
210 if has_file {
209 if has_file {
@@ -462,20 +461,19 @@ impl LanguageServer for Backend {
462 }
461 }
463 let path = entry.path();
462 let path = entry.path();
464 // Skip hidden / vendor / build directories.
463 // Skip hidden / vendor / build directories.
465− if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
464+ if let Some(name) = path.file_name().and_then(|n| n.to_str())
466− if name.starts_with('.')
465+ && (name.starts_with('.')
467 || name == "target"
466 || name == "target"
468 || name == "node_modules"
467 || name == "node_modules"
469− || name == "dist"
468+ || name == "dist")
470 {
469 {
471 continue;
470 continue;
472 }
471 }
473− }
474 if path.is_dir() {
472 if path.is_dir() {
475 stack.push(path);
473 stack.push(path);
476 continue;
474 continue;
477 }
475 }
478− if path.extension().map_or(false, |e| e == "hl")
476+ if path.extension().is_some_and(|e| e == "hl")
479 && !covered_files.contains(&path)
477 && !covered_files.contains(&path)
480 {
478 {
481 scanned += 1;
479 scanned += 1;
@@ -691,14 +689,13 @@ impl LanguageServer for Backend {
691 if let Some((n, _)) = rest.trim().split_once(' ') {
689 if let Some((n, _)) = rest.trim().split_once(' ') {
692 defs.push(Def { line: i as u32, name: n.to_string(), kind: "let" });
690 defs.push(Def { line: i as u32, name: n.to_string(), kind: "let" });
693 }
691 }
694− } else if let Some(rest) = trimmed.strip_prefix("@define ") {
692+ } else if let Some(rest) = trimmed.strip_prefix("@define ")
695− if let Some(bracket) = rest.find('[') {
693+ && let Some(bracket) = rest.find('[') {
696 let n = rest[..bracket].trim();
694 let n = rest[..bracket].trim();
697 if !n.is_empty() {
695 if !n.is_empty() {
698 defs.push(Def { line: i as u32, name: n.to_string(), kind: "define" });
696 defs.push(Def { line: i as u32, name: n.to_string(), kind: "define" });
699 }
697 }
700 }
698 }
701− }
702 }
699 }
703
700
704 let mut lenses = Vec::with_capacity(defs.len());
701 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
37 let word = word_at(line, col)?;
37 let word = word_at(line, col)?;
38
38
39 // Find definition location
39 // Find definition location
40− let (def_line, def_col, def_len) = if word.starts_with('$') {
40+ let (def_line, def_col, def_len) = if let Some(name) = word.strip_prefix('$') {
41− let name = &word[1..];
42 find_definition(text, name)?
41 find_definition(text, name)?
43− } else if word.starts_with('@') {
44− let name = &word[1..];
42+ } else if let Some(name) = word.strip_prefix('@') {
45 find_fn_definition(text, name)?
43 find_fn_definition(text, name)?
46 } else {
44 } else {
47 return None;
45 return None;
@@ -62,14 +60,12 @@ pub(crate) fn find_definition(text: &str, name: &str) -> Option<(u32, u32, u32)>
62 let trimmed = line.trim();
60 let trimmed = line.trim();
63 let offset = (line.len() - trimmed.len()) as u32;
61 let offset = (line.len() - trimmed.len()) as u32;
64
62
65− if let Some(rest) = trimmed.strip_prefix("@let ") {
63+ if let Some(rest) = trimmed.strip_prefix("@let ")
66− if let Some((n, _)) = rest.trim().split_once(' ') {
64+ && let Some((n, _)) = rest.trim().split_once(' ')
67− if n == name {
65+ && n == name {
68 let col = offset + "@let ".len() as u32;
66 let col = offset + "@let ".len() as u32;
69 return Some((i as u32, col, n.len() as u32));
67 return Some((i as u32, col, n.len() as u32));
70 }
68 }
71− }
72− }
73
69
74 if let Some(rest) = trimmed.strip_prefix("@define ") {
70 if let Some(rest) = trimmed.strip_prefix("@define ") {
75 let rest = rest.trim();
71 let rest = rest.trim();
@@ -179,10 +175,10 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
179
175
180 if is_var {
176 if is_var {
181 // Rename @let definition
177 // Rename @let definition
182− if let Some(rest) = trimmed.strip_prefix("@let ") {
178+ if let Some(rest) = trimmed.strip_prefix("@let ")
183− if let Some((n, _)) = rest.trim().split_once(' ') {
179+ && let Some((n, _)) = rest.trim().split_once(' ')
184− if n == name {
180+ && n == name
185− if let Some(pos) = line.find(n) {
181+ && let Some(pos) = line.find(n) {
186 edits.push(TextEdit {
182 edits.push(TextEdit {
187 range: Range::new(
183 range: Range::new(
188 Position::new(line_num, pos as u32),
184 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
191 new_text: new_base.to_string(),
187 new_text: new_base.to_string(),
192 });
188 });
193 }
189 }
194− }
195− }
196− }
197
190
198 // Rename @define definition
191 // Rename @define definition
199 if let Some(rest) = trimmed.strip_prefix("@define ") {
192 if let Some(rest) = trimmed.strip_prefix("@define ") {
200 let rest_trimmed = rest.trim();
193 let rest_trimmed = rest.trim();
201 if let Some(bracket) = rest_trimmed.find('[') {
194 if let Some(bracket) = rest_trimmed.find('[') {
202 let n = rest_trimmed[..bracket].trim();
195 let n = rest_trimmed[..bracket].trim();
203− if n == name {
196+ if n == name
204− if let Some(pos) = line.find(n) {
197+ && let Some(pos) = line.find(n) {
205 edits.push(TextEdit {
198 edits.push(TextEdit {
206 range: Range::new(
199 range: Range::new(
207 Position::new(line_num, pos as u32),
200 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
211 });
204 });
212 continue; // Don't also match $ references on this line
205 continue; // Don't also match $ references on this line
213 }
206 }
214− }
215 }
207 }
216 }
208 }
217
209
@@ -220,8 +212,8 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
220 let parts: Vec<&str> = rest.split_whitespace().collect();
212 let parts: Vec<&str> = rest.split_whitespace().collect();
221 for param in &parts[1..] {
213 for param in &parts[1..] {
222 let p = param.strip_prefix('$').unwrap_or(param);
214 let p = param.strip_prefix('$').unwrap_or(param);
223− if p == name {
215+ if p == name
224− if let Some(pos) = line.find(param) {
216+ && let Some(pos) = line.find(param) {
225 let prefix = if param.starts_with('$') { "$" } else { "" };
217 let prefix = if param.starts_with('$') { "$" } else { "" };
226 edits.push(TextEdit {
218 edits.push(TextEdit {
227 range: Range::new(
219 range: Range::new(
@@ -231,7 +223,6 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
231 new_text: format!("{}{}", prefix, new_base),
223 new_text: format!("{}{}", prefix, new_base),
232 });
224 });
233 }
225 }
234− }
235 }
226 }
236 }
227 }
237
228
@@ -262,8 +253,8 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
262 // Rename @fn definition
253 // Rename @fn definition
263 if let Some(rest) = trimmed.strip_prefix("@fn ") {
254 if let Some(rest) = trimmed.strip_prefix("@fn ") {
264 let parts: Vec<&str> = rest.split_whitespace().collect();
255 let parts: Vec<&str> = rest.split_whitespace().collect();
265− if parts.first() == Some(&name) {
256+ if parts.first() == Some(&name)
266− if let Some(pos) = line.find(&format!("@fn {}", name)) {
257+ && let Some(pos) = line.find(&format!("@fn {}", name)) {
267 let start = pos + 4; // skip "@fn "
258 let start = pos + 4; // skip "@fn "
268 edits.push(TextEdit {
259 edits.push(TextEdit {
269 range: Range::new(
260 range: Range::new(
@@ -273,7 +264,6 @@ pub(crate) fn rename_at(text: &str, position: Position, new_name: &str, uri: &Ur
273 new_text: new_base.to_string(),
264 new_text: new_base.to_string(),
274 });
265 });
275 }
266 }
276− }
277 }
267 }
278
268
279 // Rename @name function calls
269 // Rename @name function calls
src/convert.rs+37−55
@@ -86,8 +86,8 @@ impl<'a> HtmlParser<'a> {
86 break;
86 break;
87 }
87 }
88 // Check for closing tag
88 // Check for closing tag
89− if let Some(stop) = stop_tag {
89+ if let Some(stop) = stop_tag
90− if self.starts_with("</") {
90+ && self.starts_with("</") {
91 let saved = self.pos;
91 let saved = self.pos;
92 self.pos += 2;
92 self.pos += 2;
93 let tag = self.parse_tag_name();
93 let tag = self.parse_tag_name();
@@ -102,7 +102,6 @@ impl<'a> HtmlParser<'a> {
102 // parent handle it.
102 // parent handle it.
103 break;
103 break;
104 }
104 }
105− }
106
105
107 if self.starts_with("<!--") {
106 if self.starts_with("<!--") {
108 self.pos += 4;
107 self.pos += 4;
@@ -356,7 +355,10 @@ fn strip_px_multi(v: &str) -> String {
356/// Converts CSS properties to htmlang attribute key-value pairs.
355/// Converts CSS properties to htmlang attribute key-value pairs.
357/// Returns (attrs, leftover_css) where leftover_css is anything we
356/// Returns (attrs, leftover_css) where leftover_css is anything we
358/// could not convert to a known htmlang attribute.
357/// could not convert to a known htmlang attribute.
358+type HlAttr = (String, Option<String>);
359+type CssDecl = (String, String);
360+
359−fn css_to_hl_attrs(styles: &[(String, String)]) -> (Vec<(String, Option<String>)>, Vec<(String, String)>) {
361+fn css_to_hl_attrs(styles: &[CssDecl]) -> (Vec<HlAttr>, Vec<CssDecl>) {
360 let mut attrs: Vec<(String, Option<String>)> = Vec::new();
362 let mut attrs: Vec<(String, Option<String>)> = Vec::new();
361 let mut leftover: Vec<(String, String)> = Vec::new();
363 let mut leftover: Vec<(String, String)> = Vec::new();
362
364
@@ -387,36 +389,30 @@ fn css_to_hl_attrs(styles: &[(String, String)]) -> (Vec<(String, Option<String>)
387 }
389 }
388 "padding-left" | "padding-right" => {
390 "padding-left" | "padding-right" => {
389 // Check for padding-x pattern
391 // Check for padding-x pattern
390− if key == "padding-left" {
392+ if key == "padding-left"
391− if let Some(&pr) = style_map.get("padding-right") {
393+ && let Some(&pr) = style_map.get("padding-right")
392− if pr == val {
394+ && pr == val {
393 attrs.push(("padding-x".into(), Some(strip_px(val).into())));
395 attrs.push(("padding-x".into(), Some(strip_px(val).into())));
394 continue;
396 continue;
395 }
397 }
396− }
397− }
398− if key == "padding-right" {
398+ if key == "padding-right"
399− if style_map.get("padding-left") == Some(&val.as_str()) {
399+ && style_map.get("padding-left") == Some(&val.as_str()) {
400 // Already emitted as padding-x
400 // Already emitted as padding-x
401 continue;
401 continue;
402 }
402 }
403− }
404 leftover.push((key.clone(), val.clone()));
403 leftover.push((key.clone(), val.clone()));
405 }
404 }
406 "padding-top" | "padding-bottom" => {
405 "padding-top" | "padding-bottom" => {
407− if key == "padding-top" {
406+ if key == "padding-top"
408− if let Some(&pb) = style_map.get("padding-bottom") {
407+ && let Some(&pb) = style_map.get("padding-bottom")
409− if pb == val {
408+ && pb == val {
410 attrs.push(("padding-y".into(), Some(strip_px(val).into())));
409 attrs.push(("padding-y".into(), Some(strip_px(val).into())));
411 continue;
410 continue;
412 }
411 }
413− }
414− }
415− if key == "padding-bottom" {
412+ if key == "padding-bottom"
416− if style_map.get("padding-top") == Some(&val.as_str()) {
413+ && style_map.get("padding-top") == Some(&val.as_str()) {
417 continue;
414 continue;
418 }
415 }
419− }
420 leftover.push((key.clone(), val.clone()));
416 leftover.push((key.clone(), val.clone()));
421 }
417 }
422 "padding-inline" => {
418 "padding-inline" => {
@@ -430,35 +426,29 @@ fn css_to_hl_attrs(styles: &[(String, String)]) -> (Vec<(String, Option<String>)
430 attrs.push(("margin".into(), Some(strip_px_multi(val))));
426 attrs.push(("margin".into(), Some(strip_px_multi(val))));
431 }
427 }
432 "margin-left" | "margin-right" => {
428 "margin-left" | "margin-right" => {
433− if key == "margin-left" {
429+ if key == "margin-left"
434− if let Some(&mr) = style_map.get("margin-right") {
430+ && let Some(&mr) = style_map.get("margin-right")
435− if mr == val {
431+ && mr == val {
436 attrs.push(("margin-x".into(), Some(strip_px(val).into())));
432 attrs.push(("margin-x".into(), Some(strip_px(val).into())));
437 continue;
433 continue;
438 }
434 }
439− }
440− }
441− if key == "margin-right" {
435+ if key == "margin-right"
442− if style_map.get("margin-left") == Some(&val.as_str()) {
436+ && style_map.get("margin-left") == Some(&val.as_str()) {
443 continue;
437 continue;
444 }
438 }
445− }
446 leftover.push((key.clone(), val.clone()));
439 leftover.push((key.clone(), val.clone()));
447 }
440 }
448 "margin-top" | "margin-bottom" => {
441 "margin-top" | "margin-bottom" => {
449− if key == "margin-top" {
442+ if key == "margin-top"
450− if let Some(&mb) = style_map.get("margin-bottom") {
443+ && let Some(&mb) = style_map.get("margin-bottom")
451− if mb == val {
444+ && mb == val {
452 attrs.push(("margin-y".into(), Some(strip_px(val).into())));
445 attrs.push(("margin-y".into(), Some(strip_px(val).into())));
453 continue;
446 continue;
454 }
447 }
455− }
456− }
457− if key == "margin-bottom" {
448+ if key == "margin-bottom"
458− if style_map.get("margin-top") == Some(&val.as_str()) {
449+ && style_map.get("margin-top") == Some(&val.as_str()) {
459 continue;
450 continue;
460 }
451 }
461− }
462 leftover.push((key.clone(), val.clone()));
452 leftover.push((key.clone(), val.clone()));
463 }
453 }
464 "margin-inline" => {
454 "margin-inline" => {
@@ -677,16 +667,13 @@ fn unwrap_boilerplate(nodes: &[HtmlNode]) -> Vec<&HtmlNode> {
677 .collect();
667 .collect();
678
668
679 // If there's a single <html> element, unwrap it
669 // If there's a single <html> element, unwrap it
680− if meaningful.len() == 1 {
670+ if meaningful.len() == 1
681− if let HtmlNode::Element {
671+ && let HtmlNode::Element {
682 tag, children, ..
672 tag, children, ..
683 } = meaningful[0]
673 } = meaningful[0]
684− {
685− if tag == "html" {
674+ && tag == "html" {
686 return unwrap_html_children(children);
675 return unwrap_html_children(children);
687 }
676 }
688− }
689− }
690
677
691 // Otherwise, check if there are any html/head/body tags mixed in
678 // Otherwise, check if there are any html/head/body tags mixed in
692 let mut result = Vec::new();
679 let mut result = Vec::new();
@@ -950,13 +937,11 @@ fn emit_element(
950 }
937 }
951
938
952 // Alt text for images
939 // Alt text for images
953− if tag == "img" {
940+ if tag == "img"
954− if let Some(alt_text) = &alt {
941+ && let Some(alt_text) = &alt
955− if !alt_text.is_empty() {
942+ && !alt_text.is_empty() {
956 hl_attrs.push(("alt".into(), Some(alt_text.clone())));
943 hl_attrs.push(("alt".into(), Some(alt_text.clone())));
957 }
944 }
958− }
959− }
960
945
961 // Passthrough HTML attributes that htmlang supports
946 // Passthrough HTML attributes that htmlang supports
962 for (key, val) in attrs {
947 for (key, val) in attrs {
@@ -996,12 +981,11 @@ fn emit_element(
996 out.push(' ');
981 out.push(' ');
997 out.push_str(url);
982 out.push_str(url);
998 }
983 }
999− } else if tag == "img" {
984+ } else if tag == "img"
1000− if let Some(src_val) = &src {
985+ && let Some(src_val) = &src {
1001 out.push(' ');
986 out.push(' ');
1002 out.push_str(src_val);
987 out.push_str(src_val);
1003 }
988 }
1004− }
1005
989
1006 // Attributes block
990 // Attributes block
1007 if !hl_attrs.is_empty() {
991 if !hl_attrs.is_empty() {
@@ -1041,17 +1025,15 @@ fn emit_element(
1041 }
1025 }
1042
1026
1043 // Alt text for images
1027 // Alt text for images
1044− if tag == "img" {
1028+ if tag == "img"
1045− if let Some(alt_text) = &alt {
1029+ && let Some(alt_text) = &alt
1046− if !alt_text.is_empty() {
1030+ && !alt_text.is_empty() {
1047 // Alt is emitted as an attribute already? No -- let's add it.
1031 // Alt is emitted as an attribute already? No -- let's add it.
1048 // Actually we should add it to hl_attrs above. But since we
1032 // Actually we should add it to hl_attrs above. But since we
1049 // already flushed the line, add a comment.
1033 // already flushed the line, add a comment.
1050 // Re-check: the image alt is typically an attribute.
1034 // Re-check: the image alt is typically an attribute.
1051 // We'll add it inline. Let's fix this by checking above.
1035 // We'll add it inline. Let's fix this by checking above.
1052 }
1036 }
1053− }
1054− }
1055
1037
1056 // Emit children
1038 // Emit children
1057 for child in children {
1039 for child in children {
src/fmt.rs+5−7
@@ -92,7 +92,7 @@ fn sort_attrs(attrs_str: &str) -> String {
92 let mut parts: Vec<&str> = attrs_str.split(',').collect();
92 let mut parts: Vec<&str> = attrs_str.split(',').collect();
93 // Preserve order within same category (stable sort)
93 // Preserve order within same category (stable sort)
94 parts.sort_by_key(|part| {
94 parts.sort_by_key(|part| {
95− let key = part.trim().split_whitespace().next().unwrap_or("");
95+ let key = part.split_whitespace().next().unwrap_or("");
96 attr_category(key)
96 attr_category(key)
97 });
97 });
98 parts.iter()
98 parts.iter()
@@ -224,13 +224,12 @@ pub fn format(input: &str) -> String {
224 || (bracket_was_wrapped
224 || (bracket_was_wrapped
225 && attrs_count > 1
225 && attrs_count > 1
226 && indented_len > WRAP_MIN_WIDTH);
226 && indented_len > WRAP_MIN_WIDTH);
227− if should_wrap {
227+ if should_wrap
228− if let Some(wrapped) = wrap_attrs(&formatted, level) {
228+ && let Some(wrapped) = wrap_attrs(&formatted, level) {
229 output.push_str(&wrapped);
229 output.push_str(&wrapped);
230 bracket_was_wrapped = false;
230 bracket_was_wrapped = false;
231 continue;
231 continue;
232 }
232 }
233− }
234 output.push_str(&" ".repeat(level));
233 output.push_str(&" ".repeat(level));
235 output.push_str(&formatted);
234 output.push_str(&formatted);
236 if let Some(cmt) = pending_comment.take() {
235 if let Some(cmt) = pending_comment.take() {
@@ -275,8 +274,8 @@ pub fn format(input: &str) -> String {
275
274
276 let indented_len = level * 2 + formatted.len();
275 let indented_len = level * 2 + formatted.len();
277 // Single-line emission — only wrap when we strictly exceed the ceiling.
276 // Single-line emission — only wrap when we strictly exceed the ceiling.
278− if indented_len > MAX_LINE_WIDTH && formatted.contains('[') {
277+ if indented_len > MAX_LINE_WIDTH && formatted.contains('[')
279− if let Some(mut wrapped) = wrap_attrs(&formatted, level) {
278+ && let Some(mut wrapped) = wrap_attrs(&formatted, level) {
280 if let Some(cmt) = &trailing {
279 if let Some(cmt) = &trailing {
281 // Reattach comment on the final `]` line.
280 // Reattach comment on the final `]` line.
282 if wrapped.ends_with('\n') {
281 if wrapped.ends_with('\n') {
@@ -290,7 +289,6 @@ pub fn format(input: &str) -> String {
290 indent_stack.push(raw_indent);
289 indent_stack.push(raw_indent);
291 continue;
290 continue;
292 }
291 }
293− }
294 let _ = attrs_count;
292 let _ = attrs_count;
295
293
296 output.push_str(&" ".repeat(level));
294 output.push_str(&" ".repeat(level));
src/main.rs+65−92
@@ -218,8 +218,8 @@ fn bundle_assets(html: &str, base_dir: &Path) -> String {
218 && !path_str.is_empty()
218 && !path_str.is_empty()
219 {
219 {
220 let asset_path = base_dir.join(path_str);
220 let asset_path = base_dir.join(path_str);
221− if asset_path.exists() {
221+ if asset_path.exists()
222− if let Ok(mut file) = std::fs::File::open(&asset_path) {
222+ && let Ok(mut file) = std::fs::File::open(&asset_path) {
223 let mut buf = Vec::new();
223 let mut buf = Vec::new();
224 if file.read_to_end(&mut buf).is_ok() {
224 if file.read_to_end(&mut buf).is_ok() {
225 let mime = match asset_path.extension().and_then(|e| e.to_str()) {
225 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 {
246 continue;
246 continue;
247 }
247 }
248 }
248 }
249− }
250 }
249 }
251 // If we couldn't inline, keep original path
250 // If we couldn't inline, keep original path
252 output.push_str(&remaining[..end]);
251 output.push_str(&remaining[..end]);
@@ -261,7 +260,7 @@ fn bundle_assets(html: &str, base_dir: &Path) -> String {
261
260
262fn base64_encode(data: &[u8]) -> String {
261fn base64_encode(data: &[u8]) -> String {
263 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
262 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);
265 for chunk in data.chunks(3) {
264 for chunk in data.chunks(3) {
266 let b0 = chunk[0] as u32;
265 let b0 = chunk[0] as u32;
267 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
266 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) {
293 let path = entry.path();
292 let path = entry.path();
294 if path.is_dir() {
293 if path.is_dir() {
295 copy_non_hl_recursive(base, &path, out_dir);
294 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") {
297 let rel = path.strip_prefix(base).unwrap_or(&path);
296 let rel = path.strip_prefix(base).unwrap_or(&path);
298 let dest = out_dir.join(rel);
297 let dest = out_dir.join(rel);
299 if let Some(parent) = dest.parent() {
298 if let Some(parent) = dest.parent() {
@@ -362,12 +361,11 @@ h1{{color:#ff6b6b;margin-bottom:1rem;font-size:1.5rem}}
362
361
363fn init_project(dir: &str, template_name: Option<&str>) {
362fn init_project(dir: &str, template_name: Option<&str>) {
364 let dir = Path::new(dir);
363 let dir = Path::new(dir);
365− if dir.to_str() != Some(".") {
364+ if dir.to_str() != Some(".")
366− if let Err(e) = fs::create_dir_all(dir) {
365+ && let Err(e) = fs::create_dir_all(dir) {
367 eprintln!("error: cannot create directory '{}': {}", dir.display(), e);
366 eprintln!("error: cannot create directory '{}': {}", dir.display(), e);
368 process::exit(1);
367 process::exit(1);
369 }
368 }
370− }
371
369
372 let index_path = dir.join("index.hl");
370 let index_path = dir.join("index.hl");
373 if index_path.exists() {
371 if index_path.exists() {
@@ -501,7 +499,7 @@ fn collect_hl_recursive_inner(dir: &Path, files: &mut Vec<PathBuf>) {
501 let path = entry.path();
499 let path = entry.path();
502 if path.is_dir() {
500 if path.is_dir() {
503 collect_hl_recursive_inner(&path, files);
501 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") {
505 files.push(path);
503 files.push(path);
506 }
504 }
507 }
505 }
@@ -627,28 +625,26 @@ fn lint_nodes(nodes: &[htmlang::ast::Node], path: &str, depth: usize, warnings:
627 }
625 }
628
626
629 // @image without alt
627 // @image without alt
630− if elem.kind == htmlang::ast::ElementKind::Image {
628+ if elem.kind == htmlang::ast::ElementKind::Image
631− if !elem.attrs.iter().any(|a| a.key == "alt") {
629+ && !elem.attrs.iter().any(|a| a.key == "alt") {
632 warnings.push(format!("{}:{}:lint: @image missing 'alt' attribute (accessibility)", path, elem.line_num));
630 warnings.push(format!("{}:{}:lint: @image missing 'alt' attribute (accessibility)", path, elem.line_num));
633 }
631 }
634− }
635
632
636 // @link without content or aria-label
633 // @link without content or aria-label
637 if elem.kind == htmlang::ast::ElementKind::Link {
634 if elem.kind == htmlang::ast::ElementKind::Link {
638 let has_aria = elem.attrs.iter().any(|a| a.key == "aria-label");
635 let has_aria = elem.attrs.iter().any(|a| a.key == "aria-label");
639 let has_children = !elem.children.is_empty();
636 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);
641 if !has_aria && !has_children && !has_arg_text {
638 if !has_aria && !has_children && !has_arg_text {
642 warnings.push(format!("{}:{}:lint: @link has no visible text or aria-label (accessibility)", path, elem.line_num));
639 warnings.push(format!("{}:{}:lint: @link has no visible text or aria-label (accessibility)", path, elem.line_num));
643 }
640 }
644 }
641 }
645
642
646 // @input without type
643 // @input without type
647− if elem.kind == htmlang::ast::ElementKind::Input {
644+ if elem.kind == htmlang::ast::ElementKind::Input
648− if !elem.attrs.iter().any(|a| a.key == "type") {
645+ && !elem.attrs.iter().any(|a| a.key == "type") {
649 warnings.push(format!("{}:{}:lint: @input missing 'type' attribute", path, elem.line_num));
646 warnings.push(format!("{}:{}:lint: @input missing 'type' attribute", path, elem.line_num));
650 }
647 }
651− }
652
648
653 // Empty containers (no children, no text)
649 // Empty containers (no children, no text)
654 if matches!(elem.kind,
650 if matches!(elem.kind,
@@ -664,11 +660,10 @@ fn lint_nodes(nodes: &[htmlang::ast::Node], path: &str, depth: usize, warnings:
664 }
660 }
665
661
666 // @button without type
662 // @button without type
667− if elem.kind == htmlang::ast::ElementKind::Button {
663+ if elem.kind == htmlang::ast::ElementKind::Button
668− if !elem.attrs.iter().any(|a| a.key == "type") {
664+ && !elem.attrs.iter().any(|a| a.key == "type") {
669 warnings.push(format!("{}:{}:lint: @button missing 'type' attribute (defaults to submit)", path, elem.line_num));
665 warnings.push(format!("{}:{}:lint: @button missing 'type' attribute (defaults to submit)", path, elem.line_num));
670 }
666 }
671− }
672
667
673 lint_nodes(&elem.children, path, depth + 1, warnings);
668 lint_nodes(&elem.children, path, depth + 1, warnings);
674 }
669 }
@@ -736,17 +731,15 @@ fn count_elements(
736 for attr in &elem.attrs {
731 for attr in &elem.attrs {
737 let key = attr.key.as_str();
732 let key = attr.key.as_str();
738 // Strip pseudo/media prefixes for color/font detection
733 // Strip pseudo/media prefixes for color/font detection
739− let base_key = key.split(':').last().unwrap_or(key);
734+ let base_key = key.split(':').next_back().unwrap_or(key);
740− if matches!(base_key, "color" | "background") {
735+ if matches!(base_key, "color" | "background")
741− if let Some(ref v) = attr.value {
736+ && let Some(ref v) = attr.value {
742 colors.insert(v.clone());
737 colors.insert(v.clone());
743 }
738 }
744− }
745− if base_key == "font" {
739+ if base_key == "font"
746− if let Some(ref v) = attr.value {
740+ && let Some(ref v) = attr.value {
747 fonts.insert(v.clone());
741 fonts.insert(v.clone());
748 }
742 }
749− }
750 }
743 }
751 count_elements(&elem.children, count, colors, fonts);
744 count_elements(&elem.children, count, colors, fonts);
752 }
745 }
@@ -761,8 +754,8 @@ fn extract_shared_css(html_files: &[PathBuf], out_dir: &Path) {
761 for file in html_files {
754 for file in html_files {
762 if let Ok(html) = fs::read_to_string(file) {
755 if let Ok(html) = fs::read_to_string(file) {
763 // Extract CSS between <style> and </style>
756 // Extract CSS between <style> and </style>
764− if let Some(start) = html.find("<style>") {
757+ if let Some(start) = html.find("<style>")
765− if let Some(end) = html[start..].find("</style>") {
758+ && let Some(end) = html[start..].find("</style>") {
766 let css = &html[start + 7..start + end];
759 let css = &html[start + 7..start + end];
767 // Extract individual rules (class-based)
760 // Extract individual rules (class-based)
768 let mut seen_in_file = std::collections::HashSet::new();
761 let mut seen_in_file = std::collections::HashSet::new();
@@ -792,7 +785,6 @@ fn extract_shared_css(html_files: &[PathBuf], out_dir: &Path) {
792 }
785 }
793 }
786 }
794 }
787 }
795− }
796 }
788 }
797 }
789 }
798 // Rules appearing in ALL files are shared
790 // Rules appearing in ALL files are shared
@@ -813,9 +805,9 @@ fn extract_shared_css(html_files: &[PathBuf], out_dir: &Path) {
813
805
814 // Remove shared rules from individual files and inject <link> tag
806 // Remove shared rules from individual files and inject <link> tag
815 for file in html_files {
807 for file in html_files {
816− if let Ok(html) = fs::read_to_string(file) {
808+ if let Ok(html) = fs::read_to_string(file)
817− if let Some(style_start) = html.find("<style>") {
809+ && let Some(style_start) = html.find("<style>")
818− if let Some(style_end_rel) = html[style_start..].find("</style>") {
810+ && let Some(style_end_rel) = html[style_start..].find("</style>") {
819 let css = &html[style_start + 7..style_start + style_end_rel];
811 let css = &html[style_start + 7..style_start + style_end_rel];
820 // Rebuild CSS without shared rules
812 // Rebuild CSS without shared rules
821 let mut filtered = String::new();
813 let mut filtered = String::new();
@@ -853,8 +845,6 @@ fn extract_shared_css(html_files: &[PathBuf], out_dir: &Path) {
853 );
845 );
854 let _ = fs::write(file, new_html);
846 let _ = fs::write(file, new_html);
855 }
847 }
856− }
857− }
858 }
848 }
859}
849}
860
850
@@ -1144,8 +1134,8 @@ fn main() {
1144 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1134 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1145 content.hash(&mut hasher);
1135 content.hash(&mut hasher);
1146 let current_hash = hasher.finish().to_string();
1136 let current_hash = hasher.finish().to_string();
1147− if let Ok(cached_hash) = fs::read_to_string(&hash_path) {
1137+ if let Ok(cached_hash) = fs::read_to_string(&hash_path)
1148− if cached_hash.trim() == current_hash {
1138+ && cached_hash.trim() == current_hash {
1149 // Also verify output exists
1139 // Also verify output exists
1150 let out_exists = effective_out.as_ref().map_or(
1140 let out_exists = effective_out.as_ref().map_or(
1151 file.with_extension("html").exists(),
1141 file.with_extension("html").exists(),
@@ -1156,7 +1146,6 @@ fn main() {
1156 return;
1146 return;
1157 }
1147 }
1158 }
1148 }
1159− }
1160 // Update hash cache after compilation
1149 // Update hash cache after compilation
1161 let _ = fs::write(&hash_path, &current_hash);
1150 let _ = fs::write(&hash_path, &current_hash);
1162 }
1151 }
@@ -1301,7 +1290,7 @@ fn main() {
1301 match args[ci].as_str() {
1290 match args[ci].as_str() {
1302 "--format" => {
1291 "--format" => {
1303 ci += 1;
1292 ci += 1;
1304− if args.get(ci).map_or(false, |v| v == "json") {
1293+ if args.get(ci).is_some_and(|v| v == "json") {
1305 check_format_json = true;
1294 check_format_json = true;
1306 }
1295 }
1307 }
1296 }
@@ -1340,11 +1329,10 @@ fn main() {
1340 });
1329 });
1341 if has_errors { any_errors = true; }
1330 if has_errors { any_errors = true; }
1342 }
1331 }
1343− if check_format_json {
1332+ if check_format_json
1344− if let Some(collector) = json_collector {
1333+ && let Some(collector) = json_collector {
1345 print_json_diagnostics(&collector.lock().unwrap());
1334 print_json_diagnostics(&collector.lock().unwrap());
1346 }
1335 }
1347− }
1348 if any_errors { process::exit(1); }
1336 if any_errors { process::exit(1); }
1349 return;
1337 return;
1350 }
1338 }
@@ -1683,7 +1671,7 @@ fn main() {
1683 });
1671 });
1684 }
1672 }
1685 let (tx, _) = tokio::sync::broadcast::channel::<()>(16);
1673 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());
1687 let index_path = serve_dir.join("index.html");
1675 let index_path = serve_dir.join("index.html");
1688 let server_tx = tx.clone();
1676 let server_tx = tx.clone();
1689 let tls_for_thread = tls_config;
1677 let tls_for_thread = tls_config;
@@ -2036,11 +2024,10 @@ fn main() {
2036 let name = name.trim_end_matches('[');
2024 let name = name.trim_end_matches('[');
2037 all_def_defs.push((name.to_string(), rel.clone(), line_num + 1));
2025 all_def_defs.push((name.to_string(), rel.clone(), line_num + 1));
2038 }
2026 }
2039− } else if let Some(rest) = trimmed.strip_prefix("@let ") {
2027+ } else if let Some(rest) = trimmed.strip_prefix("@let ")
2040− if let Some(name) = rest.split_whitespace().next() {
2028+ && let Some(name) = rest.split_whitespace().next() {
2041 all_let_defs.push((name.to_string(), rel.clone(), line_num + 1));
2029 all_let_defs.push((name.to_string(), rel.clone(), line_num + 1));
2042 }
2030 }
2043− }
2044 // Collect references: @name calls and $name usages
2031 // Collect references: @name calls and $name usages
2045 if trimmed.starts_with('@') && !trimmed.starts_with("@fn ")
2032 if trimmed.starts_with('@') && !trimmed.starts_with("@fn ")
2046 && !trimmed.starts_with("@let ") && !trimmed.starts_with("@define ")
2033 && !trimmed.starts_with("@let ") && !trimmed.starts_with("@define ")
@@ -2053,11 +2040,9 @@ fn main() {
2053 && !trimmed.starts_with("@use ") && !trimmed.starts_with("@slot ")
2040 && !trimmed.starts_with("@use ") && !trimmed.starts_with("@slot ")
2054 && !trimmed.starts_with("@lang ") && !trimmed.starts_with("@favicon ")
2041 && !trimmed.starts_with("@lang ") && !trimmed.starts_with("@favicon ")
2055 && !trimmed.starts_with("--")
2042 && !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() {
2058 all_refs.insert(name.to_string());
2044 all_refs.insert(name.to_string());
2059 }
2045 }
2060− }
2061 // Collect $var references
2046 // Collect $var references
2062 let mut rest = trimmed;
2047 let mut rest = trimmed;
2063 while let Some(pos) = rest.find('$') {
2048 while let Some(pos) = rest.find('$') {
@@ -2184,7 +2169,7 @@ fn main() {
2184 "netlify" => {
2169 "netlify" => {
2185 eprintln!("deploying to Netlify...");
2170 eprintln!("deploying to Netlify...");
2186 let status = process::Command::new("npx")
2171 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()])
2188 .status();
2173 .status();
2189 match status {
2174 match status {
2190 Ok(s) if s.success() => eprintln!("deployed to Netlify!"),
2175 Ok(s) if s.success() => eprintln!("deployed to Netlify!"),
@@ -2198,7 +2183,7 @@ fn main() {
2198 "vercel" => {
2183 "vercel" => {
2199 eprintln!("deploying to Vercel...");
2184 eprintln!("deploying to Vercel...");
2200 let status = process::Command::new("npx")
2185 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()])
2202 .status();
2187 .status();
2203 match status {
2188 match status {
2204 Ok(s) if s.success() => eprintln!("deployed to Vercel!"),
2189 Ok(s) if s.success() => eprintln!("deployed to Vercel!"),
@@ -2212,7 +2197,7 @@ fn main() {
2212 "cloudflare" => {
2197 "cloudflare" => {
2213 eprintln!("deploying to Cloudflare Pages...");
2198 eprintln!("deploying to Cloudflare Pages...");
2214 let status = process::Command::new("npx")
2199 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()])
2216 .status();
2201 .status();
2217 match status {
2202 match status {
2218 Ok(s) if s.success() => eprintln!("deployed to Cloudflare Pages!"),
2203 Ok(s) if s.success() => eprintln!("deployed to Cloudflare Pages!"),
@@ -2229,7 +2214,7 @@ fn main() {
2229 // Deploy via gh-pages push
2214 // Deploy via gh-pages push
2230 let init_ok = matches!(
2215 let init_ok = matches!(
2231 process::Command::new("git")
2216 process::Command::new("git")
2232− .args(&["init"])
2217+ .args(["init"])
2233 .current_dir(&deploy_dir)
2218 .current_dir(&deploy_dir)
2234 .status(),
2219 .status(),
2235 Ok(s) if s.success()
2220 Ok(s) if s.success()
@@ -2239,16 +2224,16 @@ fn main() {
2239 process::exit(1);
2224 process::exit(1);
2240 }
2225 }
2241 let _ = process::Command::new("git")
2226 let _ = process::Command::new("git")
2242− .args(&["checkout", "-b", "gh-pages"])
2227+ .args(["checkout", "-b", "gh-pages"])
2243 .current_dir(&deploy_dir)
2228 .current_dir(&deploy_dir)
2244 .status();
2229 .status();
2245 let _ = process::Command::new("git")
2230 let _ = process::Command::new("git")
2246− .args(&["add", "."])
2231+ .args(["add", "."])
2247 .current_dir(&deploy_dir)
2232 .current_dir(&deploy_dir)
2248 .status();
2233 .status();
2249 let commit_ok = matches!(
2234 let commit_ok = matches!(
2250 process::Command::new("git")
2235 process::Command::new("git")
2251− .args(&["commit", "-m", "deploy"])
2236+ .args(["commit", "-m", "deploy"])
2252 .current_dir(&deploy_dir)
2237 .current_dir(&deploy_dir)
2253 .status(),
2238 .status(),
2254 Ok(s) if s.success()
2239 Ok(s) if s.success()
@@ -2259,19 +2244,19 @@ fn main() {
2259 }
2244 }
2260 // Check if remote origin exists in parent
2245 // Check if remote origin exists in parent
2261 let remote_output = process::Command::new("git")
2246 let remote_output = process::Command::new("git")
2262− .args(&["remote", "get-url", "origin"])
2247+ .args(["remote", "get-url", "origin"])
2263 .output();
2248 .output();
2264 if let Ok(output) = remote_output {
2249 if let Ok(output) = remote_output {
2265 if output.status.success() {
2250 if output.status.success() {
2266 let remote_url = String::from_utf8_lossy(&output.stdout).trim().to_string();
2251 let remote_url = String::from_utf8_lossy(&output.stdout).trim().to_string();
2267 let _ = process::Command::new("git")
2252 let _ = process::Command::new("git")
2268− .args(&["remote", "add", "origin", &remote_url])
2253+ .args(["remote", "add", "origin", &remote_url])
2269 .current_dir(&deploy_dir)
2254 .current_dir(&deploy_dir)
2270 .status();
2255 .status();
2271 eprintln!("pushing to gh-pages branch at {}...", remote_url);
2256 eprintln!("pushing to gh-pages branch at {}...", remote_url);
2272 let push_ok = matches!(
2257 let push_ok = matches!(
2273 process::Command::new("git")
2258 process::Command::new("git")
2274− .args(&["push", "-f", "origin", "gh-pages"])
2259+ .args(["push", "-f", "origin", "gh-pages"])
2275 .current_dir(&deploy_dir)
2260 .current_dir(&deploy_dir)
2276 .status(),
2261 .status(),
2277 Ok(s) if s.success()
2262 Ok(s) if s.success()
@@ -2612,12 +2597,11 @@ compile();
2612 }
2597 }
2613 // Also remove sitemap.xml if present
2598 // Also remove sitemap.xml if present
2614 let sitemap = dir.join("sitemap.xml");
2599 let sitemap = dir.join("sitemap.xml");
2615− if sitemap.exists() {
2600+ if sitemap.exists()
2616− if let Ok(()) = fs::remove_file(&sitemap) {
2601+ && let Ok(()) = fs::remove_file(&sitemap) {
2617 eprintln!("removed {}", sitemap.display());
2602 eprintln!("removed {}", sitemap.display());
2618 removed += 1;
2603 removed += 1;
2619 }
2604 }
2620− }
2621 eprintln!("cleaned {} file{}", removed, if removed == 1 { "" } else { "s" });
2605 eprintln!("cleaned {} file{}", removed, if removed == 1 { "" } else { "s" });
2622 return;
2606 return;
2623 }
2607 }
@@ -2973,8 +2957,8 @@ compile();
2973 let html = htmlang::codegen::generate_dev(&result.document);
2957 let html = htmlang::codegen::generate_dev(&result.document);
2974 // Extract CSS class mappings from <style> block
2958 // Extract CSS class mappings from <style> block
2975 let mut class_css: HashMap<String, String> = HashMap::new();
2959 let mut class_css: HashMap<String, String> = HashMap::new();
2976− if let Some(style_start) = html.find("<style>") {
2960+ if let Some(style_start) = html.find("<style>")
2977− if let Some(style_end) = html[style_start..].find("</style>") {
2961+ && let Some(style_end) = html[style_start..].find("</style>") {
2978 let css_block = &html[style_start + 7..style_start + style_end];
2962 let css_block = &html[style_start + 7..style_start + style_end];
2979 // Parse each CSS rule: .className { rules }
2963 // Parse each CSS rule: .className { rules }
2980 let mut pos = 0;
2964 let mut pos = 0;
@@ -3016,7 +3000,6 @@ compile();
3016 }
3000 }
3017 }
3001 }
3018 }
3002 }
3019− }
3020 // Extract data-hl-line attributes from dev HTML to map classes to source lines
3003 // Extract data-hl-line attributes from dev HTML to map classes to source lines
3021 let mut line_classes: HashMap<usize, Vec<String>> = HashMap::new();
3004 let mut line_classes: HashMap<usize, Vec<String>> = HashMap::new();
3022 let html_body = if let Some(body_start) = html.find("<body") {
3005 let html_body = if let Some(body_start) = html.find("<body") {
@@ -3036,8 +3019,8 @@ compile();
3036 // Extract data-hl-line
3019 // Extract data-hl-line
3037 if let Some(hl_pos) = tag.find("data-hl-line=\"") {
3020 if let Some(hl_pos) = tag.find("data-hl-line=\"") {
3038 let after = &tag[hl_pos + 14..];
3021 let after = &tag[hl_pos + 14..];
3039− if let Some(end) = after.find('"') {
3022+ if let Some(end) = after.find('"')
3040− if let Ok(ln) = after[..end].parse::<usize>() {
3023+ && let Ok(ln) = after[..end].parse::<usize>() {
3041 // Extract class
3024 // Extract class
3042 if let Some(cls_pos) = tag.find("class=\"") {
3025 if let Some(cls_pos) = tag.find("class=\"") {
3043 let cls_after = &tag[cls_pos + 7..];
3026 let cls_after = &tag[cls_pos + 7..];
@@ -3048,7 +3031,6 @@ compile();
3048 }
3031 }
3049 }
3032 }
3050 }
3033 }
3051− }
3052 }
3034 }
3053 scan_pos = tag_end;
3035 scan_pos = tag_end;
3054 } else {
3036 } else {
@@ -3444,11 +3426,10 @@ compile();
3444 all_included.extend(included);
3426 all_included.extend(included);
3445 }
3427 }
3446
3428
3447− if format_json {
3429+ if format_json
3448− if let Some(collector) = json_collector {
3430+ && let Some(collector) = json_collector {
3449 print_json_diagnostics(&collector.lock().unwrap());
3431 print_json_diagnostics(&collector.lock().unwrap());
3450 }
3432 }
3451− }
3452
3433
3453 if !watch {
3434 if !watch {
3454 if any_errors {
3435 if any_errors {
@@ -3487,11 +3468,10 @@ compile();
3487 compat, strict, partial,
3468 compat, strict, partial,
3488 ..Default::default()
3469 ..Default::default()
3489 });
3470 });
3490− if format_json {
3471+ if format_json
3491− if let Some(ref collector) = json_collector_single {
3472+ && let Some(ref collector) = json_collector_single {
3492 print_json_diagnostics(&collector.lock().unwrap());
3473 print_json_diagnostics(&collector.lock().unwrap());
3493 }
3474 }
3494− }
3495
3475
3496 if !watch {
3476 if !watch {
3497 if has_errors {
3477 if has_errors {
@@ -3540,7 +3520,7 @@ fn collect_hl_files(dir: &Path) -> Vec<PathBuf> {
3540 if let Ok(entries) = fs::read_dir(dir) {
3520 if let Ok(entries) = fs::read_dir(dir) {
3541 for entry in entries.flatten() {
3521 for entry in entries.flatten() {
3542 let path = entry.path();
3522 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") {
3544 files.push(path);
3524 files.push(path);
3545 }
3525 }
3546 }
3526 }
@@ -3549,6 +3529,7 @@ fn collect_hl_files(dir: &Path) -> Vec<PathBuf> {
3549 files
3529 files
3550}
3530}
3551
3531
3532+#[allow(clippy::too_many_arguments)]
3552fn watch_loop(
3533fn watch_loop(
3553 watch_dir: &Path,
3534 watch_dir: &Path,
3554 source_files: &[PathBuf],
3535 source_files: &[PathBuf],
@@ -3639,24 +3620,21 @@ fn watch_loop(
3639 }
3620 }
3640 }
3621 }
3641
3622
3642− loop {
3643− match rx.recv() {
3623+ while rx.recv().is_ok() {
3644− Ok(_) => {
3645− // Drain additional events (debounce) with configurable delay
3624+ // Drain additional events (debounce) with configurable delay
3646− std::thread::sleep(std::time::Duration::from_millis(debounce_ms));
3625+ std::thread::sleep(std::time::Duration::from_millis(debounce_ms));
3647− while rx.try_recv().is_ok() {}
3626+ while rx.try_recv().is_ok() {}
3648
3627
3649 // Collect which files actually changed (HashSet for O(1) lookups)
3628 // Collect which files actually changed (HashSet for O(1) lookups)
3650 let mut changed_files: HashSet<PathBuf> = HashSet::new();
3629 let mut changed_files: HashSet<PathBuf> = HashSet::new();
3651 let check_path = |path: &Path,
3630 let check_path = |path: &Path,
3652 hashes: &mut HashMap<PathBuf, u64>|
3631 hashes: &mut HashMap<PathBuf, u64>|
3653 -> bool {
3632 -> bool {
3654− if let Some(h) = hash_file(path) {
3633+ if let Some(h) = hash_file(path)
3655− if hashes.get(path) != Some(&h) {
3634+ && hashes.get(path) != Some(&h) {
3656 hashes.insert(path.to_path_buf(), h);
3635 hashes.insert(path.to_path_buf(), h);
3657 return true;
3636 return true;
3658 }
3637 }
3659− }
3660 false
3638 false
3661 };
3639 };
3662
3640
@@ -3668,7 +3646,7 @@ fn watch_loop(
3668 }
3646 }
3669
3647
3670 // Check included files for changes
3648 // Check included files for changes
3671− for (_, deps) in &dep_map {
3649+ for deps in dep_map.values() {
3672 for dep in deps {
3650 for dep in deps {
3673 if check_path(dep, &mut content_hashes) {
3651 if check_path(dep, &mut content_hashes) {
3674 changed_files.insert(dep.clone());
3652 changed_files.insert(dep.clone());
@@ -3722,13 +3700,11 @@ fn watch_loop(
3722 continue;
3700 continue;
3723 }
3701 }
3724 // Recompile if any of its dependencies changed
3702 // Recompile if any of its dependencies changed
3725− if let Some(deps) = dep_map.get(source) {
3703+ if let Some(deps) = dep_map.get(source)
3726− if deps.iter().any(|d| changed_files.contains(d)) {
3704+ && deps.iter().any(|d| changed_files.contains(d))
3727− if !files_to_compile.contains(source) {
3705+ && !files_to_compile.contains(source) {
3728 files_to_compile.push(source.clone());
3706 files_to_compile.push(source.clone());
3729 }
3707 }
3730− }
3731− }
3732 }
3708 }
3733
3709
3734 // If no specific files identified (e.g., first run), compile all
3710 // If no specific files identified (e.g., first run), compile all
@@ -3782,8 +3758,5 @@ fn watch_loop(
3782 if let Some(ref tx) = reload_tx {
3758 if let Some(ref tx) = reload_tx {
3783 let _ = tx.send(());
3759 let _ = tx.send(());
3784 }
3760 }
3785− }
3786− Err(_) => break,
3787− }
3788 }
3761 }
3789}
3762}
src/serve.rs+4−5
@@ -233,7 +233,7 @@ where
233 Some(fp) => {
233 Some(fp) => {
234 let body = match std::fs::read(&fp) {
234 let body = match std::fs::read(&fp) {
235 Ok(content) => {
235 Ok(content) => {
236− if fp.extension().map_or(false, |e| e == "html") {
236+ if fp.extension().is_some_and(|e| e == "html") {
237 inject_reload_script(&content)
237 inject_reload_script(&content)
238 } else {
238 } else {
239 content
239 content
@@ -304,11 +304,10 @@ fn collect_html_pages(base: &Path, dir: &Path, pages: &mut Vec<String>) {
304 let path = entry.path();
304 let path = entry.path();
305 if path.is_dir() {
305 if path.is_dir() {
306 collect_html_pages(base, &path, pages);
306 collect_html_pages(base, &path, pages);
307− } else if path.extension().map_or(false, |e| e == "html") {
307+ } else if path.extension().is_some_and(|e| e == "html")
308− if let Ok(rel) = path.strip_prefix(base) {
308+ && let Ok(rel) = path.strip_prefix(base) {
309 pages.push(format!("/{}", rel.display()));
309 pages.push(format!("/{}", rel.display()));
310 }
310 }
311− }
312 }
311 }
313 }
312 }
314}
313}
@@ -416,7 +415,7 @@ where
416
415
417 let body = match std::fs::read(&file_path) {
416 let body = match std::fs::read(&file_path) {
418 Ok(content) => {
417 Ok(content) => {
419− if file_path.extension().map_or(false, |e| e == "html") {
418+ if file_path.extension().is_some_and(|e| e == "html") {
420 inject_reload_script(&content)
419 inject_reload_script(&content)
421 } else {
420 } else {
422 content
421 content