Skip to main content

light_fw_cli_core/
scan.rs

1use 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/// Результат сканирования шаблонов.
10#[derive(Debug, Default)]
11pub struct ScanResult {
12    /// Использования тегов: имя → количество.
13    pub tags: BTreeMap<String, usize>,
14    /// Использования атрибутов: имя → количество.
15    pub attributes: BTreeMap<String, usize>,
16    /// Просканированные файлы шаблонов.
17    pub files: Vec<PathBuf>,
18    /// Ошибки разбора шаблонов.
19    pub errors: Vec<ScanError>,
20}
21
22/// Ошибка разбора отдельного шаблона.
23#[derive(Debug)]
24pub struct ScanError {
25    /// Путь к файлу.
26    pub file: PathBuf,
27    /// Сообщение парсера.
28    pub message: String,
29}
30
31/// Сканирует `*.html`-шаблоны под `root`.
32///
33/// Обход учитывает `.gitignore`/`.ignore`, пропускает скрытые каталоги
34/// и каталог `target`. Ничего не пишет на диск.
35pub 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}