Initial Commit
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
EMAIL_PROCESSING_CODES,
|
||||
EmailProcessingError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import type { CalendarEvent } from "./calendar-parser.types.js";
|
||||
|
||||
/** Parse an iCal/ICS string into calendar events. */
|
||||
export function parseICalendar(icsContent: string): CalendarEvent[] {
|
||||
if (typeof icsContent !== "string") {
|
||||
throw new EmailProcessingError(
|
||||
EMAIL_PROCESSING_CODES.INVALID_INPUT,
|
||||
"email_processing.invalid_input.ics"
|
||||
);
|
||||
}
|
||||
return parseICalendarInner(icsContent);
|
||||
}
|
||||
|
||||
function parseICalendarInner(icsContent: string): CalendarEvent[] {
|
||||
const events: CalendarEvent[] = [];
|
||||
const lines = unfoldIcsLines(icsContent);
|
||||
|
||||
let method: string | undefined;
|
||||
const methodLine = lines.find((l) => l.startsWith("METHOD:"));
|
||||
if (methodLine) {
|
||||
method = methodLine.slice("METHOD:".length).trim();
|
||||
}
|
||||
|
||||
let inEvent = false;
|
||||
let current: Partial<CalendarEvent> = {};
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === "BEGIN:VEVENT") {
|
||||
inEvent = true;
|
||||
current =
|
||||
method !== undefined
|
||||
? { attendees: [], method }
|
||||
: { attendees: [] };
|
||||
continue;
|
||||
}
|
||||
if (line === "END:VEVENT") {
|
||||
inEvent = false;
|
||||
const event: CalendarEvent = {
|
||||
// current.attendees is always set by BEGIN:VEVENT above.
|
||||
attendees: current.attendees as string[],
|
||||
};
|
||||
if (current.summary !== undefined) event.summary = current.summary;
|
||||
if (current.description !== undefined)
|
||||
event.description = current.description;
|
||||
if (current.dtStart !== undefined) event.dtStart = current.dtStart;
|
||||
if (current.dtEnd !== undefined) event.dtEnd = current.dtEnd;
|
||||
if (current.location !== undefined)
|
||||
event.location = current.location;
|
||||
if (current.organizer !== undefined)
|
||||
event.organizer = current.organizer;
|
||||
if (current.uid !== undefined) event.uid = current.uid;
|
||||
if (current.method !== undefined) event.method = current.method;
|
||||
events.push(event);
|
||||
continue;
|
||||
}
|
||||
if (!inEvent) continue;
|
||||
|
||||
if (line.startsWith("SUMMARY:")) {
|
||||
current.summary = unescapeIcs(line.slice("SUMMARY:".length));
|
||||
} else if (line.startsWith("DESCRIPTION:")) {
|
||||
current.description = unescapeIcs(
|
||||
line.slice("DESCRIPTION:".length)
|
||||
);
|
||||
} else if (line.startsWith("DTSTART")) {
|
||||
current.dtStart = extractIcsDateTime(line);
|
||||
} else if (line.startsWith("DTEND")) {
|
||||
current.dtEnd = extractIcsDateTime(line);
|
||||
} else if (line.startsWith("LOCATION:")) {
|
||||
current.location = unescapeIcs(line.slice("LOCATION:".length));
|
||||
} else if (line.startsWith("ORGANIZER")) {
|
||||
const org = extractMailto(line);
|
||||
if (org !== undefined) current.organizer = org;
|
||||
} else if (line.startsWith("ATTENDEE")) {
|
||||
const email = extractMailto(line);
|
||||
// current.attendees is always set by BEGIN:VEVENT above.
|
||||
if (email) (current.attendees as string[]).push(email);
|
||||
} else if (line.startsWith("UID:")) {
|
||||
current.uid = line.slice("UID:".length).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
function unfoldIcsLines(content: string): string[] {
|
||||
return content
|
||||
.replace(/\r\n /g, "")
|
||||
.replace(/\r\n\t/g, "")
|
||||
.split(/\r?\n/);
|
||||
}
|
||||
|
||||
function unescapeIcs(text: string): string {
|
||||
return text
|
||||
.replace(/\\n/g, "\n")
|
||||
.replace(/\\,/g, ",")
|
||||
.replace(/\\;/g, ";")
|
||||
.replace(/\\\\/g, "\\");
|
||||
}
|
||||
|
||||
function extractIcsDateTime(line: string): string {
|
||||
const colonIdx = line.indexOf(":");
|
||||
if (colonIdx === -1) return "";
|
||||
return line.slice(colonIdx + 1).trim();
|
||||
}
|
||||
|
||||
function extractMailto(line: string): string | undefined {
|
||||
const match = line.match(/mailto:([^\s;]+)/i);
|
||||
// The regex's capture group always matches when the overall regex
|
||||
// matches, so match[1] is non-undefined here.
|
||||
return match ? match[1] : undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user