obsidian_logging/
utils.rs1use chrono::{NaiveDate, NaiveTime, Timelike};
2use std::path::PathBuf;
3use crate::config::{ListType, Config, TimeFormat};
4use regex::Regex;
5use lazy_static::lazy_static;
6
7lazy_static! {
8 static ref TIME_PATTERN: Regex = Regex::new(r"^(?:[-*]\s*)?(\d{2}:\d{2}(?:\s*[AaPp][Mm])?)\s*(.+)$").unwrap();
9}
10
11pub fn format_time(time: NaiveTime, format: &TimeFormat) -> String {
13 match format {
14 TimeFormat::Hour24 => time.format("%H:%M").to_string(),
15 TimeFormat::Hour12 => {
16 let hour = time.hour();
17 let minute = time.minute();
18 let period = if hour < 12 { "AM" } else { "PM" };
19 let hour12 = match hour {
20 0 => 12,
21 13..=23 => hour - 12,
22 _ => hour,
23 };
24 format!("{:02}:{:02} {}", hour12, minute, period)
25 }
26 }
27}
28
29pub fn parse_time(time_str: &str) -> Option<NaiveTime> {
31 if let Ok(time) = NaiveTime::parse_from_str(time_str, "%H:%M") {
33 return Some(time);
34 }
35
36 let formats = vec![
38 "%I:%M %p", "%I:%M%p", "%l:%M %p", "%l:%M%p", ];
43
44 for format in formats {
45 if let Ok(time) = NaiveTime::parse_from_str(&time_str.to_uppercase(), format) {
46 return Some(time);
47 }
48 }
49
50 None
51}
52
53pub fn get_log_path_for_date(date: NaiveDate, config: &Config) -> PathBuf {
56 let mut path = PathBuf::from(&config.vault);
57
58 let year = date.format("%Y").to_string();
59 let month = date.format("%m").to_string();
60 let date_str = date.format("%Y-%m-%d").to_string();
61
62 let file_path = config.file_path_format
63 .replace("{year}", &year)
64 .replace("{month}", &month)
65 .replace("{date}", &date_str);
66
67 path.push(file_path);
68 path
69}
70
71fn format_table_row(timestamp: &str, entry: &str, time_width: usize, entry_width: usize) -> String {
73 format!("| {:<width_t$} | {:<width_e$} |",
74 timestamp, entry,
75 width_t = time_width,
76 width_e = entry_width)
77}
78
79fn format_table_separator(time_width: usize, entry_width: usize) -> String {
81 format!("|{}|{}|",
82 "-".repeat(time_width + 2),
83 "-".repeat(entry_width + 2))
84}
85
86fn parse_entry(entry: &str) -> (String, String) {
88 if entry.starts_with('|') {
89 let parts: Vec<&str> = entry.split('|').collect();
91 if parts.len() >= 4 {
92 return (parts[1].trim().to_string(), parts[2].trim().to_string());
93 }
94 } else if entry.starts_with(['*', '-']) {
95 let content = entry.trim_start_matches(|c| c == '-' || c == '*' || c == ' ');
97
98 let time_patterns = [
100 r"^(\d{1,2}:\d{2})\s+(.+)$",
102 r"^(\d{1,2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
104 ];
105
106 for pattern in &time_patterns {
107 if let Ok(regex) = Regex::new(pattern) {
108 if let Some(captures) = regex.captures(content) {
109 let time = captures.get(1).unwrap().as_str().trim();
110 let entry_text = captures.get(2).unwrap().as_str().trim();
111 return (time.to_string(), entry_text.to_string());
112 }
113 }
114 }
115
116 if let Some(space_pos) = content.find(' ') {
118 if let Some(second_space) = content[space_pos + 1..].find(' ') {
119 return (content[..space_pos + 1 + second_space].trim().to_string(),
120 content[space_pos + 1 + second_space + 1..].trim().to_string());
121 }
122 }
123 }
124 (String::new(), String::new())
125}
126
127pub fn extract_log_entries(content: &str, section_header: &str, list_type: &ListType, config: &Config, include_header: bool) -> (String, String, Vec<String>, ListType) {
131 let mut before = String::new();
132 let mut after = String::new();
133 let mut entries = Vec::new();
134 let mut found_type = list_type.clone();
135 let mut in_section = false;
136 let mut found_section = false;
137
138 let mut lines = content.lines().peekable();
139 while let Some(line) = lines.next() {
140 if line.starts_with(section_header) {
141 found_section = true;
142 in_section = true;
143 before = before.trim_end().to_string() + "\n\n";
144 continue;
145 }
146
147 if in_section {
148 if line.starts_with("##") {
149 in_section = false;
150 after = line.to_string();
151 continue;
152 }
153
154 let trimmed = line.trim();
155 if !trimmed.is_empty() {
156 if trimmed.starts_with('|') {
157 found_type = ListType::Table;
158 } else if trimmed.starts_with(['*', '-']) {
159 found_type = ListType::Bullet;
160 }
161
162 if !trimmed.contains("---") && trimmed != format!("| {} | {} |", config.time_label, config.event_label) {
164 entries.push(line.to_string());
165 }
166 }
167 } else if !found_section {
168 before.push_str(line);
169 before.push('\n');
170 } else if !line.is_empty() {
171 after.push('\n');
172 after.push_str(line);
173 }
174 }
175
176 if found_type != *list_type {
178 let mut converted_entries = Vec::new();
179
180 if *list_type == ListType::Table {
181 let mut max_time_width = config.time_label.len();
183 let mut max_entry_width = config.event_label.len();
184
185 for entry in &entries {
187 let (time, text) = parse_entry(entry);
188 let formatted_time = if let Some(parsed_time) = parse_time(&time) {
190 format_time(parsed_time, &config.time_format)
191 } else {
192 time
193 };
194 max_time_width = max_time_width.max(formatted_time.len());
195 max_entry_width = max_entry_width.max(text.len());
196 }
197
198 if include_header {
200 converted_entries.push(format_table_row(&config.time_label, &config.event_label, max_time_width, max_entry_width));
201 converted_entries.push(format_table_separator(max_time_width, max_entry_width));
202 }
203
204 for entry in entries {
206 let (time, text) = parse_entry(&entry);
207 let formatted_time = if let Some(parsed_time) = parse_time(&time) {
209 format_time(parsed_time, &config.time_format)
210 } else {
211 time
212 };
213 converted_entries.push(format_table_row(&formatted_time, &text, max_time_width, max_entry_width));
214 }
215 } else {
216 if include_header {
219 converted_entries.push(format!("<!-- {} | {} -->", config.time_label, config.event_label));
220 }
221
222 for entry in entries {
223 let (time, text) = parse_entry(&entry);
224 if !time.is_empty() && !text.is_empty() {
225 let formatted_time = if let Some(parsed_time) = parse_time(&time) {
227 format_time(parsed_time, &config.time_format)
228 } else {
229 time
230 };
231 converted_entries.push(format!("- {} {}", formatted_time, text));
232 }
233 }
234 }
235
236 entries = converted_entries;
237 } else {
238 if *list_type == ListType::Table && found_type == ListType::Table {
240 if include_header {
241 let mut max_time_width = config.time_label.len();
243 let mut max_entry_width = config.event_label.len();
244
245 for entry in &entries {
247 let (time, text) = parse_entry(entry);
248 max_time_width = max_time_width.max(time.len());
249 max_entry_width = max_entry_width.max(text.len());
250 }
251
252 let mut rebuilt_entries = Vec::new();
254 rebuilt_entries.push(format_table_row(&config.time_label, &config.event_label, max_time_width, max_entry_width));
255 rebuilt_entries.push(format_table_separator(max_time_width, max_entry_width));
256
257 for entry in entries {
259 let (time, text) = parse_entry(&entry);
260 if !time.is_empty() && !text.is_empty() {
261 rebuilt_entries.push(format_table_row(&time, &text, max_time_width, max_entry_width));
262 }
263 }
264
265 entries = rebuilt_entries;
266 }
267 }
269 }
271
272 (before, after, entries, found_type)
273}
274