1use std::path::Path;
2
3use crate::Result;
4use chrono::{DateTime, Datelike as _, Local, Timelike as _};
5use once_cell::sync::Lazy;
6use regex::Regex;
7
8pub trait MyParseFormat {
10 fn parse_all(&self) -> Result<String>;
12 fn parse_format(&self) -> Result<String>;
14 fn parse_replace<F>(&self, start: char, end: char, match_value: F) -> Result<String>
16 where
17 F: Fn(String) -> String;
18 fn parse_replace_zh<F>(&self, start: char, end: char, match_value: F) -> Result<String>
20 where
21 F: Fn(String) -> String;
22 fn parse_env(&self) -> Result<String>;
25 fn parse_path(&self) -> String;
27}
28
29pub static REGEX_FORMAT_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"\{([a-zA-Z0-9_]+)\}").expect("Invalid regex pattern for REGEX_FORMAT_PATTERN"));
31
32#[cfg(target_os = "windows")]
33pub static REGEX_ENV_PATTERN: Lazy<Regex> =
35 Lazy::new(|| Regex::new(r"\%([a-zA-Z0-9_]+)\%").expect("Invalid regex pattern for REGEX_ENV_PATTERN on Windows"));
36
37#[cfg(any(target_os = "linux", target_os = "macos"))]
38pub static REGEX_ENV_PATTERN: Lazy<Regex> =
40 Lazy::new(|| Regex::new(r"\$\{([a-zA-Z0-9_]+)\}").expect("Invalid regex pattern for REGEX_ENV_PATTERN on Unix-like systems"));
41
42#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
43pub static REGEX_ENV_PATTERN: Lazy<Regex> =
45 Lazy::new(|| Regex::new(r"\$([a-zA-Z0-9_]+)").expect("Invalid regex pattern for REGEX_ENV_PATTERN on other systems"));
46
47impl<S: AsRef<str>> MyParseFormat for S {
48 fn parse_all(&self) -> Result<String> {
49 self.as_ref().parse_format()?.parse_path().parse_env()
50 }
51
52 fn parse_format(&self) -> Result<String> {
53 let local: DateTime<Local> = Local::now();
54 Ok(
55 REGEX_FORMAT_PATTERN
56 .replace_all(self.as_ref(), |caps: ®ex::Captures<'_>| {
57 let key: String = caps.get(1).map_or("", |m| m.as_str()).to_lowercase();
58 match &*key {
59 "date" => format!("{:04}-{:02}-{:02}", local.year(), local.month(), local.day()),
61 "time" => format!("{:02}:{:02}:{:02}", local.hour(), local.minute(), local.second()),
63 "timestamp-millis" => local.timestamp_millis().to_string(),
65 "day" => format!("{:02}", local.day()),
66 "month" => format!("{:02}", local.month()),
67 "year" => format!("{:02}", local.year()),
68 "hour" => format!("{:02}", local.hour()),
69 "minute" => format!("{:02}", local.minute()),
70 "second" => format!("{:02}", local.second()),
71 "cwd" => std::env::current_dir().and_then(|x| Ok(x.display().to_string())).unwrap_or_default(),
72 "nanoid" => {
73 #[cfg(feature = "algorithm")]
74 return crate::algorithm!(nanoid 12);
75 #[cfg(not(feature = "algorithm"))]
76 return String::new();
77 }
78 _ => String::new(),
79 }
80 })
81 .to_string(),
82 )
83 }
84
85 fn parse_env(&self) -> Result<String> {
86 Ok(
87 REGEX_ENV_PATTERN
88 .replace_all(self.as_ref(), |caps: ®ex::Captures<'_>| {
89 let key = caps.get(1).map_or("", |m| m.as_str());
90 let var = std::env::var(key).unwrap_or_default();
91 var
92 })
93 .to_string(),
94 )
95 }
96
97 fn parse_path(&self) -> String {
98 self.as_ref().replace("\\\\", "/").replace("\\", "/")
99 }
100
101 fn parse_replace<F>(&self, start: char, end: char, match_callback: F) -> Result<String>
102 where
103 F: Fn(String) -> String,
104 {
105 let re = regex::Regex::new(&format!(r"\{}([a-zA-Z0-9_]+)\{}", start, end))?;
106 Ok(
107 re.replace_all(self.as_ref(), |caps: ®ex::Captures<'_>| {
108 let key: String = caps.get(1).map_or("", |m| m.as_str()).to_lowercase();
109 match_callback(key)
110 })
111 .to_string(),
112 )
113 }
114 fn parse_replace_zh<F>(&self, start: char, end: char, match_callback: F) -> Result<String>
115 where
116 F: Fn(String) -> String,
117 {
118 let re = regex::Regex::new(&format!(r"\{}([\p{{Han}}a-zA-Z0-9_@$]+)\{}", start, end))?;
119 Ok(
120 re.replace_all(self.as_ref(), |caps: ®ex::Captures<'_>| {
121 let key: String = caps.get(1).map_or("", |m| m.as_str()).to_string();
122 match_callback(key)
123 })
124 .to_string(),
125 )
126 }
127}
128
129impl MyParseFormat for Path {
130 fn parse_format(&self) -> Result<String> {
131 self.to_string_lossy().parse_format()
132 }
133
134 fn parse_env(&self) -> Result<String> {
135 self.to_string_lossy().parse_env()
136 }
137
138 fn parse_all(&self) -> Result<String> {
139 self.to_string_lossy().parse_all()
140 }
141
142 fn parse_path(&self) -> String {
143 self.to_string_lossy().parse_path()
144 }
145
146 fn parse_replace<F>(&self, start: char, end: char, match_value: F) -> Result<String>
147 where
148 F: Fn(String) -> String,
149 {
150 self.to_string_lossy().parse_replace(start, end, match_value)
151 }
152 fn parse_replace_zh<F>(&self, start: char, end: char, match_value: F) -> Result<String>
153 where
154 F: Fn(String) -> String,
155 {
156 self.to_string_lossy().parse_replace_zh(start, end, match_value)
157 }
158}
159
160#[cfg(test)]
163mod tests {
164 use super::*;
165 use std::env;
166
167 #[test]
168 fn test_parse_all() {
169 env::set_var("TEST_ENV", "test_value");
170 let input = "Date: {date}, Time: {time}, Env: %TEST_ENV%";
171 let result = input.parse_all().unwrap();
172 assert!(result.contains("Date: "));
173 assert!(result.contains("Time: "));
174 assert!(result.contains("Env: test_value"));
175 }
176
177 #[test]
178 fn test_parse_format() {
179 let input = "{date} {time} {day} {month} {year} {hour} {minute} {second} {cwd}";
180 let result = input.parse_format().unwrap();
181 println!("{result}");
182 assert!(!result.contains("{"));
183 assert!(!result.contains("}"));
184 }
185
186 #[test]
187 fn test_parse_replace() {
188 let input = "Hello [name] [age]";
189 let result = input
190 .parse_replace('[', ']', |key| match key.as_str() {
191 "name" => "Alice".to_string(),
192 "age" => "30".to_string(),
193 _ => key,
194 })
195 .unwrap();
196 assert_eq!(result, "Hello Alice 30");
197 }
198
199 #[test]
200 fn test_parse_env() {
201 env::set_var("TEST_VAR", "test_value");
202 #[cfg(target_os = "windows")]
203 let input = "Path: %TEST_VAR%";
204 #[cfg(any(target_os = "linux", target_os = "macos"))]
205 let input = "Path: ${TEST_VAR}";
206 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
207 let input = "Path: $TEST_VAR";
208
209 let result = input.parse_env().unwrap();
210 assert_eq!(result, "Path: test_value");
211 }
212
213 #[test]
214 fn test_parse_path() {
215 let input = r"C:\Users\Alice\Documents";
216 let result = input.parse_path();
217 assert_eq!(result, "C:/Users/Alice/Documents");
218 }
219
220 #[test]
221 fn test_path_parse_format() {
222 let path = Path::new("/home/user/{date}");
223 let result = path.parse_format().unwrap();
224 assert!(!result.contains("{date}"));
225 }
226
227 #[test]
228 fn test_path_parse_env() {
229 env::set_var("HOME", "/home/user");
230 #[cfg(target_os = "windows")]
231 let path = Path::new("%HOME%\\documents");
232 #[cfg(any(target_os = "linux", target_os = "macos"))]
233 let path = Path::new("${HOME}/documents");
234 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
235 let path = Path::new("$HOME/documents");
236
237 let result = path.parse_env().unwrap();
238 assert!(result.contains("/home/user"));
239 }
240
241 #[test]
242 fn test_path_parse_path() {
243 let path = Path::new("C:\\Users\\Alice\\Documents");
244 let result = path.parse_path();
245 assert_eq!(result, "C:/Users/Alice/Documents");
246 }
247
248 #[test]
249 fn test_path_parse_replace() {
250 let path = Path::new("/home/[user]/[folder]");
251 let result = path
252 .parse_replace('[', ']', |key| match key.as_str() {
253 "user" => "alice".to_string(),
254 "folder" => "documents".to_string(),
255 _ => key,
256 })
257 .unwrap();
258 assert_eq!(result, "/home/alice/documents");
259 }
260}