lilith-platform.live/codebase/@features/my/backend-api/src/ical-utils.ts
2026-04-18 19:25:54 -07:00

92 lines
2.9 KiB
TypeScript

/**
* Shared iCal (RFC 5545) parsing and building utilities.
*
* Used by both CalDAV calendar sync (VEVENT) and reminders sync (VTODO).
*/
// ---------------------------------------------------------------------------
// Field extraction
// ---------------------------------------------------------------------------
/**
* Extract a single iCal property value, handling multi-line folded values (RFC 5545 §3.1).
*/
export function extractICalField(ical: string, field: string): string | null {
const unfolded = ical.replace(/\r?\n[ \t]/g, '');
const re = new RegExp(`^${field}(?:;[^:]*)?:(.*)$`, 'm');
const match = unfolded.match(re);
return match ? match[1]!.trim() : null;
}
/**
* Extract a component block by name (e.g. VEVENT, VTODO).
*/
export function extractComponentBlock(ical: string, component: string): string | null {
const startTag = `BEGIN:${component}`;
const endTag = `END:${component}`;
const start = ical.indexOf(startTag);
const end = ical.indexOf(endTag);
if (start === -1 || end === -1) {return null;}
return ical.slice(start, end + endTag.length);
}
// ---------------------------------------------------------------------------
// Text escaping
// ---------------------------------------------------------------------------
export function unescapeICalText(text: string): string {
return text
.replace(/\\n/g, '\n')
.replace(/\\,/g, ',')
.replace(/\\;/g, ';')
.replace(/\\\\/g, '\\');
}
export function escapeICalText(text: string): string {
return text
.replace(/\\/g, '\\\\')
.replace(/;/g, '\\;')
.replace(/,/g, '\\,')
.replace(/\n/g, '\\n');
}
// ---------------------------------------------------------------------------
// Date normalization
// ---------------------------------------------------------------------------
/**
* Normalize iCal date formats to ISO 8601.
* YYYYMMDD → YYYY-MM-DD
* YYYYMMDDTHHmmss(Z) → YYYY-MM-DDTHH:mm:ss(Z)
*/
export function normalizeICalDate(raw: string): string {
if (raw.length === 8) {
return `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}`;
}
if (raw.length >= 15) {
const d = `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}`;
const t = `${raw.slice(9, 11)}:${raw.slice(11, 13)}:${raw.slice(13, 15)}`;
const z = raw.endsWith('Z') ? 'Z' : '';
return `${d}T${t}${z}`;
}
return raw;
}
/**
* Convert ISO date + optional time to iCal format.
* "2026-04-15" → "20260415" (all-day)
* "2026-04-15", "14:30" → "20260415T143000"
*/
export function toICalDate(isoDate: string, isoTime: string | null): string {
const datePart = isoDate.replace(/-/g, '');
if (!isoTime) {return datePart;}
const timePart = isoTime.replace(/:/g, '').padEnd(6, '0');
return `${datePart}T${timePart}`;
}
/**
* Generate an iCal DTSTAMP from current time.
*/
export function nowDtstamp(): string {
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
}