1use crate::config::{Config, ListType, TimeFormat};
2use chrono::{NaiveDate, NaiveTime, Timelike};
3use lazy_static::lazy_static;
4use regex::Regex;
5use std::path::PathBuf;
6
7lazy_static! {
8 static ref TIME_PATTERN: Regex =
9 Regex::new(r"^(?:[-*]\s*)?(\d{2}:\d{2}(?::\d{2})?(?:\s*[AaPp][Mm])?)\s*(.+)$").unwrap();
10}
11
12pub fn format_time(time: NaiveTime, format: &TimeFormat) -> String {
14 match format {
15 TimeFormat::Hour24 => time.format("%H:%M:%S").to_string(),
16 TimeFormat::Hour12 => {
17 let hour = time.hour();
18 let minute = time.minute();
19 let second = time.second();
20 let period = if hour < 12 { "AM" } else { "PM" };
21 let hour12 = match hour {
22 0 => 12,
23 13..=23 => hour - 12,
24 _ => hour,
25 };
26 format!("{:02}:{:02}:{:02} {}", hour12, minute, second, period)
27 }
28 }
29}
30
31pub fn parse_time(time_str: &str) -> Option<NaiveTime> {
34 if time_str.matches(':').count() >= 2 {
37 let parts: Vec<&str> = time_str.split(':').collect();
39 if parts.len() >= 3 {
40 if let Ok(seconds) = parts[2]
41 .split_whitespace()
42 .next()
43 .unwrap_or("")
44 .parse::<u32>()
45 {
46 if seconds >= 60 {
47 return None;
48 }
49 }
50 }
51 }
52
53 if let Ok(time) = NaiveTime::parse_from_str(time_str, "%H:%M:%S") {
54 return Some(time);
55 }
56
57 if let Ok(time) = NaiveTime::parse_from_str(time_str, "%H:%M") {
59 return Some(NaiveTime::from_hms_opt(time.hour(), time.minute(), 0).unwrap());
60 }
61
62 let formats_with_seconds = vec![
64 "%I:%M:%S %p", "%I:%M:%S%p", "%l:%M:%S %p", "%l:%M:%S%p", ];
69
70 for format in formats_with_seconds {
71 if let Ok(time) = NaiveTime::parse_from_str(&time_str.to_uppercase(), format) {
72 if time.second() >= 60 {
74 continue;
75 }
76 return Some(time);
77 }
78 }
79
80 let formats = vec![
82 "%I:%M %p", "%I:%M%p", "%l:%M %p", "%l:%M%p", ];
87
88 for format in formats {
89 if let Ok(time) = NaiveTime::parse_from_str(&time_str.to_uppercase(), format) {
90 return Some(NaiveTime::from_hms_opt(time.hour(), time.minute(), 0).unwrap());
91 }
92 }
93
94 None
95}
96
97pub fn get_log_path_for_date(date: NaiveDate, config: &Config) -> PathBuf {
100 let mut path = PathBuf::from(&config.vault);
101
102 let year = date.format("%Y").to_string();
103 let month = date.format("%m").to_string();
104 let date_str = date.format("%Y-%m-%d").to_string();
105
106 let file_path = config
107 .file_path_format
108 .replace("{year}", &year)
109 .replace("{month}", &month)
110 .replace("{date}", &date_str);
111
112 path.push(file_path);
113 path
114}
115
116fn format_table_row(timestamp: &str, entry: &str, time_width: usize, entry_width: usize) -> String {
118 format!(
119 "| {:<width_t$} | {:<width_e$} |",
120 timestamp,
121 entry,
122 width_t = time_width,
123 width_e = entry_width
124 )
125}
126
127fn format_table_separator(time_width: usize, entry_width: usize) -> String {
129 format!(
130 "|{}|{}|",
131 "-".repeat(time_width + 2),
132 "-".repeat(entry_width + 2)
133 )
134}
135
136fn parse_entry(entry: &str) -> (String, String) {
138 if entry.starts_with('|') {
139 let parts: Vec<&str> = entry.split('|').collect();
141 if parts.len() >= 4 {
142 return (parts[1].trim().to_string(), parts[2].trim().to_string());
143 }
144 } else if entry.starts_with(['*', '-']) {
145 let content = entry.trim_start_matches(['-', '*', ' ']);
147
148 let time_patterns = [
150 r"^(\d{1,2}:\d{2}:\d{2})\s+(.+)$",
152 r"^(\d{1,2}:\d{2})\s+(.+)$",
154 r"^(\d{1,2}:\d{2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
156 r"^(\d{1,2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
158 ];
159
160 for pattern in &time_patterns {
161 if let Ok(regex) = Regex::new(pattern) {
162 if let Some(captures) = regex.captures(content) {
163 let time = captures.get(1).unwrap().as_str().trim();
164 let entry_text = captures.get(2).unwrap().as_str().trim();
165 return (time.to_string(), entry_text.to_string());
166 }
167 }
168 }
169
170 if let Some(space_pos) = content.find(' ') {
172 if let Some(second_space) = content[space_pos + 1..].find(' ') {
173 return (
174 content[..space_pos + 1 + second_space].trim().to_string(),
175 content[space_pos + 1 + second_space + 1..]
176 .trim()
177 .to_string(),
178 );
179 }
180 }
181 }
182 (String::new(), String::new())
183}
184
185pub fn extract_log_entries(
189 content: &str,
190 section_header: &str,
191 list_type: &ListType,
192 config: &Config,
193 include_header: bool,
194) -> (String, String, Vec<String>, ListType) {
195 let mut before = String::new();
196 let mut after = String::new();
197 let mut entries = Vec::new();
198 let mut found_type = list_type.clone();
199 let mut in_section = false;
200 let mut found_section = false;
201
202 let lines = content.lines().peekable();
203 for line in lines {
204 if line.starts_with(section_header) {
205 found_section = true;
206 in_section = true;
207 before = before.trim_end().to_string() + "\n\n";
208 continue;
209 }
210
211 if in_section {
212 if line.starts_with("##") {
213 in_section = false;
214 after = line.to_string();
215 continue;
216 }
217
218 let trimmed = line.trim();
219 if !trimmed.is_empty() {
220 if trimmed.starts_with('|') {
221 found_type = ListType::Table;
222 } else if trimmed.starts_with(['*', '-']) {
223 found_type = ListType::Bullet;
224 }
225
226 if !trimmed.contains("---")
228 && trimmed != format!("| {} | {} |", config.time_label, config.event_label)
229 {
230 entries.push(line.to_string());
231 }
232 }
233 } else if !found_section {
234 before.push_str(line);
235 before.push('\n');
236 } else if !line.is_empty() {
237 after.push('\n');
238 after.push_str(line);
239 }
240 }
241
242 if found_type != *list_type {
244 let mut converted_entries = Vec::new();
245
246 if *list_type == ListType::Table {
247 let mut max_time_width = config.time_label.len();
249 let mut max_entry_width = config.event_label.len();
250
251 for entry in &entries {
253 let (time, text) = parse_entry(entry);
254 let formatted_time = if let Some(parsed_time) = parse_time(&time) {
256 format_time(parsed_time, &config.time_format)
257 } else {
258 time
259 };
260 max_time_width = max_time_width.max(formatted_time.len());
261 max_entry_width = max_entry_width.max(text.len());
262 }
263
264 if include_header {
266 converted_entries.push(format_table_row(
267 &config.time_label,
268 &config.event_label,
269 max_time_width,
270 max_entry_width,
271 ));
272 converted_entries.push(format_table_separator(max_time_width, max_entry_width));
273 }
274
275 for entry in entries {
277 let (time, text) = parse_entry(&entry);
278 let formatted_time = if let Some(parsed_time) = parse_time(&time) {
280 format_time(parsed_time, &config.time_format)
281 } else {
282 time
283 };
284 converted_entries.push(format_table_row(
285 &formatted_time,
286 &text,
287 max_time_width,
288 max_entry_width,
289 ));
290 }
291 } else {
292 if include_header {
295 converted_entries.push(format!(
296 "<!-- {} | {} -->",
297 config.time_label, config.event_label
298 ));
299 }
300
301 for entry in entries {
302 let (time, text) = parse_entry(&entry);
303 if !time.is_empty() && !text.is_empty() {
304 let formatted_time = if let Some(parsed_time) = parse_time(&time) {
306 format_time(parsed_time, &config.time_format)
307 } else {
308 time
309 };
310 converted_entries.push(format!("- {} {}", formatted_time, text));
311 }
312 }
313 }
314
315 entries = converted_entries;
316 } else if *list_type == ListType::Table
317 && found_type == ListType::Table
318 && include_header
319 {
320 let mut max_time_width = config.time_label.len();
323 let mut max_entry_width = config.event_label.len();
324
325 for entry in &entries {
327 let (time, text) = parse_entry(entry);
328 max_time_width = max_time_width.max(time.len());
329 max_entry_width = max_entry_width.max(text.len());
330 }
331
332 let mut rebuilt_entries = Vec::new();
334 rebuilt_entries.push(format_table_row(
335 &config.time_label,
336 &config.event_label,
337 max_time_width,
338 max_entry_width,
339 ));
340 rebuilt_entries.push(format_table_separator(max_time_width, max_entry_width));
341
342 for entry in entries {
344 let (time, text) = parse_entry(&entry);
345 if !time.is_empty() && !text.is_empty() {
346 rebuilt_entries.push(format_table_row(
347 &time,
348 &text,
349 max_time_width,
350 max_entry_width,
351 ));
352 }
353 }
354
355 entries = rebuilt_entries;
356 }
359
360 (before, after, entries, found_type)
361}