Commit graph www/layouts/shortcodes/calendar.html
Author SHA1 Message Date
60ab628270 Parse the calendar with ical.js
The calendar page brought its own ICS parser and its own RRULE expansion. Both
only covered the cases that happened to be needed when they were written, and
the calendar has moved on since. Against the published calendar, for September
2026:

  Spieleabend  FREQ=WEEKLY;INTERVAL=2;BYDAY=SA  shown 05. 12. 19. 26., correct 05. 19.
  CCCB Plenum  FREQ=MONTHLY;BYDAY=TU;BYSETPOS=2 shown 01.,               correct 08.
  CCCB Plenum  FREQ=MONTHLY;BYDAY=TU;BYSETPOS=4 shown 01.,               correct 22.

INTERVAL was only read for monthly rules, so the Spieleabend was shown twice as
often as it takes place. BYSETPOS was not implemented at all, and since
parseInt("TU") is NaN the fallback turned both Plenum rules into "first
Tuesday", putting two Plenums on a day without one and none on the two days
with one. UNTIL, COUNT, EXDATE, RECURRENCE-ID, BYMONTHDAY and a BYDAY listing
more than one weekday were not handled either.

The text was no better. Content lines longer than 75 characters are continued
on the next line, of which there are 477 in the calendar, and the parser did
not join them, so it cut values off in the middle of a word. It also split
every line at the first colon, which lands inside the parameter of
DESCRIPTION;ALTREP="data:text/html,...". And it never resolved the escaping, so
"\n" was shown as those two characters. 30 of 31 descriptions were wrong:

  before: "Der Club Discordia ist ein öffentliches Treffen in den Clubr"
  after:  "Der Club Discordia ist ein öffentliches Treffen in den Clubräumen des CCC Berlin"

Hand the parsing and the expansion to ical.js, which is vendored for the start
page anyway. The month view now asks the library for the occurrences that touch
the month, which removes the reimplementation along with all of the above.

While the events are being reduced to what the view needs:

- An event is entered on every day it covers, so the Amateurfunk trip from
  30.10. to 01.11. is no longer marked on 30.10. alone. The end of an event is
  not part of it, so one ending at midnight stays on the day before.
- A time that names a zone is converted to Europe/Berlin instead of being read
  off the digits of the ICS string. Every event currently carries
  TZID=Europe/Berlin, so the wall clock time shown does not change, but a UTC
  timestamp would have been shown in UTC. A date is a different matter, see
  further down.
- The URL of the event is used for the link in the detail panel. Events without
  one are shown without a link, as on the start page. This replaces
  createEventLink(), which guessed URLs from the title and was never called,
  and the panel no longer builds an <h> element, which is not an element.

Descriptions may contain line breaks, so keep them in the panel. The times of
an event are labelled in German like the rest of the page, "Beginn" and "Ende"
instead of "Start" and "End".

An all day event carries a date, and a date has neither a time nor a zone: its
digits are the day itself. `toJSDate()` reads them as midnight in the zone of
the browser, and deriving the Berlin day from that afterwards moves the event
by the offset between the two, so the promise of the same days everywhere would
have held for every event except the ones that consist of nothing but days. An
event on 30. and 31.08. would have been marked on:

  Berlin       30. 31.08.
  Los Angeles  30. 31.08. 01.09.
  Tokio        29. 30. 31.08.

Take the day from the ICAL time, which still knows whether it names a day or a
point in time, and convert only the latter. Stepping to the end of the event
moves by a day where it is made of days and by a second where it is not, which
also expresses "the end is not part of the event" in the terms of the event
itself. The occurrences of a day are sorted by their start, which for an all
day event is that same midnight, so order them before the timed events instead.

Resolving an occurrence that was modified on its own needs two more things to
be right. Unless it is told which modifications belong to an event,
`ICAL.Event` relates every VEVENT with a RECURRENCE-ID in the file to every
recurring event and keys them by the recurrence id alone; the UID is only
compared with `strictExceptions`, which then throws instead of skipping. A
modification would therefore also override the occurrence another series holds
at the same instant, so the detail panel of that day would show the wrong event
and the modified one twice. Group the modifications by the UID of the event
they belong to and hand each event its own.

And the expansion walks the unmodified recurrence times, so an occurrence
pulled forward into the month from a later one would never be reached: the walk
stops at its original time, and the month it was moved out of drops it because
it no longer falls into it, which loses it from the calendar altogether.
Iterate far enough that the largest move towards the past can still reach the
month. Moving a Plenum or a Club Discordia to the week before, out of the way
of a holiday, is exactly what produces such a modification. With a weekly
series whose occurrence of 07.09. is moved to 28.08.:

  before: August     03. 10. 17. 24. 31.
          September  14. 21. 28.
  after:  August     03. 10. 17. 24. 28. 31.
          September  14. 21. 28.

The URL ends up in the href of a link and the calendar is exported from a
CalDAV server, so whoever may write to it decides what that is;
`URL:javascript:alert(1)` on an event would run that script when a visitor
clicks the name. Pass on nothing but http and https.

The stylesheet of the page goes through `minify | fingerprint` while the script
next to it is rewritten, so both are delivered the way the assets of the start
page already are: smaller, under a name that carries their content hash, and
with an integrity hash in the tag.

The published calendar has neither all day events nor RECURRENCE-ID today, both
can be created in the CalDAV calendar the export comes from. Checked in Berlin,
Tokyo, Los Angeles and Kiritimati: an all day event over 30. and 31.08. is
marked on those two days in all four, one from 31.08. to 02.09. is marked
across the month boundary, and the month view of the published calendar is the
same in all of them.

Fixes: 4068fab565 ("improved calendar and fixed url temporarily")
Assisted-by: Claude:claude-opus-5
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
2026-08-23 01:07:25 +02:00
Marek Krug
2b5277a3a0 fixed ics generation 2025-03-12 17:41:44 +01:00
Marek Krug
4068fab565 improved calendar and fixed url temporarily 2025-03-12 16:06:29 +01:00