@pipobscure/ical 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +661 -0
  3. package/dist/alarm.d.ts +35 -0
  4. package/dist/alarm.d.ts.map +1 -0
  5. package/dist/alarm.js +87 -0
  6. package/dist/alarm.js.map +1 -0
  7. package/dist/calendar.d.ts +52 -0
  8. package/dist/calendar.d.ts.map +1 -0
  9. package/dist/calendar.js +121 -0
  10. package/dist/calendar.js.map +1 -0
  11. package/dist/component.d.ts +48 -0
  12. package/dist/component.d.ts.map +1 -0
  13. package/dist/component.js +170 -0
  14. package/dist/component.js.map +1 -0
  15. package/dist/event.d.ts +74 -0
  16. package/dist/event.d.ts.map +1 -0
  17. package/dist/event.js +263 -0
  18. package/dist/event.js.map +1 -0
  19. package/dist/freebusy.d.ts +45 -0
  20. package/dist/freebusy.d.ts.map +1 -0
  21. package/dist/freebusy.js +111 -0
  22. package/dist/freebusy.js.map +1 -0
  23. package/dist/index.d.ts +41 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +43 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/journal.d.ts +48 -0
  28. package/dist/journal.d.ts.map +1 -0
  29. package/dist/journal.js +148 -0
  30. package/dist/journal.js.map +1 -0
  31. package/dist/parse.d.ts +27 -0
  32. package/dist/parse.d.ts.map +1 -0
  33. package/dist/parse.js +140 -0
  34. package/dist/parse.js.map +1 -0
  35. package/dist/property-registry.d.ts +31 -0
  36. package/dist/property-registry.d.ts.map +1 -0
  37. package/dist/property-registry.js +83 -0
  38. package/dist/property-registry.js.map +1 -0
  39. package/dist/property.d.ts +36 -0
  40. package/dist/property.d.ts.map +1 -0
  41. package/dist/property.js +135 -0
  42. package/dist/property.js.map +1 -0
  43. package/dist/timezone.d.ts +55 -0
  44. package/dist/timezone.d.ts.map +1 -0
  45. package/dist/timezone.js +141 -0
  46. package/dist/timezone.js.map +1 -0
  47. package/dist/todo.d.ts +67 -0
  48. package/dist/todo.d.ts.map +1 -0
  49. package/dist/todo.js +220 -0
  50. package/dist/todo.js.map +1 -0
  51. package/dist/tokenize.d.ts +22 -0
  52. package/dist/tokenize.d.ts.map +1 -0
  53. package/dist/tokenize.js +95 -0
  54. package/dist/tokenize.js.map +1 -0
  55. package/dist/types.d.ts +100 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +3 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/value-types.d.ts +81 -0
  60. package/dist/value-types.d.ts.map +1 -0
  61. package/dist/value-types.js +445 -0
  62. package/dist/value-types.js.map +1 -0
  63. package/package.json +45 -0
  64. package/src/alarm.ts +105 -0
  65. package/src/calendar.ts +153 -0
  66. package/src/component.ts +193 -0
  67. package/src/event.ts +307 -0
  68. package/src/freebusy.ts +133 -0
  69. package/src/index.ts +85 -0
  70. package/src/journal.ts +174 -0
  71. package/src/parse.ts +166 -0
  72. package/src/property-registry.ts +124 -0
  73. package/src/property.ts +163 -0
  74. package/src/timezone.ts +169 -0
  75. package/src/todo.ts +253 -0
  76. package/src/tokenize.ts +99 -0
  77. package/src/types.ts +135 -0
  78. package/src/value-types.ts +498 -0
