caldir_core/event/
slugify.rs1use crate::event::{Event, EventTime};
2use chrono::Local;
3
4const EMPTY_SUMMARY_SLUG: &str = "untitled";
5
6impl Event {
7 pub fn base_slug(&self) -> String {
14 if self.recurrence.is_some() {
15 return format!("_recurring__{}", self.summary_slug());
16 }
17
18 format!("{}__{}", self.time_slug(), self.summary_slug())
19 }
20
21 fn summary_slug(&self) -> String {
22 match &self.summary {
23 Some(summary) => {
24 let cleaned: String = summary
27 .chars()
28 .filter(|c| c.is_alphanumeric() || c.is_whitespace())
29 .collect();
30
31 let slug = crate::utils::slugify(&cleaned);
34
35 if slug.is_empty() {
36 EMPTY_SUMMARY_SLUG.to_string()
37 } else {
38 slug
39 }
40 }
41 None => EMPTY_SUMMARY_SLUG.to_string(),
42 }
43 }
44
45 fn time_slug(&self) -> String {
49 match &self.start {
50 EventTime::Date(date) => date.format("%Y-%m-%d").to_string(),
52
53 _ => self
55 .start
56 .to_local_tz(&Local)
57 .format("%Y-%m-%dT%H%M")
58 .to_string(),
59 }
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use chrono::NaiveDate;
67
68 #[test]
69 fn generates_expected_slug_for_emoji_summary() {
70 let event = Event::new(
71 "Café ☕️ meeting",
72 EventTime::Date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
73 );
74
75 assert_eq!(event.summary_slug(), "cafe-meeting");
76 }
77
78 #[test]
79 fn generates_expected_slug_for_empty_summary() {
80 let event = Event::new(
81 "",
82 EventTime::Date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
83 );
84
85 assert_eq!(event.summary_slug(), "untitled");
86 }
87
88 #[test]
89 fn caps_long_summary_slug_to_avoid_filename_too_long() {
90 let event = Event::new(
93 "a ".repeat(200),
94 EventTime::Date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
95 );
96
97 assert_eq!(event.summary_slug().chars().count(), 50);
98 }
99
100 #[test]
101 fn generates_correct_base_slug_for_all_day_event() {
102 let event = Event::new(
103 "Test Event",
104 EventTime::Date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
105 );
106
107 assert_eq!(event.base_slug(), "2024-01-01__test-event");
108 }
109
110 #[test]
111 fn generates_correct_base_slug_for_timed_event() {
112 let event = Event::new(
113 "Test Event",
114 EventTime::DateTimeFloating(
115 NaiveDate::from_ymd_opt(2024, 1, 1)
116 .unwrap()
117 .and_hms_opt(15, 30, 20)
118 .unwrap(),
119 ),
120 );
121
122 assert_eq!(event.base_slug(), "2024-01-01T1530__test-event");
123 }
124
125 #[test]
126 fn uses_recurring_prefix_instead_of_date_for_recurring_events() {
127 let mut event = Event::new(
128 "Standup",
129 EventTime::DateTimeFloating(
130 NaiveDate::from_ymd_opt(2024, 1, 1)
131 .unwrap()
132 .and_hms_opt(9, 0, 0)
133 .unwrap(),
134 ),
135 );
136 event.recurrence = Some(crate::event::Recurrence::new("FREQ=DAILY"));
137
138 assert_eq!(event.base_slug(), "_recurring__standup");
139 }
140
141 #[test]
142 fn generates_untitled_base_slug_for_event_without_summary() {
143 let event = Event::new(
144 "",
145 EventTime::Date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
146 );
147
148 assert_eq!(event.base_slug(), "2024-01-01__untitled");
149 }
150}