@forcecalendar/core 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/calendar/Calendar.js +7 -9
- package/core/calendar/DateUtils.js +10 -9
- package/core/conflicts/ConflictDetector.js +24 -24
- package/core/events/Event.js +13 -19
- package/core/events/EventStore.js +26 -14
- package/core/events/RRuleParser.js +423 -394
- package/core/events/RecurrenceEngine.js +32 -20
- package/core/events/RecurrenceEngineV2.js +536 -562
- package/core/ics/ICSHandler.js +348 -348
- package/core/ics/ICSParser.js +432 -432
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +363 -398
- package/core/performance/AdaptiveMemoryManager.js +310 -308
- package/core/performance/LRUCache.js +3 -4
- package/core/performance/PerformanceOptimizer.js +4 -6
- package/core/search/EventSearch.js +409 -417
- package/core/search/SearchWorkerManager.js +338 -338
- package/core/state/StateManager.js +4 -2
- package/core/timezone/TimezoneDatabase.js +574 -271
- package/core/timezone/TimezoneManager.js +422 -402
- package/core/types.js +1 -1
- package/package.json +7 -2
package/core/ics/ICSParser.js
CHANGED
|
@@ -5,471 +5,471 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export class ICSParser {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
inAlarm = true;
|
|
65
|
-
}
|
|
66
|
-
} else if (property === 'END') {
|
|
67
|
-
if (value === 'VEVENT' && currentEvent) {
|
|
68
|
-
events.push(this.normalizeEvent(currentEvent));
|
|
69
|
-
currentEvent = null;
|
|
70
|
-
inEvent = false;
|
|
71
|
-
} else if (value === 'VALARM') {
|
|
72
|
-
inAlarm = false;
|
|
73
|
-
}
|
|
74
|
-
} else if (inEvent && !inAlarm && currentEvent) {
|
|
75
|
-
// Parse event properties
|
|
76
|
-
this.parseProperty(property, value, currentEvent);
|
|
77
|
-
}
|
|
8
|
+
constructor() {
|
|
9
|
+
// ICS line folding max width
|
|
10
|
+
this.maxLineLength = 75;
|
|
11
|
+
|
|
12
|
+
// Property mappings
|
|
13
|
+
this.propertyMap = {
|
|
14
|
+
SUMMARY: 'title',
|
|
15
|
+
DESCRIPTION: 'description',
|
|
16
|
+
LOCATION: 'location',
|
|
17
|
+
DTSTART: 'start',
|
|
18
|
+
DTEND: 'end',
|
|
19
|
+
UID: 'id',
|
|
20
|
+
CATEGORIES: 'category',
|
|
21
|
+
STATUS: 'status',
|
|
22
|
+
TRANSP: 'showAs',
|
|
23
|
+
ORGANIZER: 'organizer',
|
|
24
|
+
ATTENDEE: 'attendees',
|
|
25
|
+
RRULE: 'recurrence',
|
|
26
|
+
EXDATE: 'excludeDates'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse ICS string into events
|
|
32
|
+
* @param {string} icsString - The ICS formatted string
|
|
33
|
+
* @returns {Array} Array of event objects
|
|
34
|
+
*/
|
|
35
|
+
parse(icsString) {
|
|
36
|
+
const events = [];
|
|
37
|
+
const lines = this.unfoldLines(icsString);
|
|
38
|
+
let currentEvent = null;
|
|
39
|
+
let inEvent = false;
|
|
40
|
+
let inAlarm = false;
|
|
41
|
+
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
// Skip empty lines
|
|
44
|
+
if (!line.trim()) continue;
|
|
45
|
+
|
|
46
|
+
// Parse property and value
|
|
47
|
+
const colonIndex = line.indexOf(':');
|
|
48
|
+
const semicolonIndex = line.indexOf(';');
|
|
49
|
+
const separatorIndex =
|
|
50
|
+
semicolonIndex > -1 && semicolonIndex < colonIndex ? semicolonIndex : colonIndex;
|
|
51
|
+
|
|
52
|
+
if (separatorIndex === -1) continue;
|
|
53
|
+
|
|
54
|
+
const property = line.substring(0, separatorIndex);
|
|
55
|
+
const value = line.substring(colonIndex + 1);
|
|
56
|
+
|
|
57
|
+
// Handle component boundaries
|
|
58
|
+
if (property === 'BEGIN') {
|
|
59
|
+
if (value === 'VEVENT') {
|
|
60
|
+
inEvent = true;
|
|
61
|
+
currentEvent = this.createEmptyEvent();
|
|
62
|
+
} else if (value === 'VALARM') {
|
|
63
|
+
inAlarm = true;
|
|
78
64
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* @param {string} calendarName - Name of the calendar
|
|
87
|
-
* @returns {string} ICS formatted string
|
|
88
|
-
*/
|
|
89
|
-
export(events, calendarName = 'Lightning Calendar') {
|
|
90
|
-
const lines = [];
|
|
91
|
-
|
|
92
|
-
// Calendar header
|
|
93
|
-
lines.push('BEGIN:VCALENDAR');
|
|
94
|
-
lines.push('VERSION:2.0');
|
|
95
|
-
lines.push('PRODID:-//Force Calendar Core//EN');
|
|
96
|
-
lines.push(`X-WR-CALNAME:${calendarName}`);
|
|
97
|
-
lines.push('METHOD:PUBLISH');
|
|
98
|
-
|
|
99
|
-
// Add each event
|
|
100
|
-
for (const event of events) {
|
|
101
|
-
lines.push(...this.eventToICS(event));
|
|
65
|
+
} else if (property === 'END') {
|
|
66
|
+
if (value === 'VEVENT' && currentEvent) {
|
|
67
|
+
events.push(this.normalizeEvent(currentEvent));
|
|
68
|
+
currentEvent = null;
|
|
69
|
+
inEvent = false;
|
|
70
|
+
} else if (value === 'VALARM') {
|
|
71
|
+
inAlarm = false;
|
|
102
72
|
}
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Fold long lines and join
|
|
108
|
-
return this.foldLines(lines).join('\r\n');
|
|
73
|
+
} else if (inEvent && !inAlarm && currentEvent) {
|
|
74
|
+
// Parse event properties
|
|
75
|
+
this.parseProperty(property, value, currentEvent);
|
|
76
|
+
}
|
|
109
77
|
}
|
|
110
78
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (event.end) {
|
|
135
|
-
lines.push(`DTEND;TZID=${tzid}:${this.formatDate(event.end)}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Basic properties
|
|
140
|
-
if (event.title) lines.push(`SUMMARY:${this.escapeText(event.title)}`);
|
|
141
|
-
if (event.description) lines.push(`DESCRIPTION:${this.escapeText(event.description)}`);
|
|
142
|
-
if (event.location) lines.push(`LOCATION:${this.escapeText(event.location)}`);
|
|
143
|
-
|
|
144
|
-
// Status
|
|
145
|
-
if (event.status) {
|
|
146
|
-
const statusMap = {
|
|
147
|
-
'tentative': 'TENTATIVE',
|
|
148
|
-
'confirmed': 'CONFIRMED',
|
|
149
|
-
'cancelled': 'CANCELLED'
|
|
150
|
-
};
|
|
151
|
-
lines.push(`STATUS:${statusMap[event.status] || 'CONFIRMED'}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Show as
|
|
155
|
-
if (event.showAs) {
|
|
156
|
-
const transpMap = {
|
|
157
|
-
'busy': 'OPAQUE',
|
|
158
|
-
'free': 'TRANSPARENT'
|
|
159
|
-
};
|
|
160
|
-
lines.push(`TRANSP:${transpMap[event.showAs] || 'OPAQUE'}`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Categories
|
|
164
|
-
if (event.category) {
|
|
165
|
-
lines.push(`CATEGORIES:${event.category}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Organizer
|
|
169
|
-
if (event.organizer) {
|
|
170
|
-
const org = typeof event.organizer === 'string'
|
|
171
|
-
? event.organizer
|
|
172
|
-
: event.organizer.email || event.organizer.name;
|
|
173
|
-
lines.push(`ORGANIZER:mailto:${org}`);
|
|
174
|
-
}
|
|
79
|
+
return events;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Export events to ICS format
|
|
84
|
+
* @param {Array} events - Array of event objects
|
|
85
|
+
* @param {string} calendarName - Name of the calendar
|
|
86
|
+
* @returns {string} ICS formatted string
|
|
87
|
+
*/
|
|
88
|
+
export(events, calendarName = 'Lightning Calendar') {
|
|
89
|
+
const lines = [];
|
|
90
|
+
|
|
91
|
+
// Calendar header
|
|
92
|
+
lines.push('BEGIN:VCALENDAR');
|
|
93
|
+
lines.push('VERSION:2.0');
|
|
94
|
+
lines.push('PRODID:-//Force Calendar Core//EN');
|
|
95
|
+
lines.push(`X-WR-CALNAME:${calendarName}`);
|
|
96
|
+
lines.push('METHOD:PUBLISH');
|
|
97
|
+
|
|
98
|
+
// Add each event
|
|
99
|
+
for (const event of events) {
|
|
100
|
+
lines.push(...this.eventToICS(event));
|
|
101
|
+
}
|
|
175
102
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
103
|
+
// Calendar footer
|
|
104
|
+
lines.push('END:VCALENDAR');
|
|
105
|
+
|
|
106
|
+
// Fold long lines and join
|
|
107
|
+
return this.foldLines(lines).join('\r\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convert single event to ICS lines
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
eventToICS(event) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
lines.push('BEGIN:VEVENT');
|
|
117
|
+
|
|
118
|
+
// UID (required)
|
|
119
|
+
lines.push(`UID:${event.id || this.generateUID()}`);
|
|
120
|
+
|
|
121
|
+
// Timestamps
|
|
122
|
+
lines.push(`DTSTAMP:${this.formatDate(new Date())}`);
|
|
123
|
+
|
|
124
|
+
// Start and end dates
|
|
125
|
+
if (event.allDay) {
|
|
126
|
+
lines.push(`DTSTART;VALUE=DATE:${this.formatDate(event.start, true)}`);
|
|
127
|
+
if (event.end) {
|
|
128
|
+
lines.push(`DTEND;VALUE=DATE:${this.formatDate(event.end, true)}`);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
const tzid = event.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
132
|
+
lines.push(`DTSTART;TZID=${tzid}:${this.formatDate(event.start)}`);
|
|
133
|
+
if (event.end) {
|
|
134
|
+
lines.push(`DTEND;TZID=${tzid}:${this.formatDate(event.end)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
185
137
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
138
|
+
// Basic properties
|
|
139
|
+
if (event.title) lines.push(`SUMMARY:${this.escapeText(event.title)}`);
|
|
140
|
+
if (event.description) lines.push(`DESCRIPTION:${this.escapeText(event.description)}`);
|
|
141
|
+
if (event.location) lines.push(`LOCATION:${this.escapeText(event.location)}`);
|
|
142
|
+
|
|
143
|
+
// Status
|
|
144
|
+
if (event.status) {
|
|
145
|
+
const statusMap = {
|
|
146
|
+
tentative: 'TENTATIVE',
|
|
147
|
+
confirmed: 'CONFIRMED',
|
|
148
|
+
cancelled: 'CANCELLED'
|
|
149
|
+
};
|
|
150
|
+
lines.push(`STATUS:${statusMap[event.status] || 'CONFIRMED'}`);
|
|
151
|
+
}
|
|
200
152
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
}
|
|
153
|
+
// Show as
|
|
154
|
+
if (event.showAs) {
|
|
155
|
+
const transpMap = {
|
|
156
|
+
busy: 'OPAQUE',
|
|
157
|
+
free: 'TRANSPARENT'
|
|
158
|
+
};
|
|
159
|
+
lines.push(`TRANSP:${transpMap[event.showAs] || 'OPAQUE'}`);
|
|
160
|
+
}
|
|
211
161
|
|
|
212
|
-
|
|
213
|
-
|
|
162
|
+
// Categories
|
|
163
|
+
if (event.category) {
|
|
164
|
+
lines.push(`CATEGORIES:${event.category}`);
|
|
214
165
|
}
|
|
215
166
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// Map to event property
|
|
225
|
-
const eventProp = this.propertyMap[propName];
|
|
226
|
-
if (!eventProp) return;
|
|
227
|
-
|
|
228
|
-
switch (propName) {
|
|
229
|
-
case 'DTSTART':
|
|
230
|
-
case 'DTEND':
|
|
231
|
-
event[eventProp] = this.parseDate(value, property);
|
|
232
|
-
if (property.includes('VALUE=DATE')) {
|
|
233
|
-
event.allDay = true;
|
|
234
|
-
}
|
|
235
|
-
break;
|
|
236
|
-
|
|
237
|
-
case 'SUMMARY':
|
|
238
|
-
case 'DESCRIPTION':
|
|
239
|
-
case 'LOCATION':
|
|
240
|
-
event[eventProp] = this.unescapeText(value);
|
|
241
|
-
break;
|
|
242
|
-
|
|
243
|
-
case 'UID':
|
|
244
|
-
event.id = value;
|
|
245
|
-
break;
|
|
246
|
-
|
|
247
|
-
case 'CATEGORIES':
|
|
248
|
-
event.category = value.split(',')[0]; // Take first category
|
|
249
|
-
break;
|
|
250
|
-
|
|
251
|
-
case 'STATUS':
|
|
252
|
-
const statusMap = {
|
|
253
|
-
'TENTATIVE': 'tentative',
|
|
254
|
-
'CONFIRMED': 'confirmed',
|
|
255
|
-
'CANCELLED': 'cancelled'
|
|
256
|
-
};
|
|
257
|
-
event.status = statusMap[value] || 'confirmed';
|
|
258
|
-
break;
|
|
259
|
-
|
|
260
|
-
case 'TRANSP':
|
|
261
|
-
event.showAs = value === 'TRANSPARENT' ? 'free' : 'busy';
|
|
262
|
-
break;
|
|
263
|
-
|
|
264
|
-
case 'ORGANIZER':
|
|
265
|
-
event.organizer = value.replace('mailto:', '');
|
|
266
|
-
break;
|
|
267
|
-
|
|
268
|
-
case 'ATTENDEE':
|
|
269
|
-
if (!event.attendees) event.attendees = [];
|
|
270
|
-
const email = value.replace('mailto:', '');
|
|
271
|
-
event.attendees.push({
|
|
272
|
-
email: email,
|
|
273
|
-
name: email.split('@')[0] // Use email prefix as name
|
|
274
|
-
});
|
|
275
|
-
break;
|
|
276
|
-
|
|
277
|
-
case 'RRULE':
|
|
278
|
-
event.recurrence = value;
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
167
|
+
// Organizer
|
|
168
|
+
if (event.organizer) {
|
|
169
|
+
const org =
|
|
170
|
+
typeof event.organizer === 'string'
|
|
171
|
+
? event.organizer
|
|
172
|
+
: event.organizer.email || event.organizer.name;
|
|
173
|
+
lines.push(`ORGANIZER:mailto:${org}`);
|
|
281
174
|
}
|
|
282
175
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
dateString = dateString.replace(/^TZID=[^:]+:/, '');
|
|
290
|
-
|
|
291
|
-
// Check if it's a date-only value
|
|
292
|
-
if (property.includes('VALUE=DATE') || dateString.length === 8) {
|
|
293
|
-
// YYYYMMDD format
|
|
294
|
-
const year = dateString.substr(0, 4);
|
|
295
|
-
const month = dateString.substr(4, 2);
|
|
296
|
-
const day = dateString.substr(6, 2);
|
|
297
|
-
return new Date(year, month - 1, day);
|
|
176
|
+
// Attendees
|
|
177
|
+
if (event.attendees && event.attendees.length > 0) {
|
|
178
|
+
for (const attendee of event.attendees) {
|
|
179
|
+
const email = attendee.email || attendee;
|
|
180
|
+
if (email) {
|
|
181
|
+
lines.push(`ATTENDEE:mailto:${email}`);
|
|
298
182
|
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
299
185
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const second = parseInt(dateString.substr(13, 2)) || 0;
|
|
307
|
-
|
|
308
|
-
if (dateString.endsWith('Z')) {
|
|
309
|
-
// UTC time
|
|
310
|
-
return new Date(Date.UTC(year, month, day, hour, minute, second));
|
|
186
|
+
// Recurrence
|
|
187
|
+
if (event.recurrence) {
|
|
188
|
+
if (typeof event.recurrence === 'string') {
|
|
189
|
+
// Already in RRULE format
|
|
190
|
+
if (event.recurrence.startsWith('RRULE:')) {
|
|
191
|
+
lines.push(event.recurrence);
|
|
311
192
|
} else {
|
|
312
|
-
|
|
313
|
-
return new Date(year, month, day, hour, minute, second);
|
|
193
|
+
lines.push(`RRULE:${event.recurrence}`);
|
|
314
194
|
}
|
|
195
|
+
} else if (typeof event.recurrence === 'object') {
|
|
196
|
+
// Convert object to RRULE
|
|
197
|
+
lines.push(`RRULE:${this.objectToRRule(event.recurrence)}`);
|
|
198
|
+
}
|
|
315
199
|
}
|
|
316
200
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (dateOnly) {
|
|
327
|
-
return `${year}${month}${day}`;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const hour = String(date.getHours()).padStart(2, '0');
|
|
331
|
-
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
332
|
-
const second = String(date.getSeconds()).padStart(2, '0');
|
|
333
|
-
|
|
334
|
-
return `${year}${month}${day}T${hour}${minute}${second}`;
|
|
201
|
+
// Reminders/Alarms
|
|
202
|
+
if (event.reminders && event.reminders.length > 0) {
|
|
203
|
+
for (const reminder of event.reminders) {
|
|
204
|
+
lines.push('BEGIN:VALARM');
|
|
205
|
+
lines.push('ACTION:DISPLAY');
|
|
206
|
+
lines.push(`TRIGGER:-PT${reminder.minutes || 15}M`);
|
|
207
|
+
lines.push(`DESCRIPTION:${event.title || 'Reminder'}`);
|
|
208
|
+
lines.push('END:VALARM');
|
|
209
|
+
}
|
|
335
210
|
}
|
|
336
211
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
212
|
+
lines.push('END:VEVENT');
|
|
213
|
+
return lines;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Parse ICS property into event object
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
parseProperty(property, value, event) {
|
|
221
|
+
// Extract actual property name (before parameters)
|
|
222
|
+
const propName = property.split(';')[0];
|
|
223
|
+
|
|
224
|
+
// Map to event property
|
|
225
|
+
const eventProp = this.propertyMap[propName];
|
|
226
|
+
if (!eventProp) return;
|
|
227
|
+
|
|
228
|
+
switch (propName) {
|
|
229
|
+
case 'DTSTART':
|
|
230
|
+
case 'DTEND':
|
|
231
|
+
event[eventProp] = this.parseDate(value, property);
|
|
232
|
+
if (property.includes('VALUE=DATE')) {
|
|
233
|
+
event.allDay = true;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case 'SUMMARY':
|
|
238
|
+
case 'DESCRIPTION':
|
|
239
|
+
case 'LOCATION':
|
|
240
|
+
event[eventProp] = this.unescapeText(value);
|
|
241
|
+
break;
|
|
242
|
+
|
|
243
|
+
case 'UID':
|
|
244
|
+
event.id = value;
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case 'CATEGORIES':
|
|
248
|
+
event.category = value.split(',')[0]; // Take first category
|
|
249
|
+
break;
|
|
250
|
+
|
|
251
|
+
case 'STATUS': {
|
|
252
|
+
const statusMap = {
|
|
253
|
+
TENTATIVE: 'tentative',
|
|
254
|
+
CONFIRMED: 'confirmed',
|
|
255
|
+
CANCELLED: 'cancelled'
|
|
256
|
+
};
|
|
257
|
+
event.status = statusMap[value] || 'confirmed';
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
case 'TRANSP':
|
|
262
|
+
event.showAs = value === 'TRANSPARENT' ? 'free' : 'busy';
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case 'ORGANIZER':
|
|
266
|
+
event.organizer = value.replace('mailto:', '');
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
case 'ATTENDEE': {
|
|
270
|
+
if (!event.attendees) event.attendees = [];
|
|
271
|
+
const email = value.replace('mailto:', '');
|
|
272
|
+
event.attendees.push({
|
|
273
|
+
email: email,
|
|
274
|
+
name: email.split('@')[0] // Use email prefix as name
|
|
275
|
+
});
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case 'RRULE':
|
|
280
|
+
event.recurrence = value;
|
|
281
|
+
break;
|
|
346
282
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// Continuation lines (with space prefix)
|
|
366
|
-
while (remaining.length > 0) {
|
|
367
|
-
const chunk = remaining.substr(0, this.maxLineLength - 1);
|
|
368
|
-
folded.push(' ' + chunk);
|
|
369
|
-
remaining = remaining.substr(chunk.length);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return folded.join('\r\n');
|
|
373
|
-
}).flat();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Parse ICS date string
|
|
287
|
+
* @private
|
|
288
|
+
*/
|
|
289
|
+
parseDate(dateString, property = '') {
|
|
290
|
+
// Remove timezone if present
|
|
291
|
+
dateString = dateString.replace(/^TZID=[^:]+:/, '');
|
|
292
|
+
|
|
293
|
+
// Check if it's a date-only value
|
|
294
|
+
if (property.includes('VALUE=DATE') || dateString.length === 8) {
|
|
295
|
+
// YYYYMMDD format
|
|
296
|
+
const year = dateString.substr(0, 4);
|
|
297
|
+
const month = dateString.substr(4, 2);
|
|
298
|
+
const day = dateString.substr(6, 2);
|
|
299
|
+
return new Date(year, month - 1, day);
|
|
374
300
|
}
|
|
375
301
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
302
|
+
// Full datetime: YYYYMMDDTHHMMSS[Z]
|
|
303
|
+
const year = parseInt(dateString.substr(0, 4));
|
|
304
|
+
const month = parseInt(dateString.substr(4, 2)) - 1;
|
|
305
|
+
const day = parseInt(dateString.substr(6, 2));
|
|
306
|
+
const hour = parseInt(dateString.substr(9, 2)) || 0;
|
|
307
|
+
const minute = parseInt(dateString.substr(11, 2)) || 0;
|
|
308
|
+
const second = parseInt(dateString.substr(13, 2)) || 0;
|
|
309
|
+
|
|
310
|
+
if (dateString.endsWith('Z')) {
|
|
311
|
+
// UTC time
|
|
312
|
+
return new Date(Date.UTC(year, month, day, hour, minute, second));
|
|
387
313
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
314
|
+
// Local time
|
|
315
|
+
return new Date(year, month, day, hour, minute, second);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Format date for ICS
|
|
320
|
+
* @private
|
|
321
|
+
*/
|
|
322
|
+
formatDate(date, dateOnly = false) {
|
|
323
|
+
const year = date.getFullYear();
|
|
324
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
325
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
326
|
+
|
|
327
|
+
if (dateOnly) {
|
|
328
|
+
return `${year}${month}${day}`;
|
|
400
329
|
}
|
|
401
330
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
331
|
+
const hour = String(date.getHours()).padStart(2, '0');
|
|
332
|
+
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
333
|
+
const second = String(date.getSeconds()).padStart(2, '0');
|
|
334
|
+
|
|
335
|
+
return `${year}${month}${day}T${hour}${minute}${second}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Unfold ICS lines (reverse line folding)
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
unfoldLines(icsString) {
|
|
343
|
+
return icsString.replace(/\r\n /g, '').replace(/\n /g, '').split(/\r?\n/);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Fold long lines per ICS spec
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
foldLines(lines) {
|
|
351
|
+
return lines
|
|
352
|
+
.map(line => {
|
|
353
|
+
if (line.length <= this.maxLineLength) {
|
|
354
|
+
return line;
|
|
355
|
+
}
|
|
411
356
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
* @private
|
|
415
|
-
*/
|
|
416
|
-
createEmptyEvent() {
|
|
417
|
-
return {
|
|
418
|
-
id: null,
|
|
419
|
-
title: '',
|
|
420
|
-
description: '',
|
|
421
|
-
start: null,
|
|
422
|
-
end: null,
|
|
423
|
-
allDay: false,
|
|
424
|
-
location: '',
|
|
425
|
-
category: '',
|
|
426
|
-
status: 'confirmed',
|
|
427
|
-
showAs: 'busy',
|
|
428
|
-
attendees: [],
|
|
429
|
-
reminders: []
|
|
430
|
-
};
|
|
431
|
-
}
|
|
357
|
+
const folded = [];
|
|
358
|
+
let remaining = line;
|
|
432
359
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
*/
|
|
437
|
-
normalizeEvent(event) {
|
|
438
|
-
// Ensure required fields
|
|
439
|
-
if (!event.id) {
|
|
440
|
-
event.id = this.generateUID();
|
|
441
|
-
}
|
|
360
|
+
// First line
|
|
361
|
+
folded.push(remaining.substr(0, this.maxLineLength));
|
|
362
|
+
remaining = remaining.substr(this.maxLineLength);
|
|
442
363
|
|
|
443
|
-
|
|
444
|
-
|
|
364
|
+
// Continuation lines (with space prefix)
|
|
365
|
+
while (remaining.length > 0) {
|
|
366
|
+
const chunk = remaining.substr(0, this.maxLineLength - 1);
|
|
367
|
+
folded.push(` ${chunk}`);
|
|
368
|
+
remaining = remaining.substr(chunk.length);
|
|
445
369
|
}
|
|
446
370
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
371
|
+
return folded.join('\r\n');
|
|
372
|
+
})
|
|
373
|
+
.flat();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Escape special characters for ICS
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
escapeText(text) {
|
|
381
|
+
if (!text) return '';
|
|
382
|
+
return text
|
|
383
|
+
.replace(/\\/g, '\\\\')
|
|
384
|
+
.replace(/;/g, '\\;')
|
|
385
|
+
.replace(/,/g, '\\,')
|
|
386
|
+
.replace(/\n/g, '\\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Unescape ICS text
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
unescapeText(text) {
|
|
394
|
+
if (!text) return '';
|
|
395
|
+
return text
|
|
396
|
+
.replace(/\\n/g, '\n')
|
|
397
|
+
.replace(/\\,/g, ',')
|
|
398
|
+
.replace(/\\;/g, ';')
|
|
399
|
+
.replace(/\\\\/g, '\\');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Generate unique ID
|
|
404
|
+
* @private
|
|
405
|
+
*/
|
|
406
|
+
generateUID() {
|
|
407
|
+
const timestamp = Date.now().toString(36);
|
|
408
|
+
const random = Math.random().toString(36).substr(2, 9);
|
|
409
|
+
return `${timestamp}-${random}@lightning-calendar`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Create empty event object
|
|
414
|
+
* @private
|
|
415
|
+
*/
|
|
416
|
+
createEmptyEvent() {
|
|
417
|
+
return {
|
|
418
|
+
id: null,
|
|
419
|
+
title: '',
|
|
420
|
+
description: '',
|
|
421
|
+
start: null,
|
|
422
|
+
end: null,
|
|
423
|
+
allDay: false,
|
|
424
|
+
location: '',
|
|
425
|
+
category: '',
|
|
426
|
+
status: 'confirmed',
|
|
427
|
+
showAs: 'busy',
|
|
428
|
+
attendees: [],
|
|
429
|
+
reminders: []
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Normalize event object
|
|
435
|
+
* @private
|
|
436
|
+
*/
|
|
437
|
+
normalizeEvent(event) {
|
|
438
|
+
// Ensure required fields
|
|
439
|
+
if (!event.id) {
|
|
440
|
+
event.id = this.generateUID();
|
|
441
|
+
}
|
|
451
442
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
443
|
+
if (!event.title) {
|
|
444
|
+
event.title = 'Untitled Event';
|
|
445
|
+
}
|
|
455
446
|
|
|
456
|
-
|
|
447
|
+
// Convert dates to Date objects if needed
|
|
448
|
+
if (event.start && !(event.start instanceof Date)) {
|
|
449
|
+
event.start = new Date(event.start);
|
|
457
450
|
}
|
|
458
451
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
* @private
|
|
462
|
-
*/
|
|
463
|
-
objectToRRule(recurrence) {
|
|
464
|
-
const parts = [];
|
|
465
|
-
|
|
466
|
-
if (recurrence.freq) parts.push(`FREQ=${recurrence.freq.toUpperCase()}`);
|
|
467
|
-
if (recurrence.interval) parts.push(`INTERVAL=${recurrence.interval}`);
|
|
468
|
-
if (recurrence.count) parts.push(`COUNT=${recurrence.count}`);
|
|
469
|
-
if (recurrence.until) parts.push(`UNTIL=${this.formatDate(recurrence.until)}`);
|
|
470
|
-
if (recurrence.byDay) parts.push(`BYDAY=${recurrence.byDay.join(',')}`);
|
|
471
|
-
if (recurrence.byMonth) parts.push(`BYMONTH=${recurrence.byMonth.join(',')}`);
|
|
472
|
-
|
|
473
|
-
return parts.join(';');
|
|
452
|
+
if (event.end && !(event.end instanceof Date)) {
|
|
453
|
+
event.end = new Date(event.end);
|
|
474
454
|
}
|
|
475
|
-
|
|
455
|
+
|
|
456
|
+
return event;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Convert recurrence object to RRULE string
|
|
461
|
+
* @private
|
|
462
|
+
*/
|
|
463
|
+
objectToRRule(recurrence) {
|
|
464
|
+
const parts = [];
|
|
465
|
+
|
|
466
|
+
if (recurrence.freq) parts.push(`FREQ=${recurrence.freq.toUpperCase()}`);
|
|
467
|
+
if (recurrence.interval) parts.push(`INTERVAL=${recurrence.interval}`);
|
|
468
|
+
if (recurrence.count) parts.push(`COUNT=${recurrence.count}`);
|
|
469
|
+
if (recurrence.until) parts.push(`UNTIL=${this.formatDate(recurrence.until)}`);
|
|
470
|
+
if (recurrence.byDay) parts.push(`BYDAY=${recurrence.byDay.join(',')}`);
|
|
471
|
+
if (recurrence.byMonth) parts.push(`BYMONTH=${recurrence.byMonth.join(',')}`);
|
|
472
|
+
|
|
473
|
+
return parts.join(';');
|
|
474
|
+
}
|
|
475
|
+
}
|