@@ -0,0 +1,498 @@
1
+ /**
2
+ * RFC 5545 §3.3 — Value Data Types
3
+ *
4
+ * Each exported object has:
5
+ * parse(raw, params?) → typed value (tolerant)
6
+ * serialize(val, params?) → string (strict)
7
+ */
8
+
9
+ import type {
10
+ ICalDate,
11
+ ICalDateTime,
12
+ ICalTime,
13
+ ICalDuration,
14
+ ICalPeriod,
15
+ ICalRecur,
16
+ ICalUtcOffset,
17
+ ICalGeo,
18
+ ICalBinary,
19
+ RecurFreq,
20
+ Weekday,
21
+ ByDayRule,
22
+ } from './types.js';
23
+
24
+ // ── Helpers ──────────────────────────────────────────────────────────────────
25
+
26
+ function pad2(n: number): string {
27
+ return String(n).padStart(2, '0');
28
+ }
29
+
30
+ function pad4(n: number): string {
31
+ return String(n).padStart(4, '0');
32
+ }
33
+
34
+ function isValidDate(y: number, m: number, d: number): boolean {
35
+ if (m < 1 || m > 12 || d < 1 || d > 31) return false;
36
+ const dt = new Date(Date.UTC(y, m - 1, d));
37
+ return dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d;
38
+ }
39
+
40
+ // ── TEXT (RFC 5545 §3.3.11) ───────────────────────────────────────────────
41
+
42
+ export const TEXT = {
43
+ parse(raw: string): string {
44
+ // Unescape: \n \N \, \; \\ → newline , ; backslash
45
+ return raw
46
+ .replace(/\\n/gi, '\n')
47
+ .replace(/\\,/g, ',')
48
+ .replace(/\\;/g, ';')
49
+ .replace(/\\\\/g, '\\');
50
+ },
51
+ serialize(val: unknown): string {
52
+ if (val === null || val === undefined) return '';
53
+ return String(val)
54
+ .replace(/\\/g, '\\\\')
55
+ .replace(/;/g, '\\;')
56
+ .replace(/,/g, '\\,')
57
+ .replace(/\n/g, '\\n');
58
+ },
59
+ };
60
+
61
+ // ── BOOLEAN (RFC 5545 §3.3.2) ─────────────────────────────────────────────
62
+
63
+ export const BOOLEAN = {
64
+ parse(raw: string): boolean {
65
+ return raw.trim().toUpperCase() === 'TRUE';
66
+ },
67
+ serialize(val: unknown): string {
68
+ return val ? 'TRUE' : 'FALSE';
69
+ },
70
+ };
71
+
72
+ // ── INTEGER (RFC 5545 §3.3.8) ─────────────────────────────────────────────
73
+
74
+ export const INTEGER = {
75
+ parse(raw: string): number {
76
+ return parseInt(raw.trim(), 10);
77
+ },
78
+ serialize(val: unknown): string {
79
+ return String(Math.trunc(Number(val)));
80
+ },
81
+ };
82
+
83
+ // ── FLOAT (RFC 5545 §3.3.7) ───────────────────────────────────────────────
84
+
85
+ export const FLOAT = {
86
+ parse(raw: string): number {
87
+ return parseFloat(raw.trim());
88
+ },
89
+ serialize(val: unknown): string {
90
+ const n = Number(val);
91
+ return Number.isInteger(n) ? `${n}.0` : String(n);
92
+ },
93
+ };
94
+
95
+ // ── URI / CAL-ADDRESS (RFC 5545 §3.3.13, §3.3.3) ─────────────────────────
96
+
97
+ export const URI = {
98
+ parse(raw: string): string {
99
+ return raw.trim();
100
+ },
101
+ serialize(val: unknown): string {
102
+ return String(val ?? '');
103
+ },
104
+ };
105
+
106
+ export const CAL_ADDRESS = URI;
107
+
108
+ // ── BINARY (RFC 5545 §3.3.1) ──────────────────────────────────────────────
109
+
110
+ export const BINARY = {
111
+ parse(raw: string): ICalBinary {
112
+ const data = Buffer.from(raw.trim(), 'base64');
113
+ return { type: 'binary', data };
114
+ },
115
+ serialize(val: unknown): string {
116
+ if (typeof val === 'object' && val !== null && 'type' in val && (val as ICalBinary).type === 'binary') {
117
+ const bin = val as ICalBinary;
118
+ return Buffer.from(bin.data).toString('base64');
119
+ }
120
+ if (val instanceof Uint8Array) return Buffer.from(val).toString('base64');
121
+ return String(val);
122
+ },
123
+ };
124
+
125
+ // ── UTC-OFFSET (RFC 5545 §3.3.14) ────────────────────────────────────────
126
+
127
+ export const UTC_OFFSET = {
128
+ parse(raw: string): ICalUtcOffset {
129
+ const s = raw.trim();
130
+ const sign = s[0] === '-' ? '-' : '+';
131
+ const h = parseInt(s.slice(1, 3), 10);
132
+ const m = parseInt(s.slice(3, 5), 10);
133
+ const sec = s.length >= 7 ? parseInt(s.slice(5, 7), 10) : undefined;
134
+ return { type: 'utc-offset', sign, hours: h, minutes: m, seconds: sec };
135
+ },
136
+ serialize(val: ICalUtcOffset): string {
137
+ const { sign, hours, minutes, seconds } = val;
138
+ const base = `${sign}${pad2(hours)}${pad2(minutes)}`;
139
+ return seconds !== undefined ? base + pad2(seconds) : base;
140
+ },
141
+ };
142
+
143
+ // ── DATE (RFC 5545 §3.3.4) ────────────────────────────────────────────────
144
+
145
+ export const DATE = {
146
+ parse(raw: string): ICalDate {
147
+ const s = raw.trim();
148
+ return {
149
+ type: 'date',
150
+ year: parseInt(s.slice(0, 4), 10),
151
+ month: parseInt(s.slice(4, 6), 10),
152
+ day: parseInt(s.slice(6, 8), 10),
153
+ };
154
+ },
155
+ serialize(val: ICalDate | Date): string {
156
+ if (val instanceof Date) {
157
+ return `${pad4(val.getFullYear())}${pad2(val.getMonth() + 1)}${pad2(val.getDate())}`;
158
+ }
159
+ return `${pad4(val.year)}${pad2(val.month)}${pad2(val.day)}`;
160
+ },
161
+ fromDate(d: Date): ICalDate {
162
+ return {
163
+ type: 'date',
164
+ year: d.getFullYear(),
165
+ month: d.getMonth() + 1,
166
+ day: d.getDate(),
167
+ };
168
+ },
169
+ toDate(val: ICalDate): Date {
170
+ return new Date(val.year, val.month - 1, val.day);
171
+ },
172
+ };
173
+
174
+ // ── DATE-TIME (RFC 5545 §3.3.5) ───────────────────────────────────────────
175
+
176
+ export const DATE_TIME = {
177
+ parse(raw: string, params?: Readonly<Record<string, unknown>>): ICalDateTime {
178
+ const s = raw.trim();
179
+ const utc = s.endsWith('Z');
180
+ const core = utc ? s.slice(0, -1) : s;
181
+ const tzid = typeof params?.['TZID'] === 'string' ? params['TZID'] : undefined;
182
+ return {
183
+ type: 'date-time',
184
+ year: parseInt(core.slice(0, 4), 10),
185
+ month: parseInt(core.slice(4, 6), 10),
186
+ day: parseInt(core.slice(6, 8), 10),
187
+ hour: parseInt(core.slice(9, 11), 10),
188
+ minute: parseInt(core.slice(11, 13), 10),
189
+ second: parseInt(core.slice(13, 15), 10),
190
+ utc,
191
+ tzid,
192
+ };
193
+ },
194
+ serialize(val: ICalDateTime | Date, tzid?: string): string {
195
+ if (val instanceof Date) {
196
+ return (
197
+ `${pad4(val.getUTCFullYear())}${pad2(val.getUTCMonth() + 1)}${pad2(val.getUTCDate())}` +
198
+ `T${pad2(val.getUTCHours())}${pad2(val.getUTCMinutes())}${pad2(val.getUTCSeconds())}Z`
199
+ );
200
+ }
201
+ const core =
202
+ `${pad4(val.year)}${pad2(val.month)}${pad2(val.day)}` +
203
+ `T${pad2(val.hour)}${pad2(val.minute)}${pad2(val.second)}`;
204
+ return val.utc ? core + 'Z' : core;
205
+ },
206
+ fromDate(d: Date, tzid?: string): ICalDateTime {
207
+ if (tzid) {
208
+ return {
209
+ type: 'date-time',
210
+ year: d.getFullYear(),
211
+ month: d.getMonth() + 1,
212
+ day: d.getDate(),
213
+ hour: d.getHours(),
214
+ minute: d.getMinutes(),
215
+ second: d.getSeconds(),
216
+ utc: false,
217
+ tzid,
218
+ };
219
+ }
220
+ return {
221
+ type: 'date-time',
222
+ year: d.getUTCFullYear(),
223
+ month: d.getUTCMonth() + 1,
224
+ day: d.getUTCDate(),
225
+ hour: d.getUTCHours(),
226
+ minute: d.getUTCMinutes(),
227
+ second: d.getUTCSeconds(),
228
+ utc: true,
229
+ };
230
+ },
231
+ toDate(val: ICalDateTime): Date {
232
+ if (val.utc) {
233
+ return new Date(
234
+ Date.UTC(val.year, val.month - 1, val.day, val.hour, val.minute, val.second)
235
+ );
236
+ }
237
+ return new Date(val.year, val.month - 1, val.day, val.hour, val.minute, val.second);
238
+ },
239
+ };
240
+
241
+ // ── TIME (RFC 5545 §3.3.12) ───────────────────────────────────────────────
242
+
243
+ export const TIME = {
244
+ parse(raw: string): ICalTime {
245
+ const s = raw.trim();
246
+ const utc = s.endsWith('Z');
247
+ const core = utc ? s.slice(0, -1) : s;
248
+ return {
249
+ type: 'time',
250
+ hour: parseInt(core.slice(0, 2), 10),
251
+ minute: parseInt(core.slice(2, 4), 10),
252
+ second: parseInt(core.slice(4, 6), 10),
253
+ utc,
254
+ };
255
+ },
256
+ serialize(val: ICalTime): string {
257
+ return `${pad2(val.hour)}${pad2(val.minute)}${pad2(val.second)}${val.utc ? 'Z' : ''}`;
258
+ },
259
+ };
260
+
261
+ // ── DURATION (RFC 5545 §3.3.6) ────────────────────────────────────────────
262
+ //
263
+ // dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
264
+ // dur-date = dur-day [dur-time]
265
+ // dur-time = "T" (dur-hour / dur-minute / dur-second)
266
+ // dur-week = 1*DIGIT "W"
267
+ // dur-hour = 1*DIGIT "H" [dur-minute]
268
+ // dur-minute= 1*DIGIT "M" [dur-second]
269
+ // dur-second= 1*DIGIT "S"
270
+ // dur-day = 1*DIGIT "D"
271
+
272
+ export const DURATION = {
273
+ parse(raw: string): ICalDuration {
274
+ const s = raw.trim();
275
+ const negative = s.startsWith('-');
276
+ const str = s.replace(/^[+-]/, '');
277
+
278
+ // Strip leading 'P'
279
+ if (!str.startsWith('P')) {
280
+ return { type: 'duration', negative, days: 0 };
281
+ }
282
+ const body = str.slice(1);
283
+
284
+ const weekMatch = body.match(/^(\d+)W$/);
285
+ if (weekMatch) {
286
+ return { type: 'duration', negative, weeks: parseInt(weekMatch[1]!, 10) };
287
+ }
288
+
289
+ let days: number | undefined;
290
+ let hours: number | undefined;
291
+ let minutes: number | undefined;
292
+ let seconds: number | undefined;
293
+
294
+ const dayMatch = body.match(/(\d+)D/);
295
+ if (dayMatch) days = parseInt(dayMatch[1]!, 10);
296
+
297
+ const tIdx = body.indexOf('T');
298
+ if (tIdx !== -1) {
299
+ const timePart = body.slice(tIdx + 1);
300
+ const hourMatch = timePart.match(/(\d+)H/);
301
+ const minMatch = timePart.match(/(\d+)M/);
302
+ const secMatch = timePart.match(/(\d+)S/);
303
+ if (hourMatch) hours = parseInt(hourMatch[1]!, 10);
304
+ if (minMatch) minutes = parseInt(minMatch[1]!, 10);
305
+ if (secMatch) seconds = parseInt(secMatch[1]!, 10);
306
+ }
307
+
308
+ return { type: 'duration', negative, days, hours, minutes, seconds };
309
+ },
310
+ serialize(val: ICalDuration): string {
311
+ const sign = val.negative ? '-' : '';
312
+ if (val.weeks !== undefined) return `${sign}P${val.weeks}W`;
313
+
314
+ let s = `${sign}P`;
315
+ if (val.days) s += `${val.days}D`;
316
+
317
+ const hasTime = val.hours || val.minutes || val.seconds;
318
+ if (hasTime) {
319
+ s += 'T';
320
+ if (val.hours) s += `${val.hours}H`;
321
+ if (val.minutes) s += `${val.minutes}M`;
322
+ if (val.seconds) s += `${val.seconds}S`;
323
+ }
324
+
325
+ return s || `${sign}P0D`;
326
+ },
327
+ toSeconds(val: ICalDuration): number {
328
+ const sign = val.negative ? -1 : 1;
329
+ return (
330
+ sign *
331
+ ((val.weeks ?? 0) * 7 * 86400 +
332
+ (val.days ?? 0) * 86400 +
333
+ (val.hours ?? 0) * 3600 +
334
+ (val.minutes ?? 0) * 60 +
335
+ (val.seconds ?? 0))
336
+ );
337
+ },
338
+ };
339
+
340
+ // ── PERIOD (RFC 5545 §3.3.9) ──────────────────────────────────────────────
341
+ //
342
+ // period = date-time "/" date-time (explicit)
343
+ // / date-time "/" dur-value (start + duration)
344
+
345
+ export const PERIOD = {
346
+ parse(raw: string): ICalPeriod {
347
+ const slashIdx = raw.indexOf('/');
348
+ if (slashIdx === -1) throw new Error(`Invalid PERIOD value: ${raw}`);
349
+ const start = DATE_TIME.parse(raw.slice(0, slashIdx));
350
+ const endStr = raw.slice(slashIdx + 1).trim();
351
+ const end = endStr.startsWith('P') || endStr.startsWith('-P') || endStr.startsWith('+P')
352
+ ? DURATION.parse(endStr)
353
+ : DATE_TIME.parse(endStr);
354
+ return { type: 'period', start, end };
355
+ },
356
+ serialize(val: ICalPeriod): string {
357
+ const startStr = DATE_TIME.serialize(val.start);
358
+ const endStr =
359
+ val.end.type === 'duration'
360
+ ? DURATION.serialize(val.end)
361
+ : DATE_TIME.serialize(val.end);
362
+ return `${startStr}/${endStr}`;
363
+ },
364
+ };
365
+
366
+ // ── RECUR (RFC 5545 §3.3.10) ──────────────────────────────────────────────
367
+
368
+ const VALID_FREQS = new Set<string>([
369
+ 'SECONDLY', 'MINUTELY', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY',
370
+ ]);
371
+
372
+ const VALID_WEEKDAYS = new Set<string>(['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']);
373
+
374
+ function parseByDay(raw: string): ByDayRule[] {
375
+ return raw.split(',').map((s) => {
376
+ const m = s.trim().match(/^([+-]?\d+)?(SU|MO|TU|WE|TH|FR|SA)$/i);
377
+ if (!m) return { day: 'MO' as Weekday };
378
+ return {
379
+ day: (m[2]!.toUpperCase()) as Weekday,
380
+ ordwk: m[1] !== undefined ? parseInt(m[1], 10) : undefined,
381
+ };
382
+ });
383
+ }
384
+
385
+ export const RECUR = {
386
+ parse(raw: string): ICalRecur {
387
+ const parts = raw.trim().split(';');
388
+ const map: Record<string, string> = {};
389
+ for (const part of parts) {
390
+ const eq = part.indexOf('=');
391
+ if (eq === -1) continue;
392
+ map[part.slice(0, eq).toUpperCase()] = part.slice(eq + 1);
393
+ }
394
+
395
+ const freqStr = (map['FREQ'] ?? '').toUpperCase();
396
+ if (!VALID_FREQS.has(freqStr)) {
397
+ throw new Error(`Invalid FREQ in RRULE: ${freqStr}`);
398
+ }
399
+ const freq = freqStr as RecurFreq;
400
+
401
+ const parseInts = (k: string) => map[k]?.split(',').map(Number);
402
+ const parseWkst = (k: string): Weekday | undefined => {
403
+ const v = map[k]?.toUpperCase();
404
+ return v && VALID_WEEKDAYS.has(v) ? (v as Weekday) : undefined;
405
+ };
406
+
407
+ let until: ICalRecur['until'];
408
+ if (map['UNTIL']) {
409
+ const u = map['UNTIL']!;
410
+ until = u.includes('T') ? DATE_TIME.parse(u) : DATE.parse(u);
411
+ }
412
+
413
+ return {
414
+ type: 'recur',
415
+ freq,
416
+ until,
417
+ count: map['COUNT'] !== undefined ? parseInt(map['COUNT']!, 10) : undefined,
418
+ interval: map['INTERVAL'] !== undefined ? parseInt(map['INTERVAL']!, 10) : undefined,
419
+ bysecond: parseInts('BYSECOND'),
420
+ byminute: parseInts('BYMINUTE'),
421
+ byhour: parseInts('BYHOUR'),
422
+ byday: map['BYDAY'] ? parseByDay(map['BYDAY']!) : undefined,
423
+ bymonthday: parseInts('BYMONTHDAY'),
424
+ byyearday: parseInts('BYYEARDAY'),
425
+ byweekno: parseInts('BYWEEKNO'),
426
+ bymonth: parseInts('BYMONTH'),
427
+ bysetpos: parseInts('BYSETPOS'),
428
+ wkst: parseWkst('WKST'),
429
+ };
430
+ },
431
+ serialize(val: ICalRecur): string {
432
+ const parts: string[] = [`FREQ=${val.freq}`];
433
+ if (val.until) {
434
+ const u = val.until;
435
+ parts.push(`UNTIL=${u.type === 'date' ? DATE.serialize(u) : DATE_TIME.serialize(u)}`);
436
+ }
437
+ if (val.count !== undefined) parts.push(`COUNT=${val.count}`);
438
+ if (val.interval !== undefined) parts.push(`INTERVAL=${val.interval}`);
439
+ if (val.bysecond?.length) parts.push(`BYSECOND=${val.bysecond.join(',')}`);
440
+ if (val.byminute?.length) parts.push(`BYMINUTE=${val.byminute.join(',')}`);
441
+ if (val.byhour?.length) parts.push(`BYHOUR=${val.byhour.join(',')}`);
442
+ if (val.byday?.length) {
443
+ parts.push(
444
+ `BYDAY=${val.byday
445
+ .map((d) => (d.ordwk !== undefined ? `${d.ordwk}${d.day}` : d.day))
446
+ .join(',')}`,
447
+ );
448
+ }
449
+ if (val.bymonthday?.length) parts.push(`BYMONTHDAY=${val.bymonthday.join(',')}`);
450
+ if (val.byyearday?.length) parts.push(`BYYEARDAY=${val.byyearday.join(',')}`);
451
+ if (val.byweekno?.length) parts.push(`BYWEEKNO=${val.byweekno.join(',')}`);
452
+ if (val.bymonth?.length) parts.push(`BYMONTH=${val.bymonth.join(',')}`);
453
+ if (val.bysetpos?.length) parts.push(`BYSETPOS=${val.bysetpos.join(',')}`);
454
+ if (val.wkst) parts.push(`WKST=${val.wkst}`);
455
+ return parts.join(';');
456
+ },
457
+ };
458
+
459
+ // ── GEO — special compound property value (RFC 5545 §3.8.1.6) ────────────
460
+ // Value format: float ";" float
461
+
462
+ export const GEO = {
463
+ parse(raw: string): ICalGeo {
464
+ const [lat, lon] = raw.split(';');
465
+ return {
466
+ type: 'geo',
467
+ latitude: parseFloat(lat ?? '0'),
468
+ longitude: parseFloat(lon ?? '0'),
469
+ };
470
+ },
471
+ serialize(val: ICalGeo): string {
472
+ return `${val.latitude};${val.longitude}`;
473
+ },
474
+ };
475
+
476
+ // ── Type-name → codec lookup ──────────────────────────────────────────────
477
+
478
+ type Codec = { parse(raw: string, params?: Readonly<Record<string, unknown>>): unknown; serialize(val: unknown): string };
479
+
480
+ export const CODECS: Readonly<Record<string, Codec>> = {
481
+ 'TEXT': TEXT,
482
+ 'BOOLEAN': BOOLEAN,
483
+ 'INTEGER': INTEGER,
484
+ 'FLOAT': FLOAT,
485
+ 'URI': URI,
486
+ 'CAL-ADDRESS': CAL_ADDRESS,
487
+ 'BINARY': BINARY,
488
+ 'UTC-OFFSET': UTC_OFFSET,
489
+ 'DATE': DATE,
490
+ 'DATE-TIME': DATE_TIME,
491
+ 'TIME': TIME,
492
+ 'DURATION': DURATION,
493
+ 'PERIOD': PERIOD,
494
+ 'RECUR': RECUR,
495
+ };
496
+
497
+ // Re-export isValidDate for use in serializer validation
498
+ export { isValidDate };