@@ -344,96 +344,13 @@ fn lint_file(path: &str) -> Vec<String> {
344344 };
345345 let base = Path::new(path).parent();
346346 let result = htmlang::parser::parse_with_base(&input, base);
347− let mut warnings = Vec::new();
348−
349− // Report parser diagnostics
350− for d in &result.diagnostics {
351− let prefix = severity_label(d.severity);
352− warnings.push(format!("{}:{}:{}: {}", path, d.line, prefix, d.message));
353− }
354−
355− // Additional lint checks on the AST
356− lint_nodes(&result.document.nodes, path, 0, &mut warnings);
357− warnings
358−}
359−
360−fn lint_nodes(nodes: &[htmlang::ast::Node], path: &str, depth: usize, warnings: &mut Vec<String>) {
361− for node in nodes {
362− if let htmlang::ast::Node::Element(elem) = node {
363− // Deeply nested elements (>10 levels)
364− if depth > 10 {
365− warnings.push(format!(
366− "{}:{}:lint: deeply nested element ({} levels) — consider simplifying",
367− path, elem.line_num, depth
368− ));
369− }
370−
371− // @image without alt
372− if elem.kind == htmlang::ast::ElementKind::Image
373− && !elem.attrs.iter().any(|a| a.key == "alt")
374− {
375− warnings.push(format!(
376− "{}:{}:lint: @image missing 'alt' attribute (accessibility)",
377− path, elem.line_num
378− ));
379− }
380−
381− // @link without content or aria-label
382− if elem.kind == htmlang::ast::ElementKind::Link {
383− let has_aria = elem.attrs.iter().any(|a| a.key == "aria-label");
384− let has_children = !elem.children.is_empty();
385− let has_arg_text = elem.argument.as_ref().is_some_and(|_| false);
386− if !has_aria && !has_children && !has_arg_text {
387− warnings.push(format!(
388− "{}:{}:lint: @link has no visible text or aria-label (accessibility)",
389− path, elem.line_num
390− ));
391− }
392− }
393−
394− // @input without type
395− if elem.kind.is_tag("input")
396− && !elem.attrs.iter().any(|a| a.key == "type")
397− {
398− warnings.push(format!(
399− "{}:{}:lint: @input missing 'type' attribute",
400− path, elem.line_num
401− ));
402− }
403−
404− // Empty containers (no children, no text)
405− if matches!(
406− elem.kind,
407− htmlang::ast::ElementKind::Row
408− | htmlang::ast::ElementKind::Column
409− | htmlang::ast::ElementKind::El
410− ) && elem.children.is_empty()
411− {
412− warnings.push(format!(
413− "{}:{}:lint: empty container (@{}) has no children",
414− path,
415− elem.line_num,
416− match elem.kind {
417− htmlang::ast::ElementKind::Row => "row",
418− htmlang::ast::ElementKind::Column => "column",
419− _ => "el",
420− }
421− ));
422− }
423−
424− // @button without type
425− if elem.kind.is_tag("button")
426− && !elem.attrs.iter().any(|a| a.key == "type")
427− {
428− warnings.push(format!(
429− "{}:{}:lint: @button missing 'type' attribute (defaults to submit)",
430− path, elem.line_num
431− ));
432− }
433−
434− lint_nodes(&elem.children, path, depth + 1, warnings);
435− }
436− }
347+ let lint = htmlang::parser::lint(&result.document.nodes);
348+ result
349+ .diagnostics
350+ .iter()
351+ .chain(&lint)
352+ .map(|d| format!("{}:{}:{}: {}", path, d.line, severity_label(d.severity), d.message))
353+ .collect()
437354}
438355
439356/// Whether the build cache record at `path` matches `key` and every