-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex-cal.js
161 lines (138 loc) · 5.47 KB
/
index-cal.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// assumes ical.js, rrule.js loaded
const icsUrl = 'calendar.ics';
function escape(str) {
return str.replace(/[&<>"'/]/g, function (char) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return escapeMap[char] || char;
});
}
function linkAndEscape(str) {
if (!str) return '';
const escaped = str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
const urlRegex = /(https?:\/\/[^\s]+)/g;
return escaped.replace(urlRegex, (url) => {
const safeUrl = encodeURI(url);
return `<a href="${safeUrl}" target="_blank">${safeUrl}</a>`;
});
}
async function fetchCalendar(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`naurrrr :( failed to fetch calendar: ${response.status} ${response.statusText}`);
}
const icsData = await response.text();
const jcalData = ICAL.parse(icsData);
const vcalendar = new ICAL.Component(jcalData);
return vcalendar;
}
// Armed with a list of events, draw them into an HTML ul list.
function drawEventList(events) {
const dateOptions = {
weekday: 'long',
month: "numeric",
day: "numeric",
};
const timeOptions = {
hour: "numeric",
minute: "numeric",
};
const ul = document.createElement('ul');
events.forEach(event => {
const li = document.createElement('li');
const dateStr = `${event.start.toLocaleDateString('en-US', dateOptions)}`.toLocaleLowerCase();
const timeStr = `${event.start.toLocaleTimeString('en-US', timeOptions)} - ${event.end.toLocaleTimeString('en-US', timeOptions)}`.toLocaleLowerCase();
const descriptionStr = event.description ? `<br>${linkAndEscape(event.description)}` : '';
const recurrence = event.recurrence ? `${event.recurrence}<br>`.toLocaleLowerCase() : '';
li.innerHTML = `${escape(event.summary)}<span class="secondary">${descriptionStr}<br>${recurrence}${dateStr}<br>${timeStr}</span>`;
ul.appendChild(li);
});
return ul;
}
// Given an ical event, return a human-readable description of the recurrence
// (such as "every week").
function describeRecurrence(icalEvent) {
const ruleStr = icalEvent.component.getFirstPropertyValue('rrule').toString();
const parsed = rrule.RRule.fromString(ruleStr);
return parsed.toText();
}
// Given a parsed ical.js calendar, return a list of upcoming events ("upcoming"
// here, for now, means events after 3 hours ago. This is so that in-progress
// events are still shown).
function getUpcomingEvents(vcalendar, numEvents = 10) {
const events = vcalendar.getAllSubcomponents('vevent');
const hoursAfterEnd = ICAL.Time.now();
hoursAfterEnd.addDuration(ICAL.Duration.fromSeconds(-3 * 60 * 60));
const upcomingEvents = [];
let titlesSeen = new Set();
events.forEach(event => {
const icalEvent = new ICAL.Event(event);
if (icalEvent.isRecurring()) {
// an attempt at preventing duplicate events when the same event was
// preempted by an one-off instance created by editing the single
// occurrence in NextCloud.
if (titlesSeen.has(icalEvent.summary)) {
return;
}
const expand = icalEvent.iterator();
let next;
// only add the next instance (later we show the recurrence)
// so this actually only runs once
while (next = expand.next()) {
const duration = icalEvent.duration;
const end = next.clone();
end.addDuration(duration);
if (end.compare(hoursAfterEnd) >= 0) {
titlesSeen.add(icalEvent.summary);
upcomingEvents.push({
summary: icalEvent.summary,
description: icalEvent.description,
start: next.toJSDate(),
end: end.toJSDate(),
recurrence: describeRecurrence(icalEvent)
});
break;
}
}
} else {
const eventStart = icalEvent.startDate;
if (eventStart.compare(hoursAfterEnd) >= 0) {
titlesSeen.add(icalEvent.summary);
upcomingEvents.push({
summary: icalEvent.summary,
description: icalEvent.description,
start: eventStart.toJSDate(),
end: icalEvent.endDate.toJSDate(),
});
}
}
});
console.log("events added: " + Array.from(titlesSeen));
return upcomingEvents.sort((a, b) => a.start - b.start).slice(0, numEvents);
}
// Main function :)
async function doCalendar(calendarUrl, targetElementId) {
const targetElement = document.getElementById(targetElementId);
try {
const vcalendar = await fetchCalendar(calendarUrl);
const upcomingEvents = getUpcomingEvents(vcalendar);
const eventList = drawEventList(upcomingEvents);
targetElement.innerHTML = '';
targetElement.appendChild(eventList);
}
catch (error) {
targetElement.innerHTML = '<p>Sorry - couldn\'t load the calendar :(</p> <small>' + error + '</small>';
}
}
doCalendar(icsUrl, 'cal-parsed');