92 lines
2.9 KiB
TypeScript
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}/, '');
|
|
}
|