@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
package/src/event.ts ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * VEVENT component (RFC 5545 §3.6.1)
3
+ *
4
+ * UID and DTSTAMP are required.
5
+ * DTSTART is required when METHOD is not present in the calendar.
6
+ */
7
+
8
+ import { Component } from './component.js';
9
+ import { Property, parseProperty } from './property.js';
10
+ import { Alarm } from './alarm.js';
11
+ import type {
12
+ ICalDate,
13
+ ICalDateTime,
14
+ ICalDuration,
15
+ ICalRecur,
16
+ ICalGeo,
17
+ ICalValue,
18
+ } from './types.js';
19
+
20
+ export class Event extends Component {
21
+ constructor() {
22
+ super('VEVENT');
23
+ }
24
+
25
+ // ── Required / very common properties ───────────────────────────────
26
+
27
+ get uid(): string | null {
28
+ return this.getProperty('UID')?.text ?? null;
29
+ }
30
+ set uid(v: string) {
31
+ this.setProperty('UID', v);
32
+ }
33
+
34
+ get dtstamp(): ICalDateTime | null {
35
+ const v = this.getValue('DTSTAMP');
36
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
37
+ }
38
+ set dtstamp(v: ICalDateTime | Date) {
39
+ if (v instanceof Date) {
40
+ this.setProperty('DTSTAMP', {
41
+ type: 'date-time',
42
+ year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(),
43
+ hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(),
44
+ utc: true,
45
+ } satisfies ICalDateTime);
46
+ } else {
47
+ this.setProperty('DTSTAMP', v);
48
+ }
49
+ }
50
+
51
+ get dtstart(): ICalDateTime | ICalDate | null {
52
+ const v = this.getValue('DTSTART');
53
+ if (!v || typeof v !== 'object') return null;
54
+ return v.type === 'date-time' || v.type === 'date' ? (v as ICalDateTime | ICalDate) : null;
55
+ }
56
+ set dtstart(v: ICalDateTime | ICalDate | Date) {
57
+ if (v instanceof Date) {
58
+ this.setProperty('DTSTART', {
59
+ type: 'date-time',
60
+ year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(),
61
+ hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(),
62
+ utc: true,
63
+ } satisfies ICalDateTime);
64
+ } else if (v.type === 'date') {
65
+ this.setProperty('DTSTART', v, { VALUE: 'DATE' });
66
+ } else {
67
+ this.setProperty('DTSTART', v);
68
+ }
69
+ }
70
+
71
+ get dtend(): ICalDateTime | ICalDate | null {
72
+ const v = this.getValue('DTEND');
73
+ if (!v || typeof v !== 'object') return null;
74
+ return v.type === 'date-time' || v.type === 'date' ? (v as ICalDateTime | ICalDate) : null;
75
+ }
76
+ set dtend(v: ICalDateTime | ICalDate | Date) {
77
+ if (v instanceof Date) {
78
+ this.setProperty('DTEND', {
79
+ type: 'date-time',
80
+ year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(),
81
+ hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(),
82
+ utc: true,
83
+ } satisfies ICalDateTime);
84
+ } else if (v.type === 'date') {
85
+ this.setProperty('DTEND', v, { VALUE: 'DATE' });
86
+ } else {
87
+ this.setProperty('DTEND', v);
88
+ }
89
+ }
90
+
91
+ get duration(): ICalDuration | null {
92
+ const v = this.getValue('DURATION');
93
+ return v && typeof v === 'object' && v.type === 'duration' ? (v as ICalDuration) : null;
94
+ }
95
+ set duration(v: ICalDuration) {
96
+ this.setProperty('DURATION', v);
97
+ }
98
+
99
+ get summary(): string | null {
100
+ return this.getProperty('SUMMARY')?.text ?? null;
101
+ }
102
+ set summary(v: string) {
103
+ this.setProperty('SUMMARY', v);
104
+ }
105
+
106
+ get description(): string | null {
107
+ return this.getProperty('DESCRIPTION')?.text ?? null;
108
+ }
109
+ set description(v: string) {
110
+ this.setProperty('DESCRIPTION', v);
111
+ }
112
+
113
+ get location(): string | null {
114
+ return this.getProperty('LOCATION')?.text ?? null;
115
+ }
116
+ set location(v: string) {
117
+ this.setProperty('LOCATION', v);
118
+ }
119
+
120
+ get url(): string | null {
121
+ return this.getProperty('URL')?.text ?? null;
122
+ }
123
+ set url(v: string) {
124
+ this.setProperty('URL', v);
125
+ }
126
+
127
+ get status(): string | null {
128
+ return this.getProperty('STATUS')?.text ?? null;
129
+ }
130
+ set status(v: 'TENTATIVE' | 'CONFIRMED' | 'CANCELLED' | string) {
131
+ this.setProperty('STATUS', v);
132
+ }
133
+
134
+ get transp(): 'OPAQUE' | 'TRANSPARENT' | string | null {
135
+ return this.getProperty('TRANSP')?.text ?? null;
136
+ }
137
+ set transp(v: 'OPAQUE' | 'TRANSPARENT' | string) {
138
+ this.setProperty('TRANSP', v);
139
+ }
140
+
141
+ get klass(): string | null {
142
+ return this.getProperty('CLASS')?.text ?? null;
143
+ }
144
+ set klass(v: 'PUBLIC' | 'PRIVATE' | 'CONFIDENTIAL' | string) {
145
+ this.setProperty('CLASS', v);
146
+ }
147
+
148
+ get priority(): number | null {
149
+ return this.getProperty('PRIORITY')?.number ?? null;
150
+ }
151
+ set priority(v: number) {
152
+ this.setProperty('PRIORITY', Math.max(0, Math.min(9, Math.trunc(v))));
153
+ }
154
+
155
+ get sequence(): number | null {
156
+ return this.getProperty('SEQUENCE')?.number ?? null;
157
+ }
158
+ set sequence(v: number) {
159
+ this.setProperty('SEQUENCE', Math.max(0, Math.trunc(v)));
160
+ }
161
+
162
+ get created(): ICalDateTime | null {
163
+ const v = this.getValue('CREATED');
164
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
165
+ }
166
+ set created(v: ICalDateTime | Date) {
167
+ this.setProperty('CREATED', v instanceof Date
168
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
169
+ : v);
170
+ }
171
+
172
+ get lastModified(): ICalDateTime | null {
173
+ const v = this.getValue('LAST-MODIFIED');
174
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
175
+ }
176
+ set lastModified(v: ICalDateTime | Date) {
177
+ this.setProperty('LAST-MODIFIED', v instanceof Date
178
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
179
+ : v);
180
+ }
181
+
182
+ get recurrenceId(): ICalDateTime | ICalDate | null {
183
+ const v = this.getValue('RECURRENCE-ID');
184
+ if (!v || typeof v !== 'object') return null;
185
+ return v.type === 'date-time' || v.type === 'date' ? (v as ICalDateTime | ICalDate) : null;
186
+ }
187
+ set recurrenceId(v: ICalDateTime | ICalDate) {
188
+ const params: Record<string, string> = v.type === 'date' ? { VALUE: 'DATE' } : {};
189
+ this.setProperty('RECURRENCE-ID', v, params);
190
+ }
191
+
192
+ get geo(): ICalGeo | null {
193
+ const v = this.getValue('GEO');
194
+ return v && typeof v === 'object' && v.type === 'geo' ? (v as ICalGeo) : null;
195
+ }
196
+ set geo(v: ICalGeo) {
197
+ this.setProperty('GEO', v);
198
+ }
199
+
200
+ get organizer(): string | null {
201
+ return this.getProperty('ORGANIZER')?.text ?? null;
202
+ }
203
+ set organizer(v: string) {
204
+ this.setProperty('ORGANIZER', v);
205
+ }
206
+
207
+ // ── Multi-occurrence properties ──────────────────────────────────────
208
+
209
+ get attendees(): Property[] {
210
+ return this.getProperties('ATTENDEE');
211
+ }
212
+
213
+ addAttendee(calAddress: string, params: Record<string, string> = {}): this {
214
+ this.appendProperty('ATTENDEE', calAddress, params);
215
+ return this;
216
+ }
217
+
218
+ get categories(): ICalValue[] {
219
+ return this.getValues('CATEGORIES');
220
+ }
221
+
222
+ set categories(v: string[]) {
223
+ this.setProperty('CATEGORIES', v);
224
+ }
225
+
226
+ get comments(): Property[] {
227
+ return this.getProperties('COMMENT');
228
+ }
229
+
230
+ addComment(v: string): this {
231
+ this.appendProperty('COMMENT', v);
232
+ return this;
233
+ }
234
+
235
+ get contacts(): Property[] {
236
+ return this.getProperties('CONTACT');
237
+ }
238
+
239
+ addContact(v: string): this {
240
+ this.appendProperty('CONTACT', v);
241
+ return this;
242
+ }
243
+
244
+ get exdates(): ICalValue[] {
245
+ return this.getProperties('EXDATE').flatMap((p) => p.list);
246
+ }
247
+
248
+ addExdate(v: ICalDateTime | ICalDate): this {
249
+ const params: Record<string, string> = v.type === 'date' ? { VALUE: 'DATE' } : {};
250
+ this.appendProperty('EXDATE', v, params);
251
+ return this;
252
+ }
253
+
254
+ get rdates(): ICalValue[] {
255
+ return this.getProperties('RDATE').flatMap((p) => p.list);
256
+ }
257
+
258
+ addRdate(v: ICalDateTime | ICalDate): this {
259
+ const params: Record<string, string> = v.type === 'date' ? { VALUE: 'DATE' } : {};
260
+ this.appendProperty('RDATE', v, params);
261
+ return this;
262
+ }
263
+
264
+ get rrules(): ICalRecur[] {
265
+ return this.getProperties('RRULE')
266
+ .map((p) => p.scalar)
267
+ .filter((v): v is ICalRecur => typeof v === 'object' && v !== null && v.type === 'recur');
268
+ }
269
+
270
+ addRrule(v: ICalRecur): this {
271
+ this.appendProperty('RRULE', v);
272
+ return this;
273
+ }
274
+
275
+ get alarms(): Alarm[] {
276
+ return this.getComponents('VALARM') as Alarm[];
277
+ }
278
+
279
+ addAlarm(alarm: Alarm): this {
280
+ this.addComponent(alarm);
281
+ return this;
282
+ }
283
+
284
+ // ── Strict validation ────────────────────────────────────────────────
285
+
286
+ override toString(): string {
287
+ if (!this.uid) throw new Error('VEVENT: UID is required');
288
+ if (!this.getProperty('DTSTAMP')) throw new Error('VEVENT: DTSTAMP is required');
289
+ return super.toString();
290
+ }
291
+
292
+ // ── Factory ──────────────────────────────────────────────────────────
293
+
294
+ static fromRaw(
295
+ props: ReadonlyArray<{ name: string; params: Record<string, string>; value: string }>,
296
+ subcomponents: Component[] = [],
297
+ ): Event {
298
+ const event = new Event();
299
+ for (const { name, params, value } of props) {
300
+ event.addProperty(parseProperty(name, value, params));
301
+ }
302
+ for (const sub of subcomponents) {
303
+ event.addComponent(sub);
304
+ }
305
+ return event;
306
+ }
307
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * VFREEBUSY component (RFC 5545 §3.6.4)
3
+ *
4
+ * UID and DTSTAMP are required.
5
+ * FREEBUSY properties list free/busy time periods.
6
+ */
7
+
8
+ import { Component } from './component.js';
9
+ import { Property, parseProperty } from './property.js';
10
+ import type { ICalDateTime, ICalPeriod } from './types.js';
11
+
12
+ export class FreeBusy extends Component {
13
+ constructor() {
14
+ super('VFREEBUSY');
15
+ }
16
+
17
+ // ── Required ─────────────────────────────────────────────────────────
18
+
19
+ get uid(): string | null {
20
+ return this.getProperty('UID')?.text ?? null;
21
+ }
22
+ set uid(v: string) {
23
+ this.setProperty('UID', v);
24
+ }
25
+
26
+ get dtstamp(): ICalDateTime | null {
27
+ const v = this.getValue('DTSTAMP');
28
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
29
+ }
30
+ set dtstamp(v: ICalDateTime | Date) {
31
+ this.setProperty('DTSTAMP', v instanceof Date
32
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
33
+ : v);
34
+ }
35
+
36
+ // ── Date/time span ───────────────────────────────────────────────────
37
+
38
+ get dtstart(): ICalDateTime | null {
39
+ const v = this.getValue('DTSTART');
40
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
41
+ }
42
+ set dtstart(v: ICalDateTime | Date) {
43
+ this.setProperty('DTSTART', v instanceof Date
44
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
45
+ : v);
46
+ }
47
+
48
+ get dtend(): ICalDateTime | null {
49
+ const v = this.getValue('DTEND');
50
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
51
+ }
52
+ set dtend(v: ICalDateTime | Date) {
53
+ this.setProperty('DTEND', v instanceof Date
54
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
55
+ : v);
56
+ }
57
+
58
+ // ── Participants ──────────────────────────────────────────────────────
59
+
60
+ get organizer(): string | null {
61
+ return this.getProperty('ORGANIZER')?.text ?? null;
62
+ }
63
+ set organizer(v: string) {
64
+ this.setProperty('ORGANIZER', v);
65
+ }
66
+
67
+ get attendees(): Property[] {
68
+ return this.getProperties('ATTENDEE');
69
+ }
70
+
71
+ addAttendee(calAddress: string, params: Record<string, string> = {}): this {
72
+ this.appendProperty('ATTENDEE', calAddress, params);
73
+ return this;
74
+ }
75
+
76
+ get url(): string | null {
77
+ return this.getProperty('URL')?.text ?? null;
78
+ }
79
+ set url(v: string) {
80
+ this.setProperty('URL', v);
81
+ }
82
+
83
+ get comment(): string | null {
84
+ return this.getProperty('COMMENT')?.text ?? null;
85
+ }
86
+ set comment(v: string) {
87
+ this.setProperty('COMMENT', v);
88
+ }
89
+
90
+ // ── FREEBUSY periods ─────────────────────────────────────────────────
91
+
92
+ /** All FREEBUSY properties (may have multiple, each with FBTYPE parameter). */
93
+ get freebusyProperties(): Property[] {
94
+ return this.getProperties('FREEBUSY');
95
+ }
96
+
97
+ /** Flat list of all PERIOD values across all FREEBUSY properties. */
98
+ get periods(): ICalPeriod[] {
99
+ return this.getProperties('FREEBUSY')
100
+ .flatMap((p) => p.list)
101
+ .filter((v): v is ICalPeriod => typeof v === 'object' && v !== null && v.type === 'period');
102
+ }
103
+
104
+ /**
105
+ * Add a FREEBUSY property containing one or more periods.
106
+ * @param periods - Array of periods for this FREEBUSY property.
107
+ * @param fbtype - FBTYPE parameter value (FREE, BUSY, BUSY-UNAVAILABLE, BUSY-TENTATIVE).
108
+ */
109
+ addFreebusy(periods: ICalPeriod[], fbtype = 'BUSY'): this {
110
+ this.appendProperty('FREEBUSY', periods, { FBTYPE: fbtype });
111
+ return this;
112
+ }
113
+
114
+ // ── Validation ────────────────────────────────────────────────────────
115
+
116
+ override toString(): string {
117
+ if (!this.uid) throw new Error('VFREEBUSY: UID is required');
118
+ if (!this.getProperty('DTSTAMP')) throw new Error('VFREEBUSY: DTSTAMP is required');
119
+ return super.toString();
120
+ }
121
+
122
+ // ── Factory ──────────────────────────────────────────────────────────
123
+
124
+ static fromRaw(
125
+ props: ReadonlyArray<{ name: string; params: Record<string, string>; value: string }>,
126
+ ): FreeBusy {
127
+ const fb = new FreeBusy();
128
+ for (const { name, params, value } of props) {
129
+ fb.addProperty(parseProperty(name, value, params));
130
+ }
131
+ return fb;
132
+ }
133
+ }
package/src/index.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @pipobscure/ical — Full iCalendar (RFC 5545) implementation.
3
+ *
4
+ * Tolerant parsing, strict generation.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { parse, Calendar, Event } from '@pipobscure/ical';
9
+ *
10
+ * // Parse
11
+ * const cal = parse(icsString);
12
+ * for (const event of cal.events) {
13
+ * console.log(event.summary, event.dtstart);
14
+ * }
15
+ *
16
+ * // Build & serialize
17
+ * const cal = Calendar.create('-//My App//EN');
18
+ * const event = new Event();
19
+ * event.uid = crypto.randomUUID();
20
+ * event.dtstamp = new Date();
21
+ * event.dtstart = new Date('2025-01-01T10:00:00Z');
22
+ * event.dtend = new Date('2025-01-01T11:00:00Z');
23
+ * event.summary = 'New Year kickoff';
24
+ * cal.addEvent(event);
25
+ * console.log(cal.toString());
26
+ * ```
27
+ */
28
+
29
+ // ── Parsing ───────────────────────────────────────────────────────────────
30
+ export { parse, parseAll } from './parse.js';
31
+
32
+ // ── Components ────────────────────────────────────────────────────────────
33
+ export { Component } from './component.js';
34
+ export { Calendar } from './calendar.js';
35
+ export { Event } from './event.js';
36
+ export { Todo } from './todo.js';
37
+ export { Journal } from './journal.js';
38
+ export { FreeBusy } from './freebusy.js';
39
+ export { Timezone, Standard, Daylight, TimezoneRule } from './timezone.js';
40
+ export { Alarm } from './alarm.js';
41
+
42
+ // ── Property & value primitives ───────────────────────────────────────────
43
+ export { Property, parseProperty } from './property.js';
44
+
45
+ // ── Value type codecs (for advanced use) ─────────────────────────────────
46
+ export {
47
+ TEXT,
48
+ BOOLEAN,
49
+ INTEGER,
50
+ FLOAT,
51
+ URI,
52
+ CAL_ADDRESS,
53
+ BINARY,
54
+ UTC_OFFSET,
55
+ DATE,
56
+ DATE_TIME,
57
+ TIME,
58
+ DURATION,
59
+ PERIOD,
60
+ RECUR,
61
+ GEO,
62
+ CODECS,
63
+ } from './value-types.js';
64
+
65
+ // ── Types ─────────────────────────────────────────────────────────────────
66
+ export type {
67
+ ICalDate,
68
+ ICalDateTime,
69
+ ICalTime,
70
+ ICalDuration,
71
+ ICalPeriod,
72
+ ICalRecur,
73
+ ICalUtcOffset,
74
+ ICalGeo,
75
+ ICalBinary,
76
+ ICalStructured,
77
+ ICalValue,
78
+ RecurFreq,
79
+ Weekday,
80
+ ByDayRule,
81
+ ContentLine,
82
+ ParsedProperty,
83
+ } from './types.js';
84
+
85
+ export type { ValueTypeName, PropertyDef } from './property-registry.js';
package/src/journal.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * VJOURNAL component (RFC 5545 §3.6.3)
3
+ *
4
+ * UID and DTSTAMP are required.
5
+ */
6
+
7
+ import { Component } from './component.js';
8
+ import { Property, parseProperty } from './property.js';
9
+ import type { ICalDate, ICalDateTime, ICalRecur, ICalValue } from './types.js';
10
+
11
+ export class Journal extends Component {
12
+ constructor() {
13
+ super('VJOURNAL');
14
+ }
15
+
16
+ // ── Required ─────────────────────────────────────────────────────────
17
+
18
+ get uid(): string | null {
19
+ return this.getProperty('UID')?.text ?? null;
20
+ }
21
+ set uid(v: string) {
22
+ this.setProperty('UID', v);
23
+ }
24
+
25
+ get dtstamp(): ICalDateTime | null {
26
+ const v = this.getValue('DTSTAMP');
27
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
28
+ }
29
+ set dtstamp(v: ICalDateTime | Date) {
30
+ this.setProperty('DTSTAMP', v instanceof Date
31
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
32
+ : v);
33
+ }
34
+
35
+ // ── Date/time ─────────────────────────────────────────────────────────
36
+
37
+ get dtstart(): ICalDateTime | ICalDate | null {
38
+ const v = this.getValue('DTSTART');
39
+ if (!v || typeof v !== 'object') return null;
40
+ return v.type === 'date-time' || v.type === 'date' ? (v as ICalDateTime | ICalDate) : null;
41
+ }
42
+ set dtstart(v: ICalDateTime | ICalDate | Date) {
43
+ if (v instanceof Date) {
44
+ this.setProperty('DTSTART', { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime);
45
+ } else if (v.type === 'date') {
46
+ this.setProperty('DTSTART', v, { VALUE: 'DATE' });
47
+ } else {
48
+ this.setProperty('DTSTART', v);
49
+ }
50
+ }
51
+
52
+ // ── Descriptive ──────────────────────────────────────────────────────
53
+
54
+ get summary(): string | null {
55
+ return this.getProperty('SUMMARY')?.text ?? null;
56
+ }
57
+ set summary(v: string) {
58
+ this.setProperty('SUMMARY', v);
59
+ }
60
+
61
+ get descriptions(): Property[] {
62
+ return this.getProperties('DESCRIPTION');
63
+ }
64
+
65
+ addDescription(v: string): this {
66
+ this.appendProperty('DESCRIPTION', v);
67
+ return this;
68
+ }
69
+
70
+ get status(): 'DRAFT' | 'FINAL' | 'CANCELLED' | string | null {
71
+ return this.getProperty('STATUS')?.text ?? null;
72
+ }
73
+ set status(v: string) {
74
+ this.setProperty('STATUS', v);
75
+ }
76
+
77
+ get klass(): string | null {
78
+ return this.getProperty('CLASS')?.text ?? null;
79
+ }
80
+ set klass(v: string) {
81
+ this.setProperty('CLASS', v);
82
+ }
83
+
84
+ get url(): string | null {
85
+ return this.getProperty('URL')?.text ?? null;
86
+ }
87
+ set url(v: string) {
88
+ this.setProperty('URL', v);
89
+ }
90
+
91
+ get organizer(): string | null {
92
+ return this.getProperty('ORGANIZER')?.text ?? null;
93
+ }
94
+ set organizer(v: string) {
95
+ this.setProperty('ORGANIZER', v);
96
+ }
97
+
98
+ get sequence(): number | null {
99
+ return this.getProperty('SEQUENCE')?.number ?? null;
100
+ }
101
+ set sequence(v: number) {
102
+ this.setProperty('SEQUENCE', Math.max(0, Math.trunc(v)));
103
+ }
104
+
105
+ get created(): ICalDateTime | null {
106
+ const v = this.getValue('CREATED');
107
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
108
+ }
109
+ set created(v: ICalDateTime | Date) {
110
+ this.setProperty('CREATED', v instanceof Date
111
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
112
+ : v);
113
+ }
114
+
115
+ get lastModified(): ICalDateTime | null {
116
+ const v = this.getValue('LAST-MODIFIED');
117
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
118
+ }
119
+ set lastModified(v: ICalDateTime | Date) {
120
+ this.setProperty('LAST-MODIFIED', v instanceof Date
121
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
122
+ : v);
123
+ }
124
+
125
+ // ── Multi-occurrence ──────────────────────────────────────────────────
126
+
127
+ get attendees(): Property[] {
128
+ return this.getProperties('ATTENDEE');
129
+ }
130
+
131
+ addAttendee(calAddress: string, params: Record<string, string> = {}): this {
132
+ this.appendProperty('ATTENDEE', calAddress, params);
133
+ return this;
134
+ }
135
+
136
+ get categories(): ICalValue[] {
137
+ return this.getValues('CATEGORIES');
138
+ }
139
+
140
+ set categories(v: string[]) {
141
+ this.setProperty('CATEGORIES', v);
142
+ }
143
+
144
+ get rrules(): ICalRecur[] {
145
+ return this.getProperties('RRULE')
146
+ .map((p) => p.scalar)
147
+ .filter((v): v is ICalRecur => typeof v === 'object' && v !== null && v.type === 'recur');
148
+ }
149
+
150
+ addRrule(v: ICalRecur): this {
151
+ this.appendProperty('RRULE', v);
152
+ return this;
153
+ }
154
+
155
+ // ── Validation ────────────────────────────────────────────────────────
156
+
157
+ override toString(): string {
158
+ if (!this.uid) throw new Error('VJOURNAL: UID is required');
159
+ if (!this.getProperty('DTSTAMP')) throw new Error('VJOURNAL: DTSTAMP is required');
160
+ return super.toString();
161
+ }
162
+
163
+ // ── Factory ──────────────────────────────────────────────────────────
164
+
165
+ static fromRaw(
166
+ props: ReadonlyArray<{ name: string; params: Record<string, string>; value: string }>,
167
+ ): Journal {
168
+ const journal = new Journal();
169
+ for (const { name, params, value } of props) {
170
+ journal.addProperty(parseProperty(name, value, params));
171
+ }
172
+ return journal;
173
+ }
174
+ }