feat: implement parseICS with ical.js
This commit is contained in:
121
src/lib/ical-io.js
Normal file
121
src/lib/ical-io.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import ICAL from 'ical.js'
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Timezone registration (ical.js does not bundle IANA zones)
|
||||
// ------------------------------------------------------------------ //
|
||||
function registerTimezones(vcalendar) {
|
||||
for (const vtz of vcalendar.getAllSubcomponents('vtimezone')) {
|
||||
const tzid = vtz.getFirstPropertyValue('tzid')
|
||||
if (tzid) {
|
||||
ICAL.TimezoneService.register(tzid, new ICAL.Timezone({ component: vtz, tzid }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------ //
|
||||
function pad2(n) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
function timeToISODate(t) {
|
||||
return `${t.year}-${pad2(t.month)}-${pad2(t.day)}`
|
||||
}
|
||||
|
||||
function timeToHHMM(t) {
|
||||
return `${pad2(t.hour)}:${pad2(t.minute)}`
|
||||
}
|
||||
|
||||
function getTzid(component, propName) {
|
||||
const prop = component.getFirstProperty(propName)
|
||||
if (!prop) return 'UTC'
|
||||
const tzid = prop.getParameter('tzid')
|
||||
return tzid || 'UTC'
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Parse a single VEVENT component -> plain EventModel
|
||||
// ------------------------------------------------------------------ //
|
||||
function parseEvent(veventComp) {
|
||||
const event = new ICAL.Event(veventComp)
|
||||
const start = event.startDate
|
||||
const end = event.endDate
|
||||
|
||||
// Determine tzid
|
||||
const tzid = getTzid(veventComp, 'dtstart')
|
||||
|
||||
// RRULE
|
||||
const recur = veventComp.getFirstPropertyValue('rrule')
|
||||
let rrule = null
|
||||
if (recur) {
|
||||
const bydayArr = recur.getComponent('byday') // string[]
|
||||
let untilDate = null
|
||||
if (recur.until) {
|
||||
const u = recur.until
|
||||
untilDate = `${u.year}-${pad2(u.month)}-${pad2(u.day)}`
|
||||
}
|
||||
rrule = {
|
||||
freq: recur.freq,
|
||||
interval: recur.interval || 1,
|
||||
byday: bydayArr.length > 0 ? bydayArr[0] : null,
|
||||
untilDate,
|
||||
}
|
||||
}
|
||||
|
||||
// EXDATEs
|
||||
const exdates = []
|
||||
for (const prop of veventComp.getAllProperties('exdate')) {
|
||||
for (const t of prop.getValues()) {
|
||||
exdates.push(timeToISODate(t))
|
||||
}
|
||||
}
|
||||
|
||||
// _raw: capture DTSTAMP and any other unmapped properties
|
||||
// Use toICALString() to preserve the compact RFC5545 form ("20260711T085008Z")
|
||||
// rather than toString()'s ISO 8601 form ("2026-07-11T08:50:08Z").
|
||||
const _raw = {}
|
||||
const dtstamp = veventComp.getFirstPropertyValue('dtstamp')
|
||||
if (dtstamp) _raw.dtstamp = dtstamp.toICALString()
|
||||
|
||||
return {
|
||||
uid: veventComp.getFirstPropertyValue('uid') || '',
|
||||
summary: veventComp.getFirstPropertyValue('summary') || '',
|
||||
location: veventComp.getFirstPropertyValue('location') || '',
|
||||
description: veventComp.getFirstPropertyValue('description') || '',
|
||||
dtstartDate: timeToISODate(start),
|
||||
dtstartTime: timeToHHMM(start),
|
||||
dtendTime: timeToHHMM(end),
|
||||
tzid,
|
||||
rrule,
|
||||
exdates,
|
||||
_raw,
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Parse full ICS text -> { meta, events }
|
||||
// ------------------------------------------------------------------ //
|
||||
export function parseICS(text) {
|
||||
const jcal = ICAL.parse(text)
|
||||
const vcalendar = new ICAL.Component(jcal)
|
||||
|
||||
// Register timezones from VTIMEZONEs before constructing zoned Times
|
||||
registerTimezones(vcalendar)
|
||||
|
||||
// Meta
|
||||
const meta = {
|
||||
prodid: vcalendar.getFirstPropertyValue('prodid') || '',
|
||||
version: vcalendar.getFirstPropertyValue('version') || '2.0',
|
||||
calscale: vcalendar.getFirstPropertyValue('calscale') || 'GREGORIAN',
|
||||
method: vcalendar.getFirstPropertyValue('method') || null,
|
||||
xWrCalname: vcalendar.getFirstPropertyValue('x-wr-calname') || null,
|
||||
xWrTimezone: vcalendar.getFirstPropertyValue('x-wr-timezone') || null,
|
||||
vtimezones: vcalendar.getAllSubcomponents('vtimezone'),
|
||||
}
|
||||
|
||||
// Events
|
||||
const events = vcalendar.getAllSubcomponents('vevent').map(parseEvent)
|
||||
|
||||
return { meta, events }
|
||||
}
|
||||
Reference in New Issue
Block a user