so

Balisage 2021 ICS Calendar

Volume 5, Issue 18; 31 Jul 2021

Balisage starts today! Here’s an ICS file for the sessions and a few notes about how it was created.

Let’s not bury the lede, here’s the .ics file, if that’s all you’re after.

The background for this file is that Michael Sperberg-McQueen and I are presenting our paper at Balisage tomorrow. It’s called “Interactivity Three Ways” and it’s a comparison of different ways to add interactivity to web pages: “plain old JavaScript”, XForms, and Saxon-JS.

The bit of interactivity in question is the schedule-at-a-glance table at the bottom of the program page. In addition to having the web page open, I usually add the talks to my actual calendar. It’s convenient and it means I can put alarms on the sessions that I’m moderating.

I haven’t added them to my calendar yet and when I woke up this morning I thought, “hang on a minute, this year I’ve got XSLT for processing the Balisage schedule, can’t I just generate an .ics file for the sessions and import that into my calendar?”

Yes. Yes, I can.

I didn’t invest a lot of effort in checking the file’s conformance against the iCalendar specification. I reverse-engineered it from an exported file. I did check that it works in MacOS Calendar and Google Calendar. If you have trouble with it in your calendar, do let me know.

All the code for the three approaches will be published tomorrow, but in the meantime, I’ll scribble down a few notes about the ICS hack because that’s not part of the paper.

The only interesting parts of generating the .ics file are the UUIDs and the download link.

UUIDs

What’s interesting about UUIDs is that they have to be, uh, unique. Practically speaking, that’s usually accomplished by making them random. The UUID specification describes a number of important considerations in generating appropriately unique UUIDs. I’m ignoring them all and just generating 32 random hex digits.

But how do you generate random numbers in XSLT? XPath 3.1 introduced a random-number-generator() function, but it’s a funny kind of function. It returns a map that contains: a random number and a “next” function (and a “permute” function that isn’t relevant here).

If you call random-number-generator() over and over again with the same seed, you’ll get the same random number every time. You have to call the “next” function to get the next number in the sequence.

But you can’t (usefully) save the “next” function in a variable, because variables are immutable. So you have to do this in some kind of loop. Old school, with a recursive function, or the easy way with xsl:iterate.

And then there’s the question of the seed. One common technique for seeding a random number generator is to use the current time as the seed. But that doesn’t work in XSLT because the “current time” is fixed for the duration of the transformation. The current-dateTime() function always returns the same thing!

Luckily, every event has a unique ID, so we can just use that.

Here’s the code I came up with:

<xsl:function name="f:uuid" as="xs:string">
  <xsl:param name="seed" as="xs:string"/>

  <xsl:variable name="hexdigits"
                select="('0', '1', '2', '3', '4', '5', '6', '7',
                         '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')"/>

  <xsl:variable name="hexstring" as="xs:string">
    <xsl:iterate select="1 to 32">
      <xsl:param name="digits" select="''"/>
      <xsl:param name="random"
                 select="random-number-generator($seed)"/>
      <xsl:on-completion select="$digits"/>

      <xsl:variable name="hex"
                    select="floor($random?number * 16) + 1"/>

      <xsl:next-iteration>
        <xsl:with-param name="digits"
                        select="$digits || subsequence($hexdigits, $hex, 1)"/>
        <xsl:with-param name="random" select="$random?next()"/>
      </xsl:next-iteration>
    </xsl:iterate>
  </xsl:variable>

  <xsl:sequence select="substring($hexstring, 1, 8)
                        || '-'
                        || substring($hexstring, 9, 4)
                        || '-'
                        || substring($hexstring, 13, 4)
                        || '-'
                        || substring($hexstring, 17, 4)
                        || '-'
                        || substring($hexstring, 21)"/>

</xsl:function>

I’m sure there’s room for improvement, but it gets the job done. The caller is responsible for passing in the seed value:

  <xsl:variable name="event" as="node()">
    <xsl:text expand-text="yes">BEGIN:VEVENT
TRANSP:OPAQUE
UID:{f:uuid(@id)}
…</xsl:text>
  </xsl:variable>

The download link

My XSLT code is running in Saxon-JS in the browser. There’s no where for it to store a file, so how do I make it possible to download the ICS file? (The link at the top of this post is to a file on a web server, but that’s not how the application actually works.)

Here’s the trick:

<a href="data:text/ics;charset=utf-8,{encode-for-uri($calendar)}"
   target="_blank" download="Balisage-2021.ics">ICS</a>

Given that I’ve constructed $calendar so that it’s a single string containing the whole .ics file, that creates a “data:” URI (a great big 40+kb URI string that encodes the ICS file) and the target and download attributes are enough to persuade the browser to offer it as a download.

Much easier than I feared!

See you at Balisage! (It’s virtual this year and, at the time of writing, there’s still time to register! Don’t miss it!)