157 lines
4.0 KiB
TypeScript
157 lines
4.0 KiB
TypeScript
function parseICal(content: string): Array<{
|
|
title: string;
|
|
description?: string;
|
|
start: Date;
|
|
end?: Date;
|
|
allDay?: boolean;
|
|
rrule?: string;
|
|
uid?: string;
|
|
priority?: number;
|
|
}> {
|
|
const events: Array<{
|
|
title: string;
|
|
description?: string;
|
|
start: Date;
|
|
end?: Date;
|
|
allDay?: boolean;
|
|
rrule?: string;
|
|
uid?: string;
|
|
priority?: number;
|
|
}> = [];
|
|
|
|
const lines = content.split('\n');
|
|
let i = 0;
|
|
|
|
while (i < lines.length) {
|
|
const line = lines[i].trim();
|
|
if (!line) {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (line.startsWith('BEGIN:VEVENT')) {
|
|
const event: {
|
|
title: string;
|
|
description?: string;
|
|
start: Date;
|
|
end?: Date;
|
|
allDay?: boolean;
|
|
rrule?: string;
|
|
uid?: string;
|
|
priority?: number;
|
|
} = {
|
|
title: '',
|
|
start: new Date(0),
|
|
};
|
|
|
|
i++;
|
|
let inEvent = true;
|
|
|
|
while (i < lines.length && inEvent) {
|
|
const line = lines[i].trim();
|
|
i++;
|
|
|
|
if (line === 'END:VEVENT') {
|
|
inEvent = false;
|
|
continue;
|
|
}
|
|
|
|
if (line.startsWith('END:')) {
|
|
inEvent = false;
|
|
continue;
|
|
}
|
|
|
|
if (line.startsWith('SUMMARY:')) {
|
|
event.title = line.substring(8).trim();
|
|
} else if (line.startsWith('DESCRIPTION:')) {
|
|
event.description = line.substring(12).trim();
|
|
} else if (line.startsWith('DTSTART:')) {
|
|
const dateStr = line.substring(8).trim();
|
|
event.start = parseICalDateTime(dateStr);
|
|
} else if (line.startsWith('DTEND:')) {
|
|
const dateStr = line.substring(6).trim();
|
|
event.end = parseICalDateTime(dateStr);
|
|
} else if (line.startsWith('DTSTAMP:')) {
|
|
// ignore, we use start date
|
|
} else if (line.startsWith('UID:')) {
|
|
event.uid = line.substring(4).trim();
|
|
} else if (line.startsWith('PRIORITY:')) {
|
|
event.priority = parseInt(line.substring(9).trim(), 10);
|
|
} else if (line.startsWith('ALL-DAY:')) {
|
|
event.allDay = line.substring(10).trim().toLowerCase() === 'true';
|
|
} else if (line.startsWith('RRULE:')) {
|
|
event.rrule = line.substring(6).trim();
|
|
} else if (line.startsWith('BEGIN:VALARM')) {
|
|
// skip alarms
|
|
i++;
|
|
let inAlarm = true;
|
|
while (i < lines.length && inAlarm) {
|
|
const alarmLine = lines[i].trim();
|
|
i++;
|
|
if (alarmLine === 'END:VALARM') {
|
|
inAlarm = false;
|
|
}
|
|
}
|
|
} else {
|
|
// skip unrecognized lines
|
|
}
|
|
}
|
|
|
|
if (event.title) {
|
|
events.push(event);
|
|
}
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return events;
|
|
}
|
|
|
|
function parseICalDateTime(dateStr: string): Date {
|
|
// Handle various iCal date/time formats
|
|
// Format 1: YYYYMMDDTHHMMSSZ (UTC)
|
|
// Format 2: YYYYMMDDTHHMMSS (local)
|
|
// Format 3: YYYYMMDD (date only)
|
|
// Format 4: TZID:... (with timezone)
|
|
|
|
let datePart = '';
|
|
let timePart = '';
|
|
let hasTime = false;
|
|
|
|
// Check for Z (UTC) indicator
|
|
const hasZ = dateStr.endsWith('Z');
|
|
const cleanStr = hasZ ? dateStr.slice(0, -1) : dateStr;
|
|
|
|
// Try to split at T
|
|
const tIndex = cleanStr.indexOf('T');
|
|
if (tIndex !== -1) {
|
|
datePart = cleanStr.substring(0, tIndex);
|
|
timePart = cleanStr.substring(tIndex + 1);
|
|
hasTime = true;
|
|
} else {
|
|
datePart = cleanStr;
|
|
}
|
|
|
|
// Parse date (YYYYMMDD)
|
|
if (datePart.length >= 8) {
|
|
const year = parseInt(datePart.substring(0, 4), 10);
|
|
const month = parseInt(datePart.substring(4, 6), 10) - 1; // 0-indexed
|
|
const day = parseInt(datePart.substring(6, 8), 10);
|
|
const date = new Date(year, month, day);
|
|
|
|
if (hasTime && timePart) {
|
|
const hour = parseInt(timePart.substring(0, 2), 10);
|
|
const minute = parseInt(timePart.substring(2, 4), 10);
|
|
date.setHours(hour, minute, 0, 0);
|
|
}
|
|
|
|
return date;
|
|
}
|
|
|
|
// Fallback: try Date.parse
|
|
return new Date(dateStr);
|
|
}
|
|
|
|
export default parseICal;
|