Skip to content
File

Blob: src/worker/caldav/ical/render.ts

typescript59 lines
1import { documentHasRecurrence, parseProperty, recurrenceBase } from "@/worker/caldav/ical/core";
2import { parseCalendarDocument } from "@/worker/caldav/ical/parse";
3import { calendarOccurrences, occurrenceOverlaps } from "@/worker/caldav/ical/recurrence";
4import type { CalendarOccurrence, CalendarTimeRange, RecurrenceBounds } from "@/worker/caldav/ical/types";
5 
6function formatUtcDate(ms: number): string {
7 const date = new Date(ms);
8 const pad = (value: number) => String(value).padStart(2, "0");
9 return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}`;
10}
11 
12function formatUtcDateTime(ms: number): string {
13 const date = new Date(ms);
14 const pad = (value: number) => String(value).padStart(2, "0");
15 return `${formatUtcDate(ms)}T${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}Z`;
16}
17 
18function datePropertyLine(name: string, ms: number, valueType: "date" | "date-time"): string {
19 return valueType === "date" ? `${name};VALUE=DATE:${formatUtcDate(ms)}` : `${name}:${formatUtcDateTime(ms)}`;
20}
21 
22function expandedComponent(occurrence: CalendarOccurrence, forceRecurrenceId: boolean): string[] {
23 const excluded = new Set(["DTSTART", "DTEND", "DUE", "DURATION", "RRULE", "RDATE", "EXDATE", "RECURRENCE-ID"]);
24 const preserved = occurrence.component.lines.filter((line) => {
25 const prop = parseProperty(line);
26 return prop && !excluded.has(prop.name);
27 });
28 const lines = [`BEGIN:${occurrence.component.componentType}`, ...preserved];
29 const recurrenceValueType =
30 occurrence.component.recurrenceId?.valueType ?? recurrenceBase(occurrence.component)?.valueType ?? "date-time";
31 const startValueType =
32 occurrence.component.dtstart?.valueType ?? recurrenceBase(occurrence.component)?.valueType ?? "date-time";
33 if (forceRecurrenceId || !occurrence.isInitial || occurrence.component.recurrenceId) {
34 lines.push(datePropertyLine("RECURRENCE-ID", occurrence.originalStartMs, recurrenceValueType));
35 }
36 lines.push(datePropertyLine("DTSTART", occurrence.startMs, startValueType));
37 if (occurrence.component.componentType === "VEVENT") {
38 if (occurrence.endMs !== occurrence.startMs)
39 lines.push(datePropertyLine("DTEND", occurrence.endMs, startValueType));
40 } else if (occurrence.component.componentType === "VTODO") {
41 lines.push(datePropertyLine("DUE", occurrence.endMs, startValueType));
42 }
43 lines.push(`END:${occurrence.component.componentType}`);
44 return lines;
45}
46 
47export function expandCalendarData(body: string, range: CalendarTimeRange, bounds: RecurrenceBounds): string {
48 const document = parseCalendarDocument(body);
49 const lines = ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//dab//CalDAV//EN"];
50 const forceRecurrenceId = documentHasRecurrence(document.components) && document.overrides.length === 0;
51 for (const occurrence of calendarOccurrences(document, bounds, range)) {
52 if (occurrenceOverlaps(document, occurrence, range)) {
53 lines.push(...expandedComponent(occurrence, forceRecurrenceId));
54 }
55 }
56 lines.push("END:VCALENDAR", "");
57 return lines.join("\r\n");
58}