Skip to content

Commit 23b7497

Browse files
Fix calendar event summary rendering (#78)
Attempt to follow https://icalendar.org/RFC-Specifications/iCalendar-RFC-5545/
1 parent 27e53dc commit 23b7497

8 files changed

Lines changed: 134 additions & 3 deletions

File tree

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,7 @@ jobs:
9696
--gc \
9797
--minify \
9898
--destination public
99+
100+
- name: Validate iCalendar feed
101+
run: node scripts/validate-calendar.mjs public/calendar/calendar.ics
102+

assets/css/fontawesome-subset.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,6 @@
7474
.fa-twitter:before{content:"\f099"}
7575
.fa-user-circle:before{content:"\f2bd"}
7676
.fa-users:before{content:"\f0c0"}
77+
.fa-video:before{content:"\f03d"}
7778
.fa-windows:before{content:"\f17a"}
7879
.fa-youtube:before{content:"\f167"}

assets/css/tailwind.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/fonts/fa-solid-subset.woff2

80 Bytes
Binary file not shown.

hugo.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ params:
146146
branch: "main"
147147

148148
ical:
149-
timezone: UTC
149+
timezone: Etc/UTC
150150

151151
module:
152152
imports:

layouts/list.calendar.ics

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{{- $calendar := "BEGIN:VCALENDAR\n" -}}
2+
{{- $calendar = printf "%s%s" $calendar (partial "header.ics" .) -}}
3+
{{- $timezones := slice -}}
4+
{{- range .Pages -}}
5+
{{- if .Params.startDate -}}
6+
{{- $timezone := partial "ical/get_timezone.ics" . -}}
7+
{{- if not (in $timezones $timezone) -}}
8+
{{- $timezones = $timezones | append $timezone -}}
9+
{{- end -}}
10+
{{- end -}}
11+
{{- end -}}
12+
{{- range $timezones -}}
13+
{{- $calendar = printf "%s%s" $calendar (partial "timezone.ics" .) -}}
14+
{{- end -}}
15+
{{- range .Pages -}}
16+
{{- if .Params.startDate -}}
17+
{{- $calendar = printf "%s%s" $calendar (partial "event.ics" .) -}}
18+
{{- end -}}
19+
{{- end -}}
20+
{{- $calendar = printf "%sEND:VCALENDAR\n" $calendar -}}
21+
{{- replace $calendar "\n" "\r\n" -}}

scripts/validate-calendar.mjs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { readFile } from 'node:fs/promises';
2+
3+
const input = process.argv[2];
4+
5+
if (!input) {
6+
throw new Error('Usage: node scripts/validate-calendar.mjs <calendar.ics|URL>');
7+
}
8+
9+
const calendar = new URL(input, 'file:').protocol === 'file:'
10+
? await readFile(input, 'utf8')
11+
: await (await fetch(input)).text();
12+
13+
const errors = [];
14+
const fail = (message) => errors.push(message);
15+
16+
if (/(^|[^\r])\n/.test(calendar)) {
17+
fail('Content lines must use CRLF line endings.');
18+
}
19+
20+
const lines = calendar.replace(/\r?\n[ \t]/g, '').split(/\r?\n/).filter(Boolean);
21+
const components = [];
22+
const events = [];
23+
const timezones = new Set();
24+
const timezoneReferences = new Set();
25+
let event;
26+
27+
for (const line of lines) {
28+
if (line.startsWith('BEGIN:')) {
29+
const component = line.slice('BEGIN:'.length);
30+
components.push(component);
31+
if (component === 'VEVENT') {
32+
event = new Map();
33+
events.push(event);
34+
}
35+
continue;
36+
}
37+
38+
if (line.startsWith('END:')) {
39+
const component = line.slice('END:'.length);
40+
if (components.pop() !== component) {
41+
fail(`Mismatched component terminator: ${line}`);
42+
}
43+
if (component === 'VEVENT') {
44+
event = undefined;
45+
}
46+
continue;
47+
}
48+
49+
const separator = line.indexOf(':');
50+
if (separator < 1) {
51+
fail(`Malformed content line: ${line}`);
52+
continue;
53+
}
54+
55+
const declaration = line.slice(0, separator);
56+
const [name, ...parameters] = declaration.split(';');
57+
const value = line.slice(separator + 1);
58+
59+
for (const parameter of parameters) {
60+
if (parameter.startsWith('TZID=')) {
61+
timezoneReferences.add(parameter.slice('TZID='.length));
62+
}
63+
}
64+
65+
if (components.at(-1) === 'VTIMEZONE' && name === 'TZID') {
66+
timezones.add(value);
67+
}
68+
69+
if (event) {
70+
event.set(name, [...(event.get(name) ?? []), value]);
71+
}
72+
}
73+
74+
if (components.length) {
75+
fail(`Unclosed component: ${components.at(-1)}`);
76+
}
77+
78+
if (lines[0] !== 'BEGIN:VCALENDAR' || lines.at(-1) !== 'END:VCALENDAR') {
79+
fail('Calendar must be wrapped in BEGIN:VCALENDAR and END:VCALENDAR.');
80+
}
81+
82+
for (const [index, properties] of events.entries()) {
83+
for (const property of ['UID', 'DTSTAMP', 'DTSTART']) {
84+
if (properties.get(property)?.length !== 1) {
85+
fail(`VEVENT ${index + 1} must contain exactly one ${property}.`);
86+
}
87+
}
88+
89+
const uid = properties.get('UID')?.[0];
90+
if (uid && events.some((other) => other !== properties && other.get('UID')?.[0] === uid)) {
91+
fail(`VEVENT ${index + 1} reuses UID ${uid}.`);
92+
}
93+
}
94+
95+
for (const timezone of timezoneReferences) {
96+
if (!timezones.has(timezone)) {
97+
fail(`TZID=${timezone} has no matching VTIMEZONE component.`);
98+
}
99+
}
100+
101+
if (errors.length) {
102+
throw new Error(`Invalid RFC 5545 calendar:\n- ${errors.join('\n- ')}`);
103+
}
104+
105+
console.log(`Validated ${events.length} VEVENT components.`);

themes/powershell-community/layouts/_default/calendar.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ <h2 class="text-3xl font-bold text-gray-900">Upcoming Events</h2>
6565
<!-- Event Details -->
6666
<div class="p-6 flex-1">
6767
<h3 class="text-xl font-bold text-gray-900 mb-2">{{ .Title }}</h3>
68-
<p class="text-gray-600 mb-3">{{ .Summary }}</p>
68+
<p class="text-gray-600 mb-3">{{ .Summary | plainify | truncate 240 }}</p>
6969
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500">
7070
<div class="flex items-center">
7171
<i class="fas fa-map-marker-alt text-purple-600 mr-2"></i>

0 commit comments

Comments
 (0)