templates2.Booking.Next/lib/ics.ts
w4c-templates 6a0c2e88d2
Some checks are pending
CI / quality (push) Waiting to run
chore(template): publish Booking.Next
2026-10-11 23:27:02 +03:00

70 lines
2.2 KiB
TypeScript

// Minimal RFC 5545 (iCalendar) generator — plain text, no dependencies.
export type IcsEvent = {
uid: string;
title: string;
description?: string;
location?: string;
url?: string;
start: Date;
end: Date;
organizerName?: string;
organizerEmail?: string;
status?: 'CONFIRMED' | 'TENTATIVE' | 'CANCELLED';
stamp?: Date;
};
/** Format an instant as a UTC iCalendar DATE-TIME: YYYYMMDDTHHMMSSZ. */
export function toIcsDate(date: Date): string {
return `${date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z')}`;
}
function escapeText(value: string): string {
return value
.replace(/\\/g, '\\\\')
.replace(/;/g, '\\;')
.replace(/,/g, '\\,')
.replace(/\r?\n/g, '\\n');
}
/** Folds a content line at 75 octets per RFC 5545 (continuation lines start with a space). */
function fold(line: string): string {
if (line.length <= 75) return line;
const parts: string[] = [line.slice(0, 75)];
let rest = line.slice(75);
while (rest.length > 74) {
parts.push(' ' + rest.slice(0, 74));
rest = rest.slice(74);
}
if (rest.length) parts.push(' ' + rest);
return parts.join('\r\n');
}
/** Build a single-event VCALENDAR document (CRLF line endings). */
export function buildIcs(event: IcsEvent): string {
const stamp = event.stamp ?? new Date();
const lines: string[] = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//w4c//Booking//EN',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'BEGIN:VEVENT',
`UID:${escapeText(event.uid)}`,
`DTSTAMP:${toIcsDate(stamp)}`,
`DTSTART:${toIcsDate(event.start)}`,
`DTEND:${toIcsDate(event.end)}`,
`SUMMARY:${escapeText(event.title)}`,
];
if (event.description) lines.push(`DESCRIPTION:${escapeText(event.description)}`);
if (event.location) lines.push(`LOCATION:${escapeText(event.location)}`);
if (event.url) lines.push(`URL:${escapeText(event.url)}`);
if (event.organizerEmail) {
const cn = event.organizerName ? `;CN=${escapeText(event.organizerName)}` : '';
lines.push(`ORGANIZER${cn}:mailto:${escapeText(event.organizerEmail)}`);
}
if (event.status) lines.push(`STATUS:${event.status}`);
lines.push('END:VEVENT', 'END:VCALENDAR');
return lines.map(fold).join('\r\n') + '\r\n';
}