From 45113eeb15e147871016bb34b8308f2fd7cdc796 Mon Sep 17 00:00:00 2001 From: clonejo Date: Mon, 18 May 2026 18:10:37 +0200 Subject: [PATCH 1/3] Generate schedule.xml from conference Items Pulling in the Tuesday/Wednesday.astro AST is a hack, but this way the existing data format does not have to change. And we're generating XML by string concatenation, but i did not feel to have the authority to pull in an XML library. Output tested using https://c3voc.de/schedulexml/validate and Giggity. --- src/components/timetable/Item.astro | 8 +- src/pages/schedule/schedule.xml.ts | 200 ++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 src/pages/schedule/schedule.xml.ts diff --git a/src/components/timetable/Item.astro b/src/components/timetable/Item.astro index b191d363..ffc862e7 100644 --- a/src/components/timetable/Item.astro +++ b/src/components/timetable/Item.astro @@ -49,7 +49,7 @@ const { }: Props = Astro.props; -function get_talk() { +export function get_talk(talk_slug: string) { let talk: any = null; if (talk_slug !== null) { const matches = import.meta.glob("../../content/talks/*.md", { eager: true }); @@ -63,7 +63,7 @@ function get_talk() { return talk } -function get_speakers() { +export function get_speakers(talk_slug: string) { let speakers: any[] = []; const matches = import.meta.glob("../../content/people/*.md", { eager: true }); const files: any[] = Object.values(matches); @@ -77,11 +77,11 @@ function get_speakers() { return speakers } -const talk = get_talk(); +const talk = get_talk(talk_slug); const talk_title = title ? title : talk ? talk.frontmatter.title : striped? "TBA": "ERROR"; const talk_url = url ?? (talk_slug?`/talks/${talk_slug}`:null); -const talk_speakers = speakers ?? get_speakers(); +const talk_speakers = speakers ?? get_speakers(talk_slug); const timetable_url = `/schedule/${Astro.url.pathname.split("/").filter(i => i != "").pop()}#${talk_url?.split("/").pop()}` --- diff --git a/src/pages/schedule/schedule.xml.ts b/src/pages/schedule/schedule.xml.ts new file mode 100644 index 00000000..6fcacf5f --- /dev/null +++ b/src/pages/schedule/schedule.xml.ts @@ -0,0 +1,200 @@ +// schedule.xml machine-readable conference format, see https://c3voc.de/wiki/schedule +// Output validated against https://c3voc.de/schedulexml/validate +// +// We use an evil hack to get the AST from the Tuesday and Wednesday.astro files that contain the data we need. + +import type { APIContext } from 'astro'; +import { parse } from "@astrojs/compiler"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import path from "path"; +import { createHash } from 'crypto'; +import { main, secondary, tertiary, quaternary } from "../../components/timetable/data.ts"; +// @ts-ignore +import { get_talk, get_speakers } from "../../components/timetable/Item.astro"; + +const dir = path.dirname(fileURLToPath(import.meta.url)); +const ROOM_NAMES: Record = { + main: main, + secondary: secondary, + tertiary: tertiary, + quaternary: quaternary, +} + +const CONF_TZ = "+02:00" + +async function getAst(file: string) { + const src = readFileSync(path.resolve(dir, file), "utf8"); + const { ast } = await parse(src); + return ast; +} + + +export async function GET(context: APIContext): Promise { + const tuesdayAst = await getAst("../../components/timetable/Tuesday.astro"); + const wednesdayAst = await getAst("../../components/timetable/Wednesday.astro"); + + var rssString = ` + + + + 1.0 + + RustWeek 2026 + rustnl2026 + 2 + 2026-05-19 + 2026-05-20 + 00:05 + Europe/Amsterdam + + `; + + rssString += astroToXml(tuesdayAst, 1); + rssString += astroToXml(wednesdayAst, 2); + + rssString += `\n` + rssString = rssString.trim(); + return new Response(rssString, { + headers: { + 'Content-Type': 'application/xml', + }, + }); +} + +function astroToXml(ast: any, dayIndex: number): string { + const timetable = ast.children.filter((child: any) => child.type == "component" && child.name == "Timetable")[0]; + + const dateStr = getAttr(timetable, "day")!; + + var tracks: Array = []; + var rooms: Record = {}; + + var items: Array = []; + + timetable.children.forEach((child: any) => { + if (child.type == "component" && child.name == "TrackIndicator") { + tracks.push(getAttr(child, "track")!); + } else if (child.type == "component" && child.name == "Item") { + let item = parseItem(child); + item.track = tracks[parseInt(item.track) - 1]; + if (item.slug) { + const talk = get_talk(item.slug) + item.title = talk.frontmatter.title; + //item.track = talk.frontmatter.tracks[0]; + if (!item.speakers) { + item.speakers = item.speakers || get_speakers(item.slug); + } + } + items.push(item); + rooms[item.room] = true; + } + }); + + var rssString = `\n` + + for (let room in rooms) { + rssString += ` \n` + + items.filter((item) => item.room == room).forEach((item) => { + const slug = item.slug ? slugify(item.slug) : slugify(item.title!) + `-${dateStr}-${item.track}`; + const id = slugToId(slug); + const guid = slugToGuid(slug); + let personsXml = ""; + item.speakers?.forEach((speaker) => { + personsXml += `${speaker.name}` + }); + rssString += ` + + ${dateTimeStr(dateStr, item.time!)} + ${item.time?.padStart(5, "0")} + ${formatDuration(item.durationMins)} + ${room} + ${item.slug ? `https://2026.rustweek.org/talks/${item.slug}` : ""} + rustnl2026-${slug.padEnd(4, "_")} + ${escape(item.title ?? item.slug!)} + ${item.track} + talk + + + ${personsXml} + \n`; + }); + + rssString += ` \n` + } + rssString += `\n` + + return rssString; +} + +type Item = { + title: string | undefined; + slug: string | undefined; + track: string; + time: string; + durationMins: number; + room: string; + speakers: Array | undefined; +}; + +function parseItem(node: any): Item { + const title = getAttr(node, "title"); + const slug = getAttr(node, "talk_slug"); + const track = getAttr(node, "track")!; + const time = getAttr(node, "time")!; + const durationMins = parseInt(getAttr(node, "duration") ?? "0"); + const room = getAttr(node, "room")!; + const speakersStr = getAttr(node, "speakers"); + const speakers = speakersStr ? parseSpeakers(speakersStr) : undefined; + if (!slug && !title) throw new Error(`Item has neither slug nor title: ${node}`); + return { title, slug, track, time, durationMins, room, speakers }; +} + +function getAttr(node: any, attrName: string): string | undefined { + const attr = node.attributes.filter((attr: any) => attr.name == attrName)[0]; + if (attr === undefined) { + return undefined; + } + return attr.value; +} + +function escape(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function dateTimeStr(dateStr: string, time: string): string { + return `${dateStr}T${time.padStart(5, "0")}:00${CONF_TZ}`; +} + +function slugify(s: string): string { + return s.toLowerCase().replace("ł", "l").replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); +} + +function slugToId(seed: string): number { + const h = createHash('sha1').update(seed).digest('hex'); + return parseInt(h.slice(0, 8), 16) & 0x7fffffff; +} +function slugToGuid(seed: string): string { + const h = createHash('sha1').update(seed).digest('hex'); + return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`; +} + +function formatDuration(mins: number): string { + return `${String(Math.floor(mins / 60)).padStart(2, '0')}:${String(mins % 60).padStart(2, '0')}`; +} + +type Speaker = { + name: string; +}; + +function parseSpeakers(str: string): Array { + // i'm sorry … + const arr = Function(`"use strict"; return (${str})`)(); + return arr.map((obj: Speaker) => { return { "name": obj.name } }); +} From 4a5b2024394a18542797689c2bc94ed443c07f16 Mon Sep 17 00:00:00 2001 From: clonejo Date: Mon, 18 May 2026 18:50:13 +0200 Subject: [PATCH 2/3] Add ticket requirement hint to schedule.xml track Allows to filter out talks with a ticket requirement in Giggity. --- src/pages/schedule/schedule.xml.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/schedule/schedule.xml.ts b/src/pages/schedule/schedule.xml.ts index 6fcacf5f..3c26cfee 100644 --- a/src/pages/schedule/schedule.xml.ts +++ b/src/pages/schedule/schedule.xml.ts @@ -113,7 +113,7 @@ function astroToXml(ast: any, dayIndex: number): string { ${item.slug ? `https://2026.rustweek.org/talks/${item.slug}` : ""} rustnl2026-${slug.padEnd(4, "_")} ${escape(item.title ?? item.slug!)} - ${item.track} + ${item.track}${item.ticket ? `. ${item.ticket}` : ""} talk @@ -132,6 +132,7 @@ type Item = { title: string | undefined; slug: string | undefined; track: string; + ticket: string | undefined; time: string; durationMins: number; room: string; @@ -142,13 +143,14 @@ function parseItem(node: any): Item { const title = getAttr(node, "title"); const slug = getAttr(node, "talk_slug"); const track = getAttr(node, "track")!; + const ticket = getAttr(node, "ticket"); const time = getAttr(node, "time")!; const durationMins = parseInt(getAttr(node, "duration") ?? "0"); const room = getAttr(node, "room")!; const speakersStr = getAttr(node, "speakers"); const speakers = speakersStr ? parseSpeakers(speakersStr) : undefined; if (!slug && !title) throw new Error(`Item has neither slug nor title: ${node}`); - return { title, slug, track, time, durationMins, room, speakers }; + return { title, slug, track, ticket, time, durationMins, room, speakers }; } function getAttr(node: any, attrName: string): string | undefined { From be19e0b0dbd95bf57417644159300e00a602b1bb Mon Sep 17 00:00:00 2001 From: clonejo Date: Mon, 18 May 2026 19:17:20 +0200 Subject: [PATCH 3/3] Add URL to description Since we don't have the actual talk description, at least put the URL. For some reason Giggity does not let you open event links. --- src/pages/schedule/schedule.xml.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/schedule/schedule.xml.ts b/src/pages/schedule/schedule.xml.ts index 3c26cfee..83da5adb 100644 --- a/src/pages/schedule/schedule.xml.ts +++ b/src/pages/schedule/schedule.xml.ts @@ -104,19 +104,20 @@ function astroToXml(ast: any, dayIndex: number): string { item.speakers?.forEach((speaker) => { personsXml += `${speaker.name}` }); + const url = item.slug ? `https://2026.rustweek.org/talks/${item.slug}` : undefined; rssString += ` ${dateTimeStr(dateStr, item.time!)} ${item.time?.padStart(5, "0")} ${formatDuration(item.durationMins)} ${room} - ${item.slug ? `https://2026.rustweek.org/talks/${item.slug}` : ""} + ${url ? `${url}` : ""} rustnl2026-${slug.padEnd(4, "_")} ${escape(item.title ?? item.slug!)} ${item.track}${item.ticket ? `. ${item.ticket}` : ""} talk - + ${url ? url : ""} ${personsXml} \n`; });