microsoft_fast_build/
locator.rs1use std::collections::HashMap;
2use std::path::Path;
3use crate::error::RenderError;
4
5pub struct Locator {
6 templates: HashMap<String, String>,
7}
8
9impl Locator {
10 pub fn from_patterns(patterns: &[&str]) -> Result<Self, RenderError> {
15 let mut element_files: HashMap<String, Vec<String>> = HashMap::new();
17
18 for &pattern in patterns {
19 let dir_str = static_prefix_dir(pattern);
20 let dir_path = Path::new(&dir_str);
21
22 if !dir_path.exists() || !dir_path.is_dir() {
23 continue;
24 }
25
26 let mut files = Vec::new();
27 walk_html_files(dir_path, &mut files).map_err(|e| RenderError::TemplateReadError {
28 path: dir_str.clone(),
29 message: e.to_string(),
30 })?;
31
32 let norm_pattern = normalize_path(pattern);
33 for file_path in files {
34 let norm_file = normalize_path(&file_path.to_string_lossy());
35 if glob_match(&norm_pattern, &norm_file) {
36 if let Some(stem) = file_path.file_stem().and_then(|s| s.to_str()) {
37 element_files
38 .entry(stem.to_string())
39 .or_default()
40 .push(norm_file);
41 }
42 }
43 }
44 }
45
46 let mut templates = HashMap::new();
47 for (element, paths) in element_files {
48 if paths.len() > 1 {
49 return Err(RenderError::DuplicateTemplate { element, paths });
50 }
51 let path = &paths[0];
52 let content = std::fs::read_to_string(path).map_err(|e| RenderError::TemplateReadError {
53 path: path.clone(),
54 message: e.to_string(),
55 })?;
56 templates.insert(element, content);
57 }
58
59 Ok(Locator { templates })
60 }
61
62 pub fn from_templates(templates: HashMap<String, String>) -> Self {
64 Locator { templates }
65 }
66
67 pub fn add_template(&mut self, element_name: &str, content: &str) {
69 self.templates.insert(element_name.to_string(), content.to_string());
70 }
71
72 pub fn get_template(&self, element_name: &str) -> Option<&str> {
73 self.templates.get(element_name).map(|s| s.as_str())
74 }
75
76 pub fn has_template(&self, element_name: &str) -> bool {
77 self.templates.contains_key(element_name)
78 }
79}
80
81fn normalize_path(path: &str) -> String {
83 let normalized = path.replace('\\', "/");
84 normalized.trim_start_matches("./").to_string()
85}
86
87fn static_prefix_dir(pattern: &str) -> String {
89 let first_wild = pattern.find(|c: char| c == '*' || c == '?');
90 let base = match first_wild {
91 None => match pattern.rfind('/') {
92 Some(i) => pattern[..=i].to_string(),
93 None => ".".to_string(),
94 },
95 Some(i) => {
96 let before = &pattern[..i];
97 match before.rfind('/') {
98 Some(j) => pattern[..=j].to_string(),
99 None => ".".to_string(),
100 }
101 }
102 };
103 base
104}
105
106fn walk_html_files(dir: &Path, result: &mut Vec<std::path::PathBuf>) -> std::io::Result<()> {
108 for entry in std::fs::read_dir(dir)? {
109 let entry = entry?;
110 let path = entry.path();
111 if path.is_dir() {
112 walk_html_files(&path, result)?;
113 } else if path.extension().and_then(|s| s.to_str()) == Some("html") {
114 result.push(path);
115 }
116 }
117 Ok(())
118}
119
120fn glob_match(pattern: &str, path: &str) -> bool {
125 let pat_segs: Vec<&str> = pattern.split('/').collect();
126 let path_segs: Vec<&str> = path.split('/').collect();
127 match_segments(&pat_segs, &path_segs)
128}
129
130fn match_segments(pat: &[&str], path: &[&str]) -> bool {
131 if pat.is_empty() {
132 return path.is_empty();
133 }
134 if pat[0] == "**" {
135 if match_segments(&pat[1..], path) {
138 return true;
139 }
140 if !path.is_empty() && match_segments(pat, &path[1..]) {
142 return true;
143 }
144 return false;
145 }
146 if path.is_empty() {
147 return false;
148 }
149 if match_segment(pat[0], path[0]) {
150 match_segments(&pat[1..], &path[1..])
151 } else {
152 false
153 }
154}
155
156fn match_segment(pat: &str, seg: &str) -> bool {
157 let pat_chars: Vec<char> = pat.chars().collect();
158 let seg_chars: Vec<char> = seg.chars().collect();
159 match_chars(&pat_chars, &seg_chars)
160}
161
162fn match_chars(pat: &[char], seg: &[char]) -> bool {
163 if pat.is_empty() {
164 return seg.is_empty();
165 }
166 if pat[0] == '*' {
167 for i in 0..=seg.len() {
169 if match_chars(&pat[1..], &seg[i..]) {
170 return true;
171 }
172 }
173 return false;
174 }
175 if seg.is_empty() {
176 return false;
177 }
178 if pat[0] == '?' || pat[0] == seg[0] {
179 match_chars(&pat[1..], &seg[1..])
180 } else {
181 false
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_glob_star() {
191 assert!(glob_match("tests/fixtures/*.html", "tests/fixtures/my-button.html"));
192 assert!(!glob_match("tests/fixtures/*.html", "tests/fixtures/duplicate/my-button.html"));
193 }
194
195 #[test]
196 fn test_glob_double_star() {
197 assert!(glob_match("tests/fixtures/**/*.html", "tests/fixtures/my-button.html"));
198 assert!(glob_match("tests/fixtures/**/*.html", "tests/fixtures/duplicate/my-button.html"));
199 assert!(!glob_match("tests/fixtures/**/*.html", "tests/fixtures/my-button.txt"));
200 }
201
202 #[test]
203 fn test_glob_question_mark() {
204 assert!(glob_match("tests/fixtures/my-?.html", "tests/fixtures/my-x.html"));
205 assert!(!glob_match("tests/fixtures/my-?.html", "tests/fixtures/my-xy.html"));
206 }
207}