Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
1use globset::{Glob, GlobSet, GlobSetBuilder};
2use serde::{Deserialize, Serialize};
3use std::path::Path;
4use miette::IntoDiagnostic;
5
6#[derive(Debug, Deserialize, Serialize)]
7struct IgnoreConfig {
8 patterns: Vec<String>,
9}
10
11/// Load ignore patterns from the default .wispignore.json file
12fn load_default_patterns() -> miette::Result<Vec<String>> {
13 // Path to the default ignore patterns JSON file (in the monorepo root)
14 let default_json_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../.wispignore.json");
15
16 match std::fs::read_to_string(default_json_path) {
17 Ok(contents) => {
18 let config: IgnoreConfig = serde_json::from_str(&contents).into_diagnostic()?;
19 Ok(config.patterns)
20 }
21 Err(_) => {
22 // If the default file doesn't exist, return hardcoded patterns as fallback
23 eprintln!("⚠️ Default .wispignore.json not found, using hardcoded patterns");
24 Ok(get_hardcoded_patterns())
25 }
26 }
27}
28
29/// Hardcoded fallback patterns (same as in .wispignore.json)
30fn get_hardcoded_patterns() -> Vec<String> {
31 vec![
32 ".git".to_string(),
33 ".git/**".to_string(),
34 ".github".to_string(),
35 ".github/**".to_string(),
36 ".gitlab".to_string(),
37 ".gitlab/**".to_string(),
38 ".DS_Store".to_string(),
39 ".wisp.metadata.json".to_string(),
40 ".env".to_string(),
41 ".env.*".to_string(),
42 "node_modules".to_string(),
43 "node_modules/**".to_string(),
44 "Thumbs.db".to_string(),
45 "desktop.ini".to_string(),
46 "._*".to_string(),
47 ".Spotlight-V100".to_string(),
48 ".Spotlight-V100/**".to_string(),
49 ".Trashes".to_string(),
50 ".Trashes/**".to_string(),
51 ".fseventsd".to_string(),
52 ".fseventsd/**".to_string(),
53 ".cache".to_string(),
54 ".cache/**".to_string(),
55 ".temp".to_string(),
56 ".temp/**".to_string(),
57 ".tmp".to_string(),
58 ".tmp/**".to_string(),
59 "__pycache__".to_string(),
60 "__pycache__/**".to_string(),
61 "*.pyc".to_string(),
62 ".venv".to_string(),
63 ".venv/**".to_string(),
64 "venv".to_string(),
65 "venv/**".to_string(),
66 "env".to_string(),
67 "env/**".to_string(),
68 "*.swp".to_string(),
69 "*.swo".to_string(),
70 "*~".to_string(),
71 ".tangled".to_string(),
72 ".tangled/**".to_string(),
73 ]
74}
75
76/// Load custom ignore patterns from a .wispignore file in the given directory
77fn load_wispignore_file(dir_path: &Path) -> miette::Result<Vec<String>> {
78 let wispignore_path = dir_path.join(".wispignore");
79
80 if !wispignore_path.exists() {
81 return Ok(Vec::new());
82 }
83
84 let contents = std::fs::read_to_string(&wispignore_path).into_diagnostic()?;
85
86 // Parse gitignore-style file (one pattern per line, # for comments)
87 let patterns: Vec<String> = contents
88 .lines()
89 .filter_map(|line| {
90 let line = line.trim();
91 // Skip empty lines and comments
92 if line.is_empty() || line.starts_with('#') {
93 None
94 } else {
95 Some(line.to_string())
96 }
97 })
98 .collect();
99
100 if !patterns.is_empty() {
101 println!("Loaded {} custom patterns from .wispignore", patterns.len());
102 }
103
104 Ok(patterns)
105}
106
107/// Build a GlobSet from a list of patterns
108fn build_globset(patterns: Vec<String>) -> miette::Result<GlobSet> {
109 let mut builder = GlobSetBuilder::new();
110
111 for pattern in patterns {
112 let glob = Glob::new(&pattern).into_diagnostic()?;
113 builder.add(glob);
114 }
115
116 let globset = builder.build().into_diagnostic()?;
117 Ok(globset)
118}
119
120/// IgnoreMatcher handles checking if paths should be ignored
121pub struct IgnoreMatcher {
122 globset: GlobSet,
123}
124
125impl IgnoreMatcher {
126 /// Create a new IgnoreMatcher for the given directory
127 /// Loads default patterns and any custom .wispignore file
128 pub fn new(dir_path: &Path) -> miette::Result<Self> {
129 let mut all_patterns = load_default_patterns()?;
130
131 // Load custom patterns from .wispignore
132 let custom_patterns = load_wispignore_file(dir_path)?;
133 all_patterns.extend(custom_patterns);
134
135 let globset = build_globset(all_patterns)?;
136
137 Ok(Self { globset })
138 }
139
140 /// Check if the given path (relative to site root) should be ignored
141 pub fn is_ignored(&self, path: &str) -> bool {
142 self.globset.is_match(path)
143 }
144
145 /// Check if a filename should be ignored (checks just the filename, not full path)
146 pub fn is_filename_ignored(&self, filename: &str) -> bool {
147 self.globset.is_match(filename)
148 }
149}