Generating an .ics file is one of those tasks that looks finished long before it is. The structure is trivial — a few key-value lines between BEGIN and END — so the first version works, opens in Google Calendar, and gets waved through.
Then the spec asks for something easy to misread. RFC 5545 says lines must not exceed 75 octets, and that longer ones are folded by inserting CRLF and a single space. The naive implementation reads "75" and reaches for length.
// Wrong, and passes every test written in ASCII
if (line.length <= 75) return lineString.length counts UTF-16 code units, not bytes. Ordinary Latin text makes the two identical, which is exactly why this survives review. An em dash is three bytes. A curly quote is three bytes. An emoji is four, and takes two code units while it is at it.
This site’s copy is full of em dashes. A description line counted at 75 characters was going out at well over 100 octets — invalid, and rejected outright by stricter parsers.
Measuring the thing the spec measures
const enc = new TextEncoder()
let current = ''
let limit = 75
// Array.from splits on code points, so a surrogate pair is never cut in half.
for (const char of Array.from(line)) {
if (enc.encode(current + char).length > limit) {
out.push(current)
current = char
limit = 74 // continuation lines lose an octet to the leading space
} else {
current += char
}
}Two details carry their weight there. Iterating with Array.from walks code points, so an emoji is never split down the middle into two invalid halves. And the limit drops to 74 after the first line, because the continuation space is itself an octet — an off-by-one that only shows up on the longest lines, which are the ones least likely to be in a test.
Any spec that says "octets" is telling you your test data is too polite.
