Skip to main content

tokmd_exclude/
lib.rs

1//! Deterministic exclude-pattern normalization and dedupe helpers.
2
3#![forbid(unsafe_code)]
4
5use std::path::Path;
6
7use tokmd_path::normalize_rel_path;
8
9/// Normalize an exclude path into a deterministic pattern.
10///
11/// Rules:
12/// - if `path` is absolute and under `root`, strip the `root` prefix
13/// - convert backslashes to `/`
14/// - strip one leading `./`
15///
16/// # Examples
17///
18/// ```
19/// use std::path::Path;
20/// use tokmd_exclude::normalize_exclude_pattern;
21///
22/// let root = Path::new("/project");
23/// let relative = Path::new("./out/bundle.js");
24/// assert_eq!(normalize_exclude_pattern(root, relative), "out/bundle.js");
25/// ```
26#[must_use]
27pub fn normalize_exclude_pattern(root: &Path, path: &Path) -> String {
28    let rel = if path.is_absolute() {
29        path.strip_prefix(root).unwrap_or(path)
30    } else {
31        path
32    };
33    normalize_rel_path(&rel.to_string_lossy())
34}
35
36/// Return `true` when `existing` already contains `pattern` after normalization.
37///
38/// # Examples
39///
40/// ```
41/// use tokmd_exclude::has_exclude_pattern;
42///
43/// let existing = vec!["out/bundle".to_string()];
44/// assert!(has_exclude_pattern(&existing, "./out/bundle"));
45/// assert!(!has_exclude_pattern(&existing, "dist/app"));
46/// ```
47#[must_use]
48pub fn has_exclude_pattern(existing: &[String], pattern: &str) -> bool {
49    let normalized = normalize_rel_path(pattern);
50    existing
51        .iter()
52        .any(|candidate| normalize_rel_path(candidate) == normalized)
53}
54
55/// Add a pattern only when non-empty and not already present (after normalization).
56///
57/// Returns `true` when the pattern was inserted.
58///
59/// # Examples
60///
61/// ```
62/// use tokmd_exclude::add_exclude_pattern;
63///
64/// let mut patterns = vec![];
65/// assert!(add_exclude_pattern(&mut patterns, "out/bundle".to_string()));
66/// assert!(!add_exclude_pattern(&mut patterns, "./out/bundle".to_string())); // duplicate
67/// assert!(!add_exclude_pattern(&mut patterns, String::new())); // empty
68/// assert_eq!(patterns.len(), 1);
69/// ```
70pub fn add_exclude_pattern(existing: &mut Vec<String>, pattern: String) -> bool {
71    if pattern.is_empty() || has_exclude_pattern(existing, &pattern) {
72        return false;
73    }
74    existing.push(pattern);
75    true
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn normalize_exclude_pattern_strips_root_for_absolute_paths() {
84        let root = std::env::temp_dir().join("tokmd-exclude-lib-root");
85        let path = root.join(".handoff").join("manifest.json");
86        let got = normalize_exclude_pattern(&root, &path);
87        assert_eq!(got, ".handoff/manifest.json");
88    }
89
90    #[test]
91    fn normalize_exclude_pattern_keeps_outside_absolute_paths() {
92        let root = std::env::temp_dir().join("tokmd-exclude-lib-root");
93        let outside = std::env::temp_dir()
94            .join("tokmd-exclude-lib-outside")
95            .join("bundle.txt");
96        let got = normalize_exclude_pattern(&root, &outside);
97        let expected = tokmd_path::normalize_rel_path(&outside.to_string_lossy());
98        assert_eq!(got, expected);
99    }
100
101    #[test]
102    fn add_exclude_pattern_dedupes_after_normalization() {
103        let mut existing = vec!["./out\\bundle".to_string()];
104        assert!(!add_exclude_pattern(
105            &mut existing,
106            "out/bundle".to_string()
107        ));
108        assert_eq!(existing.len(), 1);
109    }
110
111    #[test]
112    fn add_exclude_pattern_rejects_empty_patterns() {
113        let mut existing = vec![];
114        assert!(!add_exclude_pattern(&mut existing, String::new()));
115        assert!(existing.is_empty());
116    }
117}