use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use ignore::WalkBuilder;
use light_fw_html::Node;
use crate::error::{Error, Result};
#[derive(Debug, Default)]
pub struct ScanResult {
pub tags: BTreeMap<String, usize>,
pub attributes: BTreeMap<String, usize>,
pub files: Vec<PathBuf>,
pub errors: Vec<ScanError>,
}
#[derive(Debug)]
pub struct ScanError {
pub file: PathBuf,
pub message: String,
}
pub fn scan_templates(root: &Path) -> Result<ScanResult> {
let mut result = ScanResult::default();
let walk = WalkBuilder::new(root)
.hidden(true)
.filter_entry(|entry| entry.file_name() != "target")
.build();
for entry in walk {
let entry = entry.map_err(Error::Ignore)?;
let path = entry.path();
if !path.is_file() || path.extension().is_none_or(|ext| ext != "html") {
continue;
}
let content = std::fs::read_to_string(path)?;
match light_fw_html::parse(&content) {
Ok(nodes) => {
collect(&nodes, &mut result.tags, &mut result.attributes);
result.files.push(path.to_path_buf());
}
Err(message) => result.errors.push(ScanError {
file: path.to_path_buf(),
message,
}),
}
}
result.files.sort();
Ok(result)
}
fn collect(
nodes: &[Node],
tags: &mut BTreeMap<String, usize>,
attributes: &mut BTreeMap<String, usize>,
) {
for node in nodes {
if let Node::Element(element) = node {
*tags.entry(element.name.clone()).or_insert(0) += 1;
for attr in &element.attrs {
*attributes.entry(attr.name.clone()).or_insert(0) += 1;
}
collect(&element.children, tags, attributes);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn temp_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("light-fw-cli-scan-{name}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp dir");
dir
}
#[test]
fn counts_tags_and_attributes() {
let dir = temp_dir("counts");
fs::write(
dir.join("a.html"),
"<div><text v-model=\"x\"></text><checkbox required></checkbox></div>",
)
.expect("write template");
let result = scan_templates(&dir).expect("scan");
assert_eq!(result.files.len(), 1);
assert_eq!(result.tags.get("text"), Some(&1));
assert_eq!(result.tags.get("checkbox"), Some(&1));
assert_eq!(result.attributes.get("v-model"), Some(&1));
assert_eq!(result.attributes.get("required"), Some(&1));
assert!(result.errors.is_empty());
}
#[test]
fn reports_parse_errors() {
let dir = temp_dir("errors");
fs::write(dir.join("broken.html"), "<div><text>не закрыт</div>").expect("write template");
let result = scan_templates(&dir).expect("scan");
assert!(result.files.is_empty());
assert_eq!(result.errors.len(), 1);
}
#[test]
fn ignores_non_html_files() {
let dir = temp_dir("non-html");
fs::write(dir.join("readme.txt"), "<div></div>").expect("write file");
let result = scan_templates(&dir).expect("scan");
assert!(result.files.is_empty());
}
}