1use crate::config::{VaultConfig, NoteFormat};
2use crate::date_time::DateTimeHandler;
3use crate::errors::JourneyError;
4use chrono::{DateTime, Local, NaiveDate, Datelike, Weekday};
5use std::fs;
6use std::path::PathBuf;
7
8pub struct Vault {
9 pub config: VaultConfig,
10 pub date_handler: DateTimeHandler,
11}
12
13impl Vault {
14 pub fn new(mut config: VaultConfig) -> Self {
15 if config.list_type.is_none() {
17 if let Some(fmt) = config.note_format.clone() {
18 config.list_type = Some(fmt);
19 }
20 }
21 let date_handler = DateTimeHandler::new(config.locale.clone());
22 Self {
23 config,
24 date_handler,
25 }
26 }
27
28 pub fn get_table_headers(&self) -> (String, String) {
30 if let Some(ref custom_headers) = self.config.table_headers {
32 return (custom_headers.time.clone(), custom_headers.content.clone());
33 }
34
35 let locale = &self.config.locale;
36
37 if locale.starts_with("no_") || locale.starts_with("nb_") || locale.starts_with("nn_") {
39 ("Tid".to_string(), "Innhold".to_string())
40 }
41 else if locale.starts_with("sv_") {
43 ("Tid".to_string(), "Innehåll".to_string())
44 }
45 else if locale.starts_with("da_") {
47 ("Tid".to_string(), "Indhold".to_string())
48 }
49 else if locale.starts_with("fi_") {
51 ("Aika".to_string(), "Sisältö".to_string())
52 }
53 else if locale.starts_with("de_") {
55 ("Zeit".to_string(), "Inhalt".to_string())
56 }
57 else if locale.starts_with("fr_") {
59 ("Heure".to_string(), "Contenu".to_string())
60 }
61 else if locale.starts_with("es_") {
63 ("Hora".to_string(), "Contenido".to_string())
64 }
65 else if locale.starts_with("it_") {
67 ("Ora".to_string(), "Contenuto".to_string())
68 }
69 else if locale.starts_with("nl_") {
71 ("Tijd".to_string(), "Inhoud".to_string())
72 }
73 else if locale.starts_with("pt_") {
75 ("Hora".to_string(), "Conteúdo".to_string())
76 }
77 else if locale.starts_with("ru_") {
79 ("Время".to_string(), "Содержание".to_string())
80 }
81 else if locale.starts_with("ja_") {
83 ("時間".to_string(), "内容".to_string())
84 }
85 else if locale.starts_with("zh_") {
87 ("时间".to_string(), "内容".to_string())
88 }
89 else {
91 ("Time".to_string(), "Content".to_string())
92 }
93 }
94
95 pub fn get_note_path(&self, date: NaiveDate) -> PathBuf {
96 if let Some(ref format) = self.config.file_path_format {
97 let formatted_path = self.format_custom_path(format, date);
99 self.config.path.join(formatted_path)
100 } else {
101 let date_str = self.date_handler.format_date(date);
103 self.config.path.join(format!("{}.md", date_str))
104 }
105 }
106
107 pub fn add_note(&self, content: &str, timestamp: Option<DateTime<Local>>) -> Result<(), JourneyError> {
109 self.add_note_with_category(content, timestamp, None)
110 }
111
112 pub fn add_note_with_category(&self, content: &str, timestamp: Option<DateTime<Local>>, category: Option<&str>) -> Result<(), JourneyError> {
114 let timestamp = timestamp.unwrap_or_else(|| self.date_handler.get_current_datetime());
115 let date = timestamp.date_naive();
116 let note_path = self.get_note_path(date);
117
118 if let Some(parent) = note_path.parent() {
120 fs::create_dir_all(parent)?;
121 }
122
123 let expanded_content = self.expand_phrases(content);
125 let formatted_time = self.date_handler.format_datetime(timestamp);
126
127 let note_format = self.config.list_type.as_ref().unwrap_or(&NoteFormat::Bullet);
129 let note_entry = self.format_note_entry(&formatted_time, &expanded_content, note_format);
130
131 if note_path.exists() {
133 let existing_content = fs::read_to_string(¬e_path)?;
134
135 let converted_content = self.convert_note_format_if_needed(&existing_content, note_format)?;
137
138 if let Some(section_name) = self.config.get_section_header(category) {
140 if let Some(section_start) = self.find_section(&converted_content, section_name) {
141 let mut lines: Vec<&str> = converted_content.lines().collect();
142
143 let insertion_index = self.find_insertion_index(
145 &lines,
146 section_start,
147 &formatted_time,
148 self.config.list_type.as_ref().unwrap_or(&NoteFormat::Bullet)
149 );
150
151 lines.insert(insertion_index, ¬e_entry);
153
154 fs::write(¬e_path, lines.join("\n"))?;
155 return Ok(());
156 } else {
157 let mut new_content = converted_content;
159 if !new_content.ends_with('\n') {
160 new_content.push('\n');
161 }
162 new_content.push_str(&format!("\n# {}\n", section_name));
163 new_content.push_str(¬e_entry);
164 fs::write(¬e_path, new_content)?;
165 return Ok(());
166 }
167 }
168
169 let mut lines: Vec<&str> = converted_content.lines().collect();
171 let insertion_index = self.find_insertion_index(
172 &lines,
173 0,
174 &formatted_time,
175 self.config.list_type.as_ref().unwrap_or(&NoteFormat::Bullet)
176 );
177 lines.insert(insertion_index, ¬e_entry);
178 fs::write(¬e_path, lines.join("\n"))?;
179 } else {
180 let file_content = if let Some(template_file) = &self.config.template_file {
182 self.create_file_from_template(template_file, timestamp, ¬e_entry)?
184 } else {
185 self.create_default_file_content(date, ¬e_entry, category)
187 };
188
189 fs::write(¬e_path, file_content)?;
190 }
191
192 Ok(())
193 }
194
195 fn create_default_file_content(&self, date: NaiveDate, note_entry: &str, category: Option<&str>) -> String {
196 let mut file_content = String::new();
197
198 file_content.push_str("---\n");
200 file_content.push_str(&format!("date: {}\n", self.date_handler.format_date(date)));
201 file_content.push_str("---\n\n");
202
203 if let Some(section_name) = self.config.get_section_header(category) {
205 file_content.push_str(&format!("# {}\n\n", section_name));
206 }
207
208 let note_format = self.config.list_type.as_ref().unwrap_or(&NoteFormat::Bullet);
210 if note_format == &NoteFormat::Table {
211 let (time_header, content_header) = self.get_table_headers();
212 file_content.push_str(&format!("| {} | {} |\n", time_header, content_header));
213 file_content.push_str("|------|----------|\n");
214 }
215
216 file_content.push_str(note_entry);
217 file_content
218 }
219
220 fn is_table_header_line(&self, line: &str) -> bool {
222 let trimmed = line.trim();
223
224 if trimmed.starts_with("|---") {
226 return true;
227 }
228
229 if trimmed.starts_with('|') {
231 let (time_header, content_header) = self.get_table_headers();
232 let expected = format!("| {} | {} |", time_header, content_header);
233 if trimmed == expected {
234 return true;
235 }
236 }
237
238 false
239 }
240
241 fn create_file_from_template(&self, template_file: &str, timestamp: DateTime<Local>, note_entry: &str) -> Result<String, JourneyError> {
242 let template_path = PathBuf::from(template_file);
244 let template_content = fs::read_to_string(&template_path)
245 .map_err(|e| JourneyError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, format!("Failed to read template file '{}': {}", template_file, e))))?;
246
247 let mut processed_content = template_content;
249
250 let date = timestamp.date_naive();
252 processed_content = processed_content.replace("{{date}}", &self.date_handler.format_date(date));
253 processed_content = processed_content.replace("{{time}}", &self.date_handler.format_time(timestamp.time()));
254 processed_content = processed_content.replace("{{datetime}}", &self.date_handler.format_datetime(timestamp));
255
256 let yesterday_date = date - chrono::Duration::days(1);
259 let tomorrow_date = date + chrono::Duration::days(1);
260 let yesterday = self.date_handler.format_date(yesterday_date);
261 let tomorrow = self.date_handler.format_date(tomorrow_date);
262 processed_content = processed_content.replace("{yesterday}", &yesterday);
263 processed_content = processed_content.replace("{tomorrow}", &tomorrow);
264 processed_content = processed_content.replace("{{yesterday}}", &yesterday);
265 processed_content = processed_content.replace("{{tomorrow}}", &tomorrow);
266
267 let weekday = date.format("%A").to_string();
269 processed_content = processed_content.replace("{weekday}", &weekday);
270 processed_content = processed_content.replace("{{weekday}}", &weekday);
271
272 let weekday_short = date.format("%a").to_string();
274 processed_content = processed_content.replace("{Weekday}", &weekday_short);
275 processed_content = processed_content.replace("{{Weekday}}", &weekday_short);
276
277 let created = timestamp.format("%Y-%m-%d %H:%M:%S").to_string();
280 processed_content = processed_content.replace("{created}", &created);
281 processed_content = processed_content.replace("{{created}}", &created);
282
283 let today = self.date_handler.format_date(date);
285 processed_content = processed_content.replace("{today}", &today);
286 processed_content = processed_content.replace("{{today}}", &today);
287
288 if let Some(section_title) = self.config.section_header.as_ref().or(self.config.section_name.as_ref()) {
290 processed_content = processed_content.replace("{{section_header}}", section_title);
291 processed_content = processed_content.replace("{section_header}", section_title);
292 processed_content = processed_content.replace("{{section_name}}", section_title);
293 processed_content = processed_content.replace("{section_name}", section_title);
294 }
295
296 if !processed_content.contains("{{note}}") && !processed_content.contains("{note}") {
298 processed_content.push_str(note_entry);
299 } else {
300 processed_content = processed_content.replace("{{note}}", note_entry);
301 processed_content = processed_content.replace("{note}", note_entry);
302 }
303
304 Ok(processed_content)
305 }
306
307 pub fn find_section(&self, content: &str, section_name: &str) -> Option<usize> {
308 let lines: Vec<&str> = content.lines().collect();
309 for (i, line) in lines.iter().enumerate() {
310 if line.trim().starts_with('#') && line.contains(section_name) {
311 return Some(i);
312 }
313 }
314 None
315 }
316
317 pub fn find_section_end(&self, lines: &[&str], section_start: usize) -> usize {
318 for i in (section_start + 1)..lines.len() {
320 if lines[i].trim().starts_with('#') {
321 return i;
322 }
323 }
324 lines.len()
326 }
327
328 pub fn find_section_content_end(&self, lines: &[&str], section_start: usize) -> usize {
329 let section_end = self.find_section_end(lines, section_start);
330
331 let mut content_end = section_start + 1;
333 for i in section_start + 1..section_end {
334 if !lines[i].trim().is_empty() {
335 content_end = i + 1;
336 }
337 }
338
339 content_end
340 }
341
342 pub fn list_notes(&self, date: NaiveDate) -> Result<Vec<String>, JourneyError> {
344 self.list_notes_with_category(date, None)
345 }
346
347 pub fn list_notes_with_category(&self, date: NaiveDate, category: Option<&str>) -> Result<Vec<String>, JourneyError> {
349 let note_path = self.get_note_path(date);
350
351 if !note_path.exists() {
352 return Ok(vec![]);
353 }
354
355 let content = fs::read_to_string(¬e_path)?;
356 let lines: Vec<&str> = content.lines().collect();
357 let mut notes = Vec::new();
358
359 if let Some(section_name) = self.config.get_section_header(category) {
361 if let Some(section_start) = self.find_section(&content, section_name) {
362 let section_end = self.find_section_end(&lines, section_start);
363
364 for line in &lines[section_start..section_end] {
366 let trimmed = line.trim();
367 if trimmed.starts_with("- ") {
369 notes.push(line.to_string());
370 }
371 else if trimmed.starts_with("|") && !trimmed.starts_with("|---") && trimmed.contains("|") {
373 if !self.is_table_header_line(trimmed) {
375 notes.push(line.to_string());
376 }
377 }
378 }
379 }
380 } else {
382 for line in &lines {
384 let trimmed = line.trim();
385 if trimmed.starts_with("- ") {
387 notes.push(line.to_string());
388 }
389 else if trimmed.starts_with("|") && !trimmed.starts_with("|---") && trimmed.contains("|") {
391 if !self.is_table_header_line(trimmed) {
393 notes.push(line.to_string());
394 }
395 }
396 }
397 }
398
399 let note_format = self.config.list_type.as_ref().unwrap_or(&NoteFormat::Bullet);
401 notes.sort_by(|a, b| {
402 let ta = self.extract_time_from_note_line(a, note_format);
403 let tb = self.extract_time_from_note_line(b, note_format);
404
405 if let (Some(time_a), Some(time_b)) = (ta, tb) {
407 time_a.cmp(&time_b)
408 }
409 else if ta.is_some() {
411 std::cmp::Ordering::Less
412 }
413 else if tb.is_some() {
414 std::cmp::Ordering::Greater
415 }
416 else {
418 std::cmp::Ordering::Equal
419 }
420 });
421
422 Ok(notes)
423 }
424
425 pub fn get_editor_path(&self, date: NaiveDate) -> PathBuf {
426 self.get_note_path(date)
427 }
428
429 fn expand_phrases(&self, content: &str) -> String {
431 let mut result = content.to_string();
432
433 let mut phrases: Vec<_> = self.config.phrases.iter().collect();
435 phrases.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
436
437 for (phrase, replacement) in phrases {
438 result = result.replace(phrase, replacement);
440 }
441
442 result
443 }
444
445 fn format_custom_path(&self, format: &str, date: NaiveDate) -> String {
447 let year = date.year();
448 let month = date.month();
449 let day = date.day();
450 let weekday = date.weekday();
451
452 let mut result = format.to_string();
453
454 result = result.replace("{year}", &year.to_string());
456
457 result = result.replace("{month:02}", &format!("{:02}", month));
459 result = result.replace("{month}", &month.to_string());
460
461 result = result.replace("{{date:MM}}", &format!("{:02}", month));
464 result = result.replace("{date:MM}", &format!("{:02}", month));
465
466 result = result.replace("{{date:y}}", &format!("{:02}", year % 100));
469 result = result.replace("{date:y}", &format!("{:02}", year % 100));
470
471 result = result.replace("{day:02}", &format!("{:02}", day));
473 result = result.replace("{date:02}", &format!("{:02}", day));
474 result = result.replace("{day}", &day.to_string());
475 result = result.replace("{date}", &day.to_string());
476
477 result = result.replace("{Weekday}", &self.format_weekday(weekday, false));
479 result = result.replace("{weekday}", &self.format_weekday(weekday, false).to_lowercase());
480 result = result.replace("{Weekday_short}", &self.format_weekday(weekday, true));
481 result = result.replace("{weekday_short}", &self.format_weekday(weekday, true).to_lowercase());
482
483 result = result.replace("{Month}", &self.format_month(month, false));
485 result = result.replace("{month_name}", &self.format_month(month, false).to_lowercase());
486 result = result.replace("{Month_short}", &self.format_month(month, true));
487 result = result.replace("{month_short}", &self.format_month(month, true).to_lowercase());
488
489 result
490 }
491
492 fn format_weekday(&self, weekday: Weekday, short: bool) -> String {
494 match weekday {
495 Weekday::Mon => if short { "Mon".to_string() } else { "Monday".to_string() },
496 Weekday::Tue => if short { "Tue".to_string() } else { "Tuesday".to_string() },
497 Weekday::Wed => if short { "Wed".to_string() } else { "Wednesday".to_string() },
498 Weekday::Thu => if short { "Thu".to_string() } else { "Thursday".to_string() },
499 Weekday::Fri => if short { "Fri".to_string() } else { "Friday".to_string() },
500 Weekday::Sat => if short { "Sat".to_string() } else { "Saturday".to_string() },
501 Weekday::Sun => if short { "Sun".to_string() } else { "Sunday".to_string() },
502 }
503 }
504
505 fn format_month(&self, month: u32, short: bool) -> String {
507 match month {
508 1 => if short { "Jan".to_string() } else { "January".to_string() },
509 2 => if short { "Feb".to_string() } else { "February".to_string() },
510 3 => if short { "Mar".to_string() } else { "March".to_string() },
511 4 => if short { "Apr".to_string() } else { "April".to_string() },
512 5 => if short { "May".to_string() } else { "May".to_string() },
513 6 => if short { "Jun".to_string() } else { "June".to_string() },
514 7 => if short { "Jul".to_string() } else { "July".to_string() },
515 8 => if short { "Aug".to_string() } else { "August".to_string() },
516 9 => if short { "Sep".to_string() } else { "September".to_string() },
517 10 => if short { "Oct".to_string() } else { "October".to_string() },
518 11 => if short { "Nov".to_string() } else { "November".to_string() },
519 12 => if short { "Dec".to_string() } else { "December".to_string() },
520 _ => "Unknown".to_string(),
521 }
522 }
523
524 fn format_note_entry(&self, timestamp: &str, content: &str, format: &NoteFormat) -> String {
526 match format {
527 NoteFormat::Bullet => format!("- [{}] {}\n", timestamp, content),
528 NoteFormat::Table => format!("| {} | {} |\n", timestamp, content),
529 }
530 }
531
532 fn extract_time_from_note_line(&self, line: &str, format: &NoteFormat) -> Option<chrono::NaiveTime> {
534 match format {
535 NoteFormat::Bullet => {
536 let trimmed = line.trim_start();
538 if let Some(rest) = trimmed.strip_prefix("- ") {
539 if let Some(after_bracket) = rest.strip_prefix('[') {
540 if let Some(closing) = after_bracket.find(']') {
541 let time_str = &after_bracket[..closing];
542 chrono::NaiveTime::parse_from_str(time_str, "%H:%M:%S")
543 .or_else(|_| chrono::NaiveTime::parse_from_str(time_str, "%H:%M")).ok()
544 } else { None }
545 } else { None }
546 } else { None }
547 }
548 NoteFormat::Table => {
549 let trimmed = line.trim();
551 if trimmed.starts_with('|') {
552 let cols: Vec<&str> = trimmed.trim_matches('|').split('|').map(|s| s.trim()).collect();
553 if let Some(time_str) = cols.get(0) {
554 chrono::NaiveTime::parse_from_str(time_str, "%H:%M:%S")
555 .or_else(|_| chrono::NaiveTime::parse_from_str(time_str, "%H:%M")).ok()
556 } else { None }
557 } else { None }
558 }
559 }
560 }
561
562 fn find_insertion_index(&self, lines: &[&str], section_start: usize, new_time_str: &str, format: &NoteFormat) -> usize {
564 let section_end = if section_start == 0 { lines.len() } else { self.find_section_end(lines, section_start) };
565 let new_time = chrono::NaiveTime::parse_from_str(new_time_str, "%H:%M:%S")
566 .or_else(|_| chrono::NaiveTime::parse_from_str(new_time_str, "%H:%M")).ok();
567
568 let mut content_end = section_start + 1;
570 let mut insert_at = None;
571 for i in section_start + 1..section_end {
572 let trimmed = lines[i].trim();
573 if !trimmed.is_empty() { content_end = i + 1; }
574
575 let existing_time_opt = match format {
576 NoteFormat::Bullet => {
577 if trimmed.starts_with("- ") {
578 if let Some(rest) = trimmed.strip_prefix("- ") {
579 if let Some(after_bracket) = rest.strip_prefix('[') {
580 if let Some(closing) = after_bracket.find(']') {
581 let ts = &after_bracket[..closing];
582 chrono::NaiveTime::parse_from_str(ts, "%H:%M:%S")
583 .or_else(|_| chrono::NaiveTime::parse_from_str(ts, "%H:%M")).ok()
584 } else { None }
585 } else { None }
586 } else { None }
587 } else { None }
588 }
589 NoteFormat::Table => {
590 if trimmed.starts_with('|') && !trimmed.starts_with("|---") {
591 let cols: Vec<&str> = trimmed.trim_matches('|').split('|').map(|s| s.trim()).collect();
592 cols.get(0).and_then(|ts| chrono::NaiveTime::parse_from_str(ts, "%H:%M:%S")
593 .or_else(|_| chrono::NaiveTime::parse_from_str(ts, "%H:%M")).ok())
594 } else { None }
595 }
596 };
597
598 if let (Some(new_t), Some(old_t)) = (new_time, existing_time_opt) {
599 if new_t < old_t {
600 insert_at = Some(i);
601 break;
602 }
603 }
604 }
605
606 insert_at.unwrap_or(content_end)
607 }
608
609 pub fn detect_note_format(&self, content: &str) -> Option<NoteFormat> {
611 let lines: Vec<&str> = content.lines().collect();
612
613 let has_bullet_notes = lines.iter().any(|line| line.trim().starts_with("- "));
615
616 let has_table_notes = lines.iter().any(|line| {
618 let trimmed = line.trim();
619 trimmed.starts_with("|") && trimmed.contains("|") && !trimmed.starts_with("|---")
620 });
621
622 if has_bullet_notes && !has_table_notes {
623 Some(NoteFormat::Bullet)
624 } else if has_table_notes && !has_bullet_notes {
625 Some(NoteFormat::Table)
626 } else {
627 None }
629 }
630
631 fn convert_note_format_if_needed(&self, content: &str, target_format: &NoteFormat) -> Result<String, JourneyError> {
633 let current_format = self.detect_note_format(content);
634
635 if current_format.is_none() || current_format.as_ref() == Some(target_format) {
637 if target_format == &NoteFormat::Table {
638 return Ok(self.clean_table_blank_lines(content));
639 }
640 return Ok(content.to_string());
641 }
642
643 let lines: Vec<&str> = content.lines().collect();
644 let mut converted_lines = Vec::new();
645 let mut first_note_found = false;
646
647 for line in lines {
648 let trimmed = line.trim();
649
650 if target_format == &NoteFormat::Table && trimmed.starts_with("- ") {
652 if !first_note_found {
654 let (time_header, content_header) = self.get_table_headers();
655 converted_lines.push(format!("| {} | {} |", time_header, content_header));
656 converted_lines.push("|------|----------|".to_string());
657 first_note_found = true;
658 }
659
660 if let Some((timestamp, note_content)) = self.parse_bullet_note(trimmed) {
661 converted_lines.push(format!("| {} | {} |", timestamp, note_content));
662 } else {
663 converted_lines.push(line.to_string());
664 }
665 }
666 else if target_format == &NoteFormat::Table && trimmed.is_empty() && first_note_found {
668 continue;
670 }
671 else if target_format == &NoteFormat::Bullet && trimmed.starts_with("|") && !trimmed.starts_with("|---") && !trimmed.contains("Time") && !trimmed.contains("Content") {
673 if let Some((timestamp, note_content)) = self.parse_table_note(trimmed) {
674 converted_lines.push(format!("- [{}] {}", timestamp, note_content));
675 } else {
676 converted_lines.push(line.to_string());
677 }
678 }
679 else if target_format == &NoteFormat::Bullet && trimmed.starts_with("|") && (trimmed.contains("Time") || trimmed.contains("Content") || trimmed.starts_with("|---")) {
681 continue;
683 }
684 else if target_format == &NoteFormat::Bullet && trimmed.is_empty() {
686 continue;
688 }
689 else {
690 converted_lines.push(line.to_string());
691 }
692 }
693
694 Ok(converted_lines.join("\n"))
695 }
696
697 fn clean_table_blank_lines(&self, content: &str) -> String {
699 let lines: Vec<&str> = content.lines().collect();
700 let mut cleaned_lines = Vec::new();
701 let mut in_table = false;
702 let mut last_was_table_row = false;
703
704 for line in lines {
705 let trimmed = line.trim();
706
707 if trimmed.starts_with("|") && !trimmed.starts_with("|---") && !trimmed.contains("Time") && !trimmed.contains("Content") {
709 in_table = true;
710 last_was_table_row = true;
711 cleaned_lines.push(line.to_string());
712 }
713 else if trimmed.starts_with("|") && (trimmed.contains("Time") || trimmed.contains("Content") || trimmed.starts_with("|---")) {
715 in_table = true;
716 last_was_table_row = false;
717 cleaned_lines.push(line.to_string());
718 }
719 else if trimmed.is_empty() {
721 if !in_table || !last_was_table_row {
723 cleaned_lines.push(line.to_string());
724 }
725 last_was_table_row = false;
726 }
727 else {
729 in_table = false;
730 last_was_table_row = false;
731 cleaned_lines.push(line.to_string());
732 }
733 }
734
735 cleaned_lines.join("\n")
736 }
737
738 fn parse_bullet_note(&self, line: &str) -> Option<(String, String)> {
740 if let Some(after_dash) = line.trim_start().strip_prefix("- ") {
742 if let Some(after_bracket) = after_dash.strip_prefix('[') {
743 if let Some(closing) = after_bracket.find(']') {
744 let timestamp = after_bracket[..closing].to_string();
745 let content = after_bracket[closing + 1..].trim().to_string();
746 return Some((timestamp, content));
747 }
748 }
749 }
750 None
751 }
752
753 fn parse_table_note(&self, line: &str) -> Option<(String, String)> {
755 let parts: Vec<&str> = line.split('|').collect();
757 if parts.len() >= 3 {
758 let timestamp = parts[1].trim().to_string();
759 let content = parts[2].trim().to_string();
760 return Some((timestamp, content));
761 }
762 None
763 }
764}
765
766#[cfg(test)]
767mod tests {
768 use super::*;
769 use std::fs;
770 use std::collections::HashMap;
771 use tempfile::TempDir;
772
773 fn create_test_vault() -> (Vault, TempDir) {
774 let temp_dir = TempDir::new().unwrap();
775 let vault_path = temp_dir.path().join("test_vault");
776 fs::create_dir_all(&vault_path).unwrap();
777
778 let config = VaultConfig {
779 name: "test".to_string(),
780 path: vault_path,
781 locale: "en_US.UTF-8".to_string(),
782 phrases: HashMap::new(),
783 section_header: None,
784 section_header_work: None,
785 section_header_personal: None,
786 section_header_health: None,
787 section_header_meetings: None,
788 table_headers: None,
789 date_format: None,
790 template_file: None,
791 file_path_format: None,
792 list_type: Some(NoteFormat::Table),
793 section_name: None,
794 weekly_format: None,
795 monthly_format: None,
796 quarterly_format: None,
797 yearly_format: None,
798 note_format: None,
799 };
800
801 let vault = Vault::new(config);
802 (vault, temp_dir)
803 }
804
805 #[test]
806 fn test_table_format_no_blank_lines() {
807 let (vault, _temp_dir) = create_test_vault();
808 let date = chrono::Local::now().date_naive();
809
810 vault.add_note("Test1", None).unwrap();
812
813 vault.add_note("Test2", None).unwrap();
815
816 let note_path = vault.get_note_path(date);
818 let content = fs::read_to_string(¬e_path).unwrap();
819 let lines: Vec<&str> = content.lines().collect();
820
821
822 let mut table_start = None;
824 for (i, line) in lines.iter().enumerate() {
825 if line.starts_with("| Time | Content |") {
826 table_start = Some(i);
827 break;
828 }
829 }
830
831 assert!(table_start.is_some(), "Table header not found");
832 let start = table_start.unwrap();
833
834 let table_lines = &lines[start..];
836
837 let mut data_start = None;
839 for (i, line) in table_lines.iter().enumerate() {
840 if line.starts_with("|") && !line.starts_with("|---") && !line.contains("Time") && !line.contains("Content") {
841 data_start = Some(i);
842 break;
843 }
844 }
845
846 assert!(data_start.is_some(), "No data rows found in table");
847 let data_start = data_start.unwrap();
848
849 let mut in_data_section = false;
851 let mut blank_line_found = false;
852
853 for line in &table_lines[data_start..] {
854 if line.starts_with("|") && !line.starts_with("|---") && !line.contains("Time") && !line.contains("Content") {
855 if in_data_section && blank_line_found {
856 panic!("Blank line found between table rows");
857 }
858 in_data_section = true;
859 blank_line_found = false;
860 } else if line.trim().is_empty() {
861 if in_data_section {
862 blank_line_found = true;
863 }
864 }
865 }
866 }
867
868 #[test]
869 fn test_table_format_existing_file_no_blank_lines() {
870 let (vault, _temp_dir) = create_test_vault();
871 let date = chrono::Local::now().date_naive();
872
873 let note_path = vault.get_note_path(date);
875 let existing_content = "---
876date: 2025-10-26
877---
878
879| Time | Content |
880|------|----------|
881| 00:04:43 | Test1 |
882
883| 00:05:28 | Test2 |
884";
885 fs::write(¬e_path, existing_content).unwrap();
886
887 vault.add_note("Test3", None).unwrap();
889
890 vault.add_note("Test4", None).unwrap();
892
893 let note_path = vault.get_note_path(date);
895 let content = fs::read_to_string(¬e_path).unwrap();
896 let lines: Vec<&str> = content.lines().collect();
897
898
899 let mut table_start = None;
901 for (i, line) in lines.iter().enumerate() {
902 if line.starts_with("| Time | Content |") {
903 table_start = Some(i);
904 break;
905 }
906 }
907
908 assert!(table_start.is_some(), "Table header not found");
909 let start = table_start.unwrap();
910
911 let table_lines = &lines[start..];
913
914 let mut data_start = None;
916 for (i, line) in table_lines.iter().enumerate() {
917 if line.starts_with("|") && !line.starts_with("|---") && !line.contains("Time") && !line.contains("Content") {
918 data_start = Some(i);
919 break;
920 }
921 }
922
923 assert!(data_start.is_some(), "No data rows found in table");
924 let data_start = data_start.unwrap();
925
926 let mut in_data_section = false;
928 let mut blank_line_found = false;
929
930 for line in &table_lines[data_start..] {
931 if line.starts_with("|") && !line.starts_with("|---") && !line.contains("Time") && !line.contains("Content") {
932 if in_data_section && blank_line_found {
933 panic!("Blank line found between table rows in existing file");
934 }
935 in_data_section = true;
936 blank_line_found = false;
937 } else if line.trim().is_empty() {
938 if in_data_section {
939 blank_line_found = true;
940 }
941 }
942 }
943 }
944
945 #[test]
946 fn test_bullet_format_no_header() {
947 let (mut vault, _temp_dir) = create_test_vault();
948
949 vault.config.list_type = Some(NoteFormat::Bullet);
951
952 let date = chrono::Local::now().date_naive();
953
954 vault.add_note("Test1", None).unwrap();
956
957 let note_path = vault.get_note_path(date);
959 let content = fs::read_to_string(¬e_path).unwrap();
960
961
962 assert!(!content.contains("| Time | Content |"), "Bullet format should not have table headers");
964 assert!(!content.contains("|------|----------|"), "Bullet format should not have table separators");
965
966 assert!(content.contains("- "), "Bullet format should contain bullet points");
968 }
969
970 #[test]
971 fn test_table_to_bullet_conversion_removes_header() {
972 let (mut vault, _temp_dir) = create_test_vault();
973
974 vault.config.list_type = Some(NoteFormat::Table);
976
977 let date = chrono::Local::now().date_naive();
978
979 vault.add_note("Test1", None).unwrap();
981
982 vault.config.list_type = Some(NoteFormat::Bullet);
984
985 vault.add_note("Test2", None).unwrap();
987
988 let note_path = vault.get_note_path(date);
990 let content = fs::read_to_string(¬e_path).unwrap();
991
992
993 assert!(!content.contains("| Time | Content |"), "Converted bullet format should not have table headers");
995 assert!(!content.contains("|------|----------|"), "Converted bullet format should not have table separators");
996
997 assert!(content.contains("- "), "Converted bullet format should contain bullet points");
999 }
1000
1001 #[test]
1002 fn test_bullet_to_table_conversion_no_blank_lines() {
1003 let (mut vault, _temp_dir) = create_test_vault();
1004
1005 vault.config.list_type = Some(NoteFormat::Bullet);
1007
1008 let date = chrono::Local::now().date_naive();
1009
1010 let note_path = vault.get_note_path(date);
1012 let existing_content = "---
1013date: 2025-10-26
1014---
1015
1016- 00:04:43 Test1
1017
1018- 00:05:28 Test2
1019";
1020 fs::write(¬e_path, existing_content).unwrap();
1021
1022 vault.config.list_type = Some(NoteFormat::Table);
1024
1025 vault.add_note("Test3", None).unwrap();
1027
1028 let note_path = vault.get_note_path(date);
1030 let content = fs::read_to_string(¬e_path).unwrap();
1031
1032
1033 let lines: Vec<&str> = content.lines().collect();
1035 let mut in_table = false;
1036 let mut blank_line_found = false;
1037
1038 for line in &lines {
1039 if line.starts_with("|") && !line.starts_with("|---") && !line.contains("Time") && !line.contains("Content") {
1040 if in_table && blank_line_found {
1041 panic!("Blank line found between table rows in bullet-to-table conversion");
1042 }
1043 in_table = true;
1044 blank_line_found = false;
1045 } else if line.trim().is_empty() {
1046 if in_table {
1047 blank_line_found = true;
1048 }
1049 }
1050 }
1051 }
1052
1053 #[test]
1054 fn test_table_to_bullet_conversion_no_blank_lines() {
1055 let (mut vault, _temp_dir) = create_test_vault();
1056
1057 vault.config.list_type = Some(NoteFormat::Table);
1059
1060 let date = chrono::Local::now().date_naive();
1061
1062 let note_path = vault.get_note_path(date);
1064 let existing_content = "---
1065date: 2025-10-26
1066---
1067
1068| Time | Content |
1069|------|----------|
1070| 00:04:43 | Test1 |
1071
1072| 00:05:28 | Test2 |
1073";
1074 fs::write(¬e_path, existing_content).unwrap();
1075
1076 vault.config.list_type = Some(NoteFormat::Bullet);
1078
1079 vault.add_note("Test3", None).unwrap();
1081
1082 let note_path = vault.get_note_path(date);
1084 let content = fs::read_to_string(¬e_path).unwrap();
1085
1086
1087 let lines: Vec<&str> = content.lines().collect();
1089 let mut in_bullet_section = false;
1090 let mut blank_line_found = false;
1091
1092 for line in &lines {
1093 if line.starts_with("- ") {
1094 if in_bullet_section && blank_line_found {
1095 panic!("Blank line found between bullet points in table-to-bullet conversion");
1096 }
1097 in_bullet_section = true;
1098 blank_line_found = false;
1099 } else if line.trim().is_empty() {
1100 if in_bullet_section {
1101 blank_line_found = true;
1102 }
1103 }
1104 }
1105 }
1106
1107 #[test]
1108 fn test_adding_note_to_existing_file_no_blank_lines() {
1109 let (vault, _temp_dir) = create_test_vault();
1110 let date = chrono::Local::now().date_naive();
1111
1112 let note_path = vault.get_note_path(date);
1114 let existing_content = "---
1115date: 2025-10-26
1116---
1117
1118| Time | Content |
1119|------|----------|
1120| 00:04:43 | Test1 |
1121
1122| 00:05:28 | Test2 |
1123";
1124 fs::write(¬e_path, existing_content).unwrap();
1125
1126 vault.add_note("Test3", None).unwrap();
1128
1129 vault.add_note("Test4", None).unwrap();
1131
1132 let note_path = vault.get_note_path(date);
1134 let content = fs::read_to_string(¬e_path).unwrap();
1135
1136 println!("Adding notes to existing file content:");
1138 println!("{}", content);
1139
1140 let lines: Vec<&str> = content.lines().collect();
1142 let mut in_notes_section = false;
1143 let mut blank_line_found = false;
1144
1145 for line in &lines {
1146 if line.starts_with("|") && !line.starts_with("|---") && !line.contains("Time") && !line.contains("Content") {
1147 if in_notes_section && blank_line_found {
1148 panic!("Blank line found between notes when adding to existing file");
1149 }
1150 in_notes_section = true;
1151 blank_line_found = false;
1152 } else if line.trim().is_empty() {
1153 if in_notes_section {
1154 blank_line_found = true;
1155 }
1156 }
1157 }
1158 }
1159
1160 #[test]
1161 fn test_append_note_without_conversion_no_blank_lines() {
1162 let (vault, _temp_dir) = create_test_vault();
1163 let date = chrono::Local::now().date_naive();
1164
1165 let note_path = vault.get_note_path(date);
1167 let existing_content = "---
1168date: 2025-10-26
1169---
1170
1171| Time | Content |
1172|------|----------|
1173| 00:04:43 | Test1 |
1174| 00:05:28 | Test2 |
1175";
1176 fs::write(¬e_path, existing_content).unwrap();
1177
1178 vault.add_note("Test3", None).unwrap();
1180
1181 let note_path = vault.get_note_path(date);
1183 let content = fs::read_to_string(¬e_path).unwrap();
1184
1185
1186 let lines: Vec<&str> = content.lines().collect();
1188 let mut in_notes_section = false;
1189 let mut blank_line_found = false;
1190
1191 for line in &lines {
1192 if line.starts_with("|") && !line.starts_with("|---") && !line.contains("Time") && !line.contains("Content") {
1193 if in_notes_section && blank_line_found {
1194 panic!("Blank line found between notes when appending to existing file");
1195 }
1196 in_notes_section = true;
1197 blank_line_found = false;
1198 } else if line.trim().is_empty() {
1199 if in_notes_section {
1200 blank_line_found = true;
1201 }
1202 }
1203 }
1204 }
1205
1206 #[test]
1207 fn test_section_insertion_no_blank_lines() {
1208 let (mut vault, _temp_dir) = create_test_vault();
1209
1210 vault.config.section_header = Some("Test Section".to_string());
1212
1213 let date = chrono::Local::now().date_naive();
1214
1215 let note_path = vault.get_note_path(date);
1217 let existing_content = "---
1218date: 2025-10-26
1219---
1220
1221# Test Section
1222
1223| Time | Content |
1224|------|----------|
1225| 00:04:43 | Test1 |
1226| 00:05:28 | Test2 |
1227
1228# Another Section
1229Some other content
1230";
1231 fs::write(¬e_path, existing_content).unwrap();
1232
1233 vault.add_note("Test3", None).unwrap();
1235
1236 let note_path = vault.get_note_path(date);
1238 let content = fs::read_to_string(¬e_path).unwrap();
1239
1240 println!("Section insertion content:");
1242 println!("{}", content);
1243
1244 let lines: Vec<&str> = content.lines().collect();
1246 let mut in_section = false;
1247 let mut blank_line_found = false;
1248
1249 for line in &lines {
1250 if *line == "# Test Section" {
1251 in_section = true;
1252 blank_line_found = false;
1253 continue;
1254 }
1255 if *line == "# Another Section" {
1256 in_section = false;
1257 break;
1258 }
1259 if in_section && line.starts_with("|") && !line.starts_with("|---") && !line.contains("Time") && !line.contains("Content") {
1260 if blank_line_found {
1261 panic!("Blank line found between notes in section");
1262 }
1263 blank_line_found = false;
1264 } else if in_section && (line.starts_with("|---") || line.contains("Time") || line.contains("Content")) {
1265 blank_line_found = false;
1267 } else if in_section && line.trim().is_empty() {
1268 blank_line_found = true;
1269 }
1270 }
1271 }
1272
1273 #[test]
1274 fn test_header_flag_functionality() {
1275 let (vault, _temp_dir) = create_test_vault();
1276 let date = chrono::Local::now().date_naive();
1277
1278 vault.add_note("Test1", None).unwrap();
1280 vault.add_note("Test2", None).unwrap();
1281
1282 let notes = vault.list_notes(date).unwrap();
1284 assert!(!notes.is_empty(), "Should have notes");
1285
1286 for note in ¬es {
1288 assert!(!note.contains("Time") && !note.contains("Content"),
1289 "Table headers should not be included in notes: {}", note);
1290 }
1291 }
1292
1293 #[test]
1294 fn test_table_format_with_section_no_blank_lines() {
1295 let (mut vault, _temp_dir) = create_test_vault();
1296
1297 vault.config.section_header = Some("Test Section".to_string());
1299
1300 let date = chrono::Local::now().date_naive();
1301
1302 vault.add_note("Test1", None).unwrap();
1304
1305 vault.add_note("Test2", None).unwrap();
1307
1308 let note_path = vault.get_note_path(date);
1310 let content = fs::read_to_string(¬e_path).unwrap();
1311 let lines: Vec<&str> = content.lines().collect();
1312
1313 let mut section_start = None;
1315 for (i, line) in lines.iter().enumerate() {
1316 if *line == "# Test Section" {
1317 section_start = Some(i);
1318 break;
1319 }
1320 }
1321
1322 assert!(section_start.is_some(), "Section header not found");
1323 let start = section_start.unwrap();
1324
1325 let section_lines = &lines[start..];
1327
1328 assert!(section_lines.len() > 1, "No content after section header");
1330
1331 let mut table_header_found = false;
1333 for line in section_lines {
1334 if line.starts_with("| Time | Content |") {
1335 table_header_found = true;
1336 break;
1337 }
1338 }
1339 assert!(table_header_found, "Table header not found after section");
1340
1341 let mut in_table = false;
1343 let mut blank_line_found = false;
1344
1345 for line in section_lines {
1346 if line.starts_with("|") && !line.starts_with("|---") && !line.contains("Time") && !line.contains("Content") {
1347 if in_table && blank_line_found {
1348 panic!("Blank line found between table rows in section");
1349 }
1350 in_table = true;
1351 blank_line_found = false;
1352 } else if line.trim().is_empty() {
1353 if in_table {
1354 blank_line_found = true;
1355 }
1356 }
1357 }
1358 }
1359}
1360