light_fw_cli_core/
scan.rs1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use ignore::WalkBuilder;
5use light_fw_html::Node;
6
7use crate::error::{Error, Result};
8
9#[derive(Debug, Default)]
11pub struct ScanResult {
12 pub tags: BTreeMap<String, usize>,
14 pub attributes: BTreeMap<String, usize>,
16 pub files: Vec<PathBuf>,
18 pub errors: Vec<ScanError>,
20}
21
22#[derive(Debug)]
24pub struct ScanError {
25 pub file: PathBuf,
27 pub message: String,
29}
30
31pub fn scan_templates(root: &Path) -> Result<ScanResult> {
36 let mut result = ScanResult::default();
37 let walk = WalkBuilder::new(root)
38 .hidden(true)
39 .filter_entry(|entry| entry.file_name() != "target")
40 .build();
41 for entry in walk {
42 let entry = entry.map_err(Error::Ignore)?;
43 let path = entry.path();
44 if !path.is_file() || path.extension().is_none_or(|ext| ext != "html") {
45 continue;
46 }
47 let content = std::fs::read_to_string(path)?;
48 match light_fw_html::parse(&content) {
49 Ok(nodes) => {
50 collect(&nodes, &mut result.tags, &mut result.attributes);
51 result.files.push(path.to_path_buf());
52 }
53 Err(message) => result.errors.push(ScanError {
54 file: path.to_path_buf(),
55 message,
56 }),
57 }
58 }
59 result.files.sort();
60 Ok(result)
61}
62
63fn collect(
64 nodes: &[Node],
65 tags: &mut BTreeMap<String, usize>,
66 attributes: &mut BTreeMap<String, usize>,
67) {
68 for node in nodes {
69 if let Node::Element(element) = node {
70 *tags.entry(element.name.clone()).or_insert(0) += 1;
71 for attr in &element.attrs {
72 *attributes.entry(attr.name.clone()).or_insert(0) += 1;
73 }
74 collect(&element.children, tags, attributes);
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use std::fs;
83
84 fn temp_dir(name: &str) -> PathBuf {
85 let dir = std::env::temp_dir().join(format!("light-fw-cli-scan-{name}"));
86 let _ = fs::remove_dir_all(&dir);
87 fs::create_dir_all(&dir).expect("create temp dir");
88 dir
89 }
90
91 #[test]
92 fn counts_tags_and_attributes() {
93 let dir = temp_dir("counts");
94 fs::write(
95 dir.join("a.html"),
96 "<div><text v-model=\"x\"></text><checkbox required></checkbox></div>",
97 )
98 .expect("write template");
99 let result = scan_templates(&dir).expect("scan");
100 assert_eq!(result.files.len(), 1);
101 assert_eq!(result.tags.get("text"), Some(&1));
102 assert_eq!(result.tags.get("checkbox"), Some(&1));
103 assert_eq!(result.attributes.get("v-model"), Some(&1));
104 assert_eq!(result.attributes.get("required"), Some(&1));
105 assert!(result.errors.is_empty());
106 }
107
108 #[test]
109 fn reports_parse_errors() {
110 let dir = temp_dir("errors");
111 fs::write(dir.join("broken.html"), "<div><text>не закрыт</div>").expect("write template");
112 let result = scan_templates(&dir).expect("scan");
113 assert!(result.files.is_empty());
114 assert_eq!(result.errors.len(), 1);
115 }
116
117 #[test]
118 fn ignores_non_html_files() {
119 let dir = temp_dir("non-html");
120 fs::write(dir.join("readme.txt"), "<div></div>").expect("write file");
121 let result = scan_templates(&dir).expect("scan");
122 assert!(result.files.is_empty());
123 }
124}