Skip to main content

plot/lib/logic/
path_patterns.rs

1use regex::Regex;
2
3use crate::lib::logic::PathContext;
4
5// ============================================================================
6// ENV ABSTRAKCJA (ODCIĘCIE OD FS / STRUKTUR KOLEKCJI)
7// ============================================================================
8
9pub trait PattEnvIndex {
10	fn has_dir(&self, dir: &str) -> bool;
11	fn has_file_with_prefix(&self, prefix: &str) -> bool;
12	fn any_file_in_dir(&self, dir: &str, check: &mut dyn FnMut(&str) -> bool) -> bool;
13}
14
15// ============================================================================
16// PATTERNS (PUBLIC API)
17// ============================================================================
18
19/// [DOMENA]: Silnie typowana lista surowych wzorców
20#[derive(Debug, Clone)]
21pub struct PattRaw(pub Vec<String>);
22
23/// [DOMENA]: Silnie typowana lista rozwiniętych wzorców
24#[derive(Debug, Clone)]
25pub struct PattExp(pub Vec<String>);
26
27#[derive(Debug, Clone)]
28pub struct PatternsQueries {
29	pub patterns: PattRaw,
30	pub expanded: PattExp,
31	compiled: Vec<PatternCompiled>,
32}
33
34// ============================================================================
35// COMPILED RULE
36// ============================================================================
37
38#[derive(Debug, Clone)]
39struct PatternCompiled {
40	regex: Regex,
41	targets_file: bool,
42	requires_sibling: bool,
43	requires_orphan: bool,
44	is_deep: bool,
45	include_parents: bool,
46	base_name: String,
47	pub is_negated: bool,
48	#[allow(dead_code)]
49	pub original: String,
50}
51
52// ============================================================================
53// PATTERNS IMPLEMENTATION
54// ============================================================================
55
56impl PatternsQueries {
57	pub fn new<I, S>(patterns: I, ignore_case_sensitive: bool) -> Self
58	where
59		I: IntoIterator<Item = S>,
60		S: AsRef<str>, {
61		let mut patterns_vec = Vec::new();
62		let mut expanded_vec = Vec::new();
63
64		for p in patterns {
65			let s = p.as_ref();
66			patterns_vec.push(s.to_string());
67			expanded_vec.extend(Self::expand_braces(s));
68		}
69
70		let compiled = expanded_vec.iter().map(|p| PatternCompiled::new(p, ignore_case_sensitive).unwrap()).collect();
71
72		Self {
73			// Pakujemy wektory w nasze nowe typy domenowe!
74			patterns: PattRaw(patterns_vec),
75			expanded: PattExp(expanded_vec),
76			compiled,
77		}
78	}
79
80	/// public API
81	pub fn is_match<E: PattEnvIndex>(&self, path: &str, env: &E) -> bool {
82		let mut has_positive = false;
83		let mut matched = false;
84
85		for c in &self.compiled {
86			if c.is_negated {
87				if c.is_match(path, env) {
88					return false;
89				}
90			} else {
91				has_positive = true;
92				if !matched && c.is_match(path, env) {
93					matched = true;
94				}
95			}
96		}
97
98		if has_positive { matched } else { true }
99	}
100
101	pub fn rules_count(&self) -> usize { self.compiled.len() }
102
103	// =========================================================================
104	// BRACE EXPANSION (DRY, REKURENCYJNE {{a,b},c} - znajdź najgłębsze)
105	// =========================================================================
106
107	fn expand_braces(input: &str) -> Vec<String> {
108		// Szukamy pierwszej zamykającej klamry
109		if let Some(end) = input.find('}') {
110			// Szukamy otwierającej klamry najbliżej tej zamykającej (najgłębszy poziom)
111			if let Some(start) = input[..end].rfind('{') {
112				let prefix = &input[..start];
113				let suffix = &input[end + 1..];
114				let options = &input[start + 1..end];
115
116				let mut result = Vec::new();
117				for opt in options.split(',') {
118					let merged = format!("{}{}{}", prefix, opt, suffix);
119					// Rekurencja dla kolejnych klamer
120					result.extend(Self::expand_braces(&merged));
121				}
122				return result;
123			}
124		}
125		vec![input.to_string()]
126	}
127}
128
129// ============================================================================
130// COMPILATION & MATCHING LOGIC
131// ============================================================================
132
133impl PatternCompiled {
134	pub fn new(pattern: &str, ignore_case_sensitive: bool) -> Result<Self, regex::Error> {
135		let is_negated = pattern.starts_with('!');
136		let p = if is_negated { &pattern[1..] } else { pattern };
137
138		let is_deep = p.ends_with('+');
139		let include_parents = p.ends_with("&/") || p.ends_with("&\\");
140		let requires_sibling = p.contains('@');
141		let requires_orphan = p.contains('$');
142
143		let mut clean = p.replace(['@', '$', '+'], "");
144
145		if include_parents {
146			clean = clean.strip_suffix("&/").or_else(|| clean.strip_suffix("&\\")).unwrap_or(&clean).to_string();
147		}
148
149		// 🧠 ŹLE DZIAŁA FLAGA `+` np w "./dist/+"
150		// ⚡ KRYTYCZNA ZMIANA: Obliczamy targets_file ZANIM utniemy ukośnik!
151		// let targets_file = !clean.ends_with('/') && !clean.ends_with("**");
152		// // ⚡ Usuwamy wiszący ukośnik, aby uniknąć podwójnego slasha `//` w regexie
153		// if is_deep && clean.ends_with('/') {
154		//     clean.pop();
155		// }
156
157		let base_name = clean
158			.trim_end_matches('/')
159			.split('/')
160			.next_back()
161			.unwrap_or("")
162			.split('.')
163			.next()
164			.unwrap_or("")
165			.to_string();
166
167		let mut re = String::new();
168
169		if ignore_case_sensitive {
170			re.push_str("(?i)");
171		}
172
173		let mut p_str = clean.as_str();
174		let mut anchored = false;
175
176		let targets_file = !p_str.ends_with('/') && !p_str.ends_with("**");
177
178		if p_str.starts_with("./") {
179			anchored = true;
180			p_str = &p_str[2..];
181		} else if p_str.starts_with("**/") {
182			anchored = true;
183		}
184
185		if anchored {
186			re.push('^');
187		} else {
188			re.push_str("(?:^|/)");
189		}
190
191		let chars: Vec<char> = p_str.chars().collect();
192		let mut i = 0;
193
194		while i < chars.len() {
195			match chars[i] {
196				'\\' => {
197					i += 1;
198					if i < chars.len() {
199						re.push_str(&regex::escape(&chars[i].to_string()));
200					}
201				}
202				'.' => re.push_str("\\."),
203				'/' => re.push('/'),
204				'*' => {
205					if i + 1 < chars.len() && chars[i + 1] == '*' {
206						re.push_str(".+");
207						i += 1;
208					} else {
209						re.push_str("[^/]*");
210					}
211				}
212				'?' => re.push_str("[^/]"),
213				'[' => {
214					re.push('[');
215					if i + 1 < chars.len() && chars[i + 1] == '!' {
216						re.push('^');
217						i += 1;
218					}
219				}
220				']' | '-' | '^' => re.push(chars[i]),
221				// Klamry są już obsłużone przez expand_braces!
222				_ => re.push_str(&regex::escape(&chars[i].to_string())),
223			}
224			i += 1;
225		}
226
227		if is_deep {
228			re.push_str("(?:/.*)?$");
229		} else {
230			re.push('$');
231		}
232
233		Ok(Self {
234			regex: Regex::new(&re)?,
235			targets_file,
236			requires_sibling,
237			requires_orphan,
238			is_deep,
239			include_parents,
240			base_name,
241			is_negated,
242			original: pattern.to_string(),
243		})
244	}
245
246	pub fn is_match<E: PattEnvIndex>(&self, path: &str, env: &E) -> bool {
247		let is_dir = path.ends_with('/');
248		let clean = path.strip_prefix("./").unwrap_or(path);
249
250		// 1. Ochrona rodziców (Flaga &/)
251		if self.targets_file && is_dir {
252			if self.include_parents {
253				return env.any_file_in_dir(path, &mut |p| self.regex.is_match(p.strip_prefix("./").unwrap_or(p)));
254			}
255			return false;
256		}
257
258		if !self.regex.is_match(clean) {
259			if is_dir && self.include_parents {
260				return env.any_file_in_dir(path, &mut |p| self.regex.is_match(p.strip_prefix("./").unwrap_or(p)));
261			}
262			return false;
263		}
264
265		// 2. Relacje dla plików (@ i $)
266		if (self.requires_sibling || self.requires_orphan) && !is_dir {
267			if self.is_deep && self.requires_sibling {
268				if !self.check_authorized_root(path, env) {
269					return false;
270				}
271				return true;
272			}
273
274			let ctx = PathContext::from(path);
275			let expected_folder = if ctx.parent.is_empty() {
276				format!("{}/", self.base_name)
277			} else {
278				format!("{}/{}/", ctx.parent, self.base_name)
279			};
280
281			let exists = env.has_dir(&expected_folder);
282
283			if self.requires_sibling && !exists {
284				return false;
285			}
286			if self.requires_orphan && exists {
287				return false;
288			}
289		}
290
291		// 3. Relacje dla folderów (@ i $)
292		if (self.requires_sibling || self.requires_orphan) && is_dir {
293			if self.is_deep && self.requires_sibling {
294				if !self.check_authorized_root(path, env) {
295					return false;
296				}
297			} else {
298				let dir_no_slash = path.trim_end_matches('/');
299				let search_prefix = format!("{}.", dir_no_slash);
300				let has_file_sibling = env.has_file_with_prefix(&search_prefix);
301
302				if self.requires_sibling && !has_file_sibling {
303					return false;
304				}
305				if self.requires_orphan && has_file_sibling {
306					return false;
307				}
308			}
309		}
310
311		true
312	}
313
314	fn check_authorized_root<E: PattEnvIndex>(&self, path: &str, env: &E) -> bool {
315		let clean = path.strip_prefix("./").unwrap_or(path);
316		let components: Vec<&str> = clean.split('/').collect();
317
318		for i in 0..components.len() {
319			let comp_core = components[i].split('.').next().unwrap_or("");
320
321			if comp_core == self.base_name {
322				let base_dir = if i == 0 {
323					self.base_name.clone()
324				} else {
325					format!("{}/{}", components[0..i].join("/"), self.base_name)
326				};
327
328				let full_base_dir = if path.starts_with("./") { format!("./{}", base_dir) } else { base_dir };
329				let dir_path = format!("{}/", full_base_dir);
330
331				let has_dir = env.has_dir(&dir_path);
332				let search_prefix = format!("{}.", full_base_dir);
333				let has_file = env.has_file_with_prefix(&search_prefix);
334
335				if has_dir && has_file {
336					return true;
337				}
338			}
339		}
340		false
341	}
342}