ical-generator 3.5.1 → 3.5.2-develop.2

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.
package/src/event.ts ADDED
@@ -0,0 +1,1669 @@
1
+ 'use strict';
2
+
3
+ import uuid from 'uuid-random';
4
+ import {
5
+ addOrGetCustomAttributes,
6
+ checkDate,
7
+ checkEnum,
8
+ checkNameAndMail,
9
+ escape,
10
+ formatDate,
11
+ formatDateTZ,
12
+ generateCustomAttributes,
13
+ isRRule,
14
+ toDate,
15
+ toJSON
16
+ } from './tools';
17
+ import ICalAttendee, {ICalAttendeeData} from './attendee';
18
+ import ICalAlarm, {ICalAlarmData} from './alarm';
19
+ import ICalCategory, {ICalCategoryData} from './category';
20
+ import ICalCalendar from './calendar';
21
+ import {
22
+ ICalDateTimeValue,
23
+ ICalDescription,
24
+ ICalEventRepeatingFreq,
25
+ ICalLocation,
26
+ ICalOrganizer,
27
+ ICalRepeatingOptions,
28
+ ICalRRuleStub,
29
+ ICalWeekday
30
+ } from './types';
31
+
32
+
33
+ export enum ICalEventStatus {
34
+ CONFIRMED = 'CONFIRMED',
35
+ TENTATIVE = 'TENTATIVE',
36
+ CANCELLED = 'CANCELLED'
37
+ }
38
+
39
+ export enum ICalEventBusyStatus {
40
+ FREE = 'FREE',
41
+ TENTATIVE = 'TENTATIVE',
42
+ BUSY = 'BUSY',
43
+ OOF = 'OOF'
44
+ }
45
+
46
+ export enum ICalEventTransparency {
47
+ TRANSPARENT = 'TRANSPARENT',
48
+ OPAQUE = 'OPAQUE'
49
+ }
50
+
51
+ export enum ICalEventClass {
52
+ PUBLIC = 'PUBLIC',
53
+ PRIVATE = 'PRIVATE',
54
+ CONFIDENTIAL = 'CONFIDENTIAL'
55
+ }
56
+
57
+ export interface ICalEventData {
58
+ id?: string | number | null,
59
+ sequence?: number,
60
+ start?: ICalDateTimeValue | null,
61
+ end?: ICalDateTimeValue | null,
62
+ recurrenceId?: ICalDateTimeValue | null,
63
+ timezone?: string | null,
64
+ stamp?: ICalDateTimeValue,
65
+ allDay?: boolean,
66
+ floating?: boolean,
67
+ repeating?: ICalRepeatingOptions | ICalRRuleStub | string | null,
68
+ summary?: string,
69
+ location?: ICalLocation | string | null,
70
+ description?: ICalDescription | string | null,
71
+ organizer?: ICalOrganizer | string | null,
72
+ attendees?: ICalAttendee[] | ICalAttendeeData[],
73
+ alarms?: ICalAlarm[] | ICalAlarmData[],
74
+ categories?: ICalCategory[] | ICalCategoryData[],
75
+ status?: ICalEventStatus | null,
76
+ busystatus?: ICalEventBusyStatus | null,
77
+ priority?: number | null,
78
+ url?: string | null,
79
+ attachments?: string[],
80
+ transparency?: ICalEventTransparency | null,
81
+ created?: ICalDateTimeValue | null,
82
+ lastModified?: ICalDateTimeValue | null,
83
+ class?: ICalEventClass | null;
84
+ x?: {key: string, value: string}[] | [string, string][] | Record<string, string>;
85
+ }
86
+
87
+ interface ICalEventInternalData {
88
+ id: string,
89
+ sequence: number,
90
+ start: ICalDateTimeValue | null,
91
+ end: ICalDateTimeValue | null,
92
+ recurrenceId: ICalDateTimeValue | null,
93
+ timezone: string | null,
94
+ stamp: ICalDateTimeValue,
95
+ allDay: boolean,
96
+ floating: boolean,
97
+ repeating: ICalEventInternalRepeatingData | ICalRRuleStub | string | null,
98
+ summary: string,
99
+ location: ICalLocation | null,
100
+ description: ICalDescription | null,
101
+ organizer: ICalOrganizer | null,
102
+ attendees: ICalAttendee[],
103
+ alarms: ICalAlarm[],
104
+ categories: ICalCategory[],
105
+ status: ICalEventStatus | null,
106
+ busystatus: ICalEventBusyStatus | null,
107
+ priority: number | null,
108
+ url: string | null,
109
+ attachments: string[],
110
+ transparency: ICalEventTransparency | null,
111
+ created: ICalDateTimeValue | null,
112
+ lastModified: ICalDateTimeValue | null,
113
+ class: ICalEventClass | null,
114
+ x: [string, string][];
115
+ }
116
+
117
+ export interface ICalEventJSONData {
118
+ id: string,
119
+ sequence: number,
120
+ start: string | null,
121
+ end: string | null,
122
+ recurrenceId: string | null,
123
+ timezone: string | null,
124
+ stamp: string,
125
+ allDay: boolean,
126
+ floating: boolean,
127
+ repeating: ICalEventInternalRepeatingData | string | null,
128
+ summary: string,
129
+ location: ICalLocation | null,
130
+ description: ICalDescription | null,
131
+ organizer: ICalOrganizer | null,
132
+ attendees: ICalAttendee[],
133
+ alarms: ICalAlarm[],
134
+ categories: ICalCategory[],
135
+ status: ICalEventStatus | null,
136
+ busystatus: ICalEventBusyStatus | null,
137
+ priority?: number | null,
138
+ url: string | null,
139
+ attachments: string[],
140
+ transparency: ICalEventTransparency | null,
141
+ created: string | null,
142
+ lastModified: string | null,
143
+ x: {key: string, value: string}[];
144
+ }
145
+
146
+ interface ICalEventInternalRepeatingData {
147
+ freq: ICalEventRepeatingFreq;
148
+ count?: number;
149
+ interval?: number;
150
+ until?: ICalDateTimeValue;
151
+ byDay?: ICalWeekday[];
152
+ byMonth?: number[];
153
+ byMonthDay?: number[];
154
+ bySetPos?: number;
155
+ exclude?: ICalDateTimeValue[];
156
+ startOfWeek?: ICalWeekday;
157
+ }
158
+
159
+
160
+ /**
161
+ * Usually you get an `ICalCalendar` object like this:
162
+ * ```javascript
163
+ * import ical from 'ical-generator';
164
+ * const calendar = ical();
165
+ * const event = calendar.createEvent();
166
+ * ```
167
+ */
168
+ export default class ICalEvent {
169
+ private readonly data: ICalEventInternalData;
170
+ private readonly calendar: ICalCalendar;
171
+
172
+ /**
173
+ * Constructor of [[`ICalEvent`]. The calendar reference is
174
+ * required to query the calendar's timezone when required.
175
+ *
176
+ * @param data Calendar Event Data
177
+ * @param calendar Reference to ICalCalendar object
178
+ */
179
+ constructor(data: ICalEventData, calendar: ICalCalendar) {
180
+ this.data = {
181
+ id: uuid(),
182
+ sequence: 0,
183
+ start: null,
184
+ end: null,
185
+ recurrenceId: null,
186
+ timezone: null,
187
+ stamp: new Date(),
188
+ allDay: false,
189
+ floating: false,
190
+ repeating: null,
191
+ summary: '',
192
+ location: null,
193
+ description: null,
194
+ organizer: null,
195
+ attendees: [],
196
+ alarms: [],
197
+ categories: [],
198
+ status: null,
199
+ busystatus: null,
200
+ priority: null,
201
+ url: null,
202
+ attachments: [],
203
+ transparency: null,
204
+ created: null,
205
+ lastModified: null,
206
+ class: null,
207
+ x: []
208
+ };
209
+
210
+ this.calendar = calendar;
211
+ if (!calendar) {
212
+ throw new Error('`calendar` option required!');
213
+ }
214
+
215
+ data.id && this.id(data.id);
216
+ data.sequence !== undefined && this.sequence(data.sequence);
217
+ data.start && this.start(data.start);
218
+ data.end !== undefined && this.end(data.end);
219
+ data.recurrenceId !== undefined && this.recurrenceId(data.recurrenceId);
220
+ data.timezone !== undefined && this.timezone(data.timezone);
221
+ data.stamp !== undefined && this.stamp(data.stamp);
222
+ data.allDay !== undefined && this.allDay(data.allDay);
223
+ data.floating !== undefined && this.floating(data.floating);
224
+ data.repeating !== undefined && this.repeating(data.repeating);
225
+ data.summary !== undefined && this.summary(data.summary);
226
+ data.location !== undefined && this.location(data.location);
227
+ data.description !== undefined && this.description(data.description);
228
+ data.organizer !== undefined && this.organizer(data.organizer);
229
+ data.attendees !== undefined && this.attendees(data.attendees);
230
+ data.alarms !== undefined && this.alarms(data.alarms);
231
+ data.categories !== undefined && this.categories(data.categories);
232
+ data.status !== undefined && this.status(data.status);
233
+ data.busystatus !== undefined && this.busystatus(data.busystatus);
234
+ data.priority !== undefined && this.priority(data.priority);
235
+ data.url !== undefined && this.url(data.url);
236
+ data.attachments !== undefined && this.attachments(data.attachments);
237
+ data.transparency !== undefined && this.transparency(data.transparency);
238
+ data.created !== undefined && this.created(data.created);
239
+ data.lastModified !== undefined && this.lastModified(data.lastModified);
240
+ data.class !== undefined && this.class(data.class);
241
+ data.x !== undefined && this.x(data.x);
242
+ }
243
+
244
+ /**
245
+ * Get the event's ID
246
+ * @since 0.2.0
247
+ */
248
+ id(): string;
249
+
250
+ /**
251
+ * Use this method to set the event's ID.
252
+ * If not set, a UUID will be generated randomly.
253
+ *
254
+ * @param id Event ID you want to set
255
+ */
256
+ id(id: string | number): this;
257
+ id(id?: string | number): this | string {
258
+ if (id === undefined) {
259
+ return this.data.id;
260
+ }
261
+
262
+ this.data.id = String(id);
263
+ return this;
264
+ }
265
+
266
+ /**
267
+ * Get the event's ID
268
+ * @since 0.2.0
269
+ * @alias id
270
+ */
271
+ uid(): string;
272
+
273
+ /**
274
+ * Use this method to set the event's ID.
275
+ * If not set, a UUID will be generated randomly.
276
+ *
277
+ * @param id Event ID you want to set
278
+ * @alias id
279
+ */
280
+ uid(id: string | number): this;
281
+ uid(id?: string | number): this | string {
282
+ return id === undefined ? this.id() : this.id(id);
283
+ }
284
+
285
+ /**
286
+ * Get the event's SEQUENCE number. Use this method to get the event's
287
+ * revision sequence number of the calendar component within a sequence of revisions.
288
+ *
289
+ * @since 0.2.6
290
+ */
291
+ sequence(): number;
292
+
293
+ /**
294
+ * Set the event's SEQUENCE number. For a new event, this should be zero.
295
+ * Each time the organizer makes a significant revision, the sequence
296
+ * number should be incremented.
297
+ *
298
+ * @param sequence Sequence number or null to unset it
299
+ */
300
+ sequence(sequence: number): this;
301
+ sequence(sequence?: number): this | number {
302
+ if (sequence === undefined) {
303
+ return this.data.sequence;
304
+ }
305
+
306
+ const s = parseInt(String(sequence), 10);
307
+ if (isNaN(s)) {
308
+ throw new Error('`sequence` must be a number!');
309
+ }
310
+
311
+ this.data.sequence = sequence;
312
+ return this;
313
+ }
314
+
315
+ /**
316
+ * Get the event start time which is currently
317
+ * set. Can be any supported date object.
318
+ *
319
+ * @since 0.2.0
320
+ */
321
+ start(): ICalDateTimeValue | null;
322
+
323
+ /**
324
+ * Set the appointment date of beginning, which is required for all events.
325
+ * You can use any supported date object, see
326
+ * [Readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones)
327
+ * for details about supported values and timezone handling.
328
+ *
329
+ * @since 0.2.0
330
+ */
331
+ start(start: ICalDateTimeValue): this;
332
+ start(start?: ICalDateTimeValue): this | ICalDateTimeValue | null {
333
+ if (start === undefined) {
334
+ return this.data.start;
335
+ }
336
+
337
+ this.data.start = checkDate(start, 'start');
338
+ if (this.data.start && this.data.end && toDate(this.data.start).getTime() > toDate(this.data.end).getTime()) {
339
+ const t = this.data.start;
340
+ this.data.start = this.data.end;
341
+ this.data.end = t;
342
+ }
343
+
344
+ return this;
345
+ }
346
+
347
+ /**
348
+ * Get the event end time which is currently
349
+ * set. Can be any supported date object.
350
+ *
351
+ * @since 0.2.0
352
+ */
353
+ end(): ICalDateTimeValue | null;
354
+
355
+ /**
356
+ * Set the appointment date of end. You can use any supported date object, see
357
+ * [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones)
358
+ * for details about supported values and timezone handling.
359
+ *
360
+ * @since 0.2.0
361
+ */
362
+ end(end: ICalDateTimeValue | null): this;
363
+ end(end?: ICalDateTimeValue | null): this | ICalDateTimeValue | null {
364
+ if (end === undefined) {
365
+ return this.data.end;
366
+ }
367
+ if (end === null) {
368
+ this.data.end = null;
369
+ return this;
370
+ }
371
+
372
+ this.data.end = checkDate(end, 'end');
373
+ if (this.data.start && this.data.end && toDate(this.data.start).getTime() > toDate(this.data.end).getTime()) {
374
+ const t = this.data.start;
375
+ this.data.start = this.data.end;
376
+ this.data.end = t;
377
+ }
378
+
379
+ return this;
380
+ }
381
+
382
+ /**
383
+ * Get the event's recurrence id
384
+ * @since 0.2.0
385
+ */
386
+ recurrenceId(): ICalDateTimeValue | null;
387
+
388
+ /**
389
+ * Set the event's recurrence id. You can use any supported date object, see
390
+ * [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones)
391
+ * for details about supported values and timezone handling.
392
+ *
393
+ * @since 0.2.0
394
+ */
395
+ recurrenceId(recurrenceId: ICalDateTimeValue | null): this;
396
+ recurrenceId(recurrenceId?: ICalDateTimeValue | null): this | ICalDateTimeValue | null {
397
+ if (recurrenceId === undefined) {
398
+ return this.data.recurrenceId;
399
+ }
400
+ if (recurrenceId === null) {
401
+ this.data.recurrenceId = null;
402
+ return this;
403
+ }
404
+
405
+ this.data.recurrenceId = checkDate(recurrenceId, 'recurrenceId');
406
+ return this;
407
+ }
408
+
409
+ /**
410
+ * Get the event's timezone.
411
+ * @since 0.2.6
412
+ */
413
+ timezone(): string | null;
414
+
415
+ /**
416
+ * Sets the time zone to be used for this event. If a time zone has been
417
+ * defined in both the event and the calendar, the time zone of the event
418
+ * is used.
419
+ *
420
+ * Please note that if the time zone is set, ical-generator assumes
421
+ * that all times are already in the correct time zone. Alternatively,
422
+ * a `moment-timezone` or a Luxon object can be passed with `setZone`,
423
+ * ical-generator will then set the time zone itself.
424
+ *
425
+ * This and the 'floating' flag (see below) are mutually exclusive, and setting a timezone will unset the
426
+ * 'floating' flag. If neither 'timezone' nor 'floating' are set, the date will be output with in UTC format
427
+ * (see [date-time form #2 in section 3.3.5 of RFC 554](https://tools.ietf.org/html/rfc5545#section-3.3.5)).
428
+ *
429
+ * See [Readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) for details about
430
+ * supported values and timezone handling.
431
+ *
432
+ * ```javascript
433
+ * event.timezone('America/New_York');
434
+ * ```
435
+ *
436
+ * @see https://github.com/sebbo2002/ical-generator#-date-time--timezones
437
+ * @since 0.2.6
438
+ */
439
+ timezone(timezone: string | null): this;
440
+ timezone(timezone?: string | null): this | string | null {
441
+ if (timezone === undefined && this.data.timezone !== null) {
442
+ return this.data.timezone;
443
+ }
444
+ if (timezone === undefined) {
445
+ return this.calendar.timezone();
446
+ }
447
+
448
+ this.data.timezone = timezone && timezone !== 'UTC' ? timezone.toString() : null;
449
+ if (this.data.timezone) {
450
+ this.data.floating = false;
451
+ }
452
+
453
+ return this;
454
+ }
455
+
456
+ /**
457
+ * Get the event's timestamp
458
+ * @since 0.2.0
459
+ */
460
+ stamp(): ICalDateTimeValue;
461
+
462
+ /**
463
+ * Set the appointment date of creation. Defaults to the current time and date (`new Date()`). You can use
464
+ * any supported date object, see [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones)
465
+ * for details about supported values and timezone handling.
466
+ *
467
+ * @since 0.2.0
468
+ */
469
+ stamp(stamp: ICalDateTimeValue): this;
470
+ stamp(stamp?: ICalDateTimeValue): this | ICalDateTimeValue {
471
+ if (stamp === undefined) {
472
+ return this.data.stamp;
473
+ }
474
+
475
+ this.data.stamp = checkDate(stamp, 'stamp');
476
+ return this;
477
+ }
478
+
479
+ /**
480
+ * Get the event's timestamp
481
+ * @since 0.2.0
482
+ * @alias stamp
483
+ */
484
+ timestamp(): ICalDateTimeValue;
485
+
486
+ /**
487
+ * Set the appointment date of creation. Defaults to the current time and date (`new Date()`). You can use
488
+ * any supported date object, see [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones)
489
+ * for details about supported values and timezone handling.
490
+ *
491
+ * @since 0.2.0
492
+ * @alias stamp
493
+ */
494
+ timestamp(stamp: ICalDateTimeValue): this;
495
+ timestamp(stamp?: ICalDateTimeValue): this | ICalDateTimeValue {
496
+ if (stamp === undefined) {
497
+ return this.stamp();
498
+ }
499
+
500
+ return this.stamp(stamp);
501
+ }
502
+
503
+ /**
504
+ * Get the event's allDay flag
505
+ * @since 0.2.0
506
+ */
507
+ allDay(): boolean;
508
+
509
+ /**
510
+ * Set the event's allDay flag.
511
+ *
512
+ * ```javascript
513
+ * event.allDay(true); // → appointment is for the whole day
514
+ * ```
515
+ *
516
+ * @since 0.2.0
517
+ */
518
+ allDay(allDay: boolean): this;
519
+ allDay(allDay?: boolean): this | boolean {
520
+ if (allDay === undefined) {
521
+ return this.data.allDay;
522
+ }
523
+
524
+ this.data.allDay = Boolean(allDay);
525
+ return this;
526
+ }
527
+
528
+ /**
529
+ * Get the event's floating flag.
530
+ * @since 0.2.0
531
+ */
532
+ floating(): boolean;
533
+ floating(floating: boolean): this;
534
+
535
+ /**
536
+ * Set the event's floating flag. This unsets the event's timezone.
537
+ * Events whose floating flag is set to true always take place at the
538
+ * same time, regardless of the time zone.
539
+ *
540
+ * @since 0.2.0
541
+ */
542
+ floating(floating?: boolean): this | boolean {
543
+ if (floating === undefined) {
544
+ return this.data.floating;
545
+ }
546
+
547
+ this.data.floating = Boolean(floating);
548
+ if (this.data.floating) {
549
+ this.data.timezone = null;
550
+ }
551
+
552
+ return this;
553
+ }
554
+
555
+ /**
556
+ * Get the event's repeating options
557
+ * @since 0.2.0
558
+ */
559
+ repeating(): ICalEventInternalRepeatingData | ICalRRuleStub | string | null;
560
+
561
+ /**
562
+ * Set the event's repeating options by passing an [[`ICalRepeatingOptions`]] object.
563
+ *
564
+ * ```javascript
565
+ * event.repeating({
566
+ * freq: 'MONTHLY', // required
567
+ * count: 5,
568
+ * interval: 2,
569
+ * until: new Date('Jan 01 2014 00:00:00 UTC'),
570
+ * byDay: ['su', 'mo'], // repeat only sunday and monday
571
+ * byMonth: [1, 2], // repeat only in january and february,
572
+ * byMonthDay: [1, 15], // repeat only on the 1st and 15th
573
+ * bySetPos: 3, // repeat every 3rd sunday (will take the first element of the byDay array)
574
+ * exclude: [new Date('Dec 25 2013 00:00:00 UTC')], // exclude these dates
575
+ * excludeTimezone: 'Europe/Berlin', // timezone of exclude
576
+ * wkst: 'SU' // Start the week on Sunday, default is Monday
577
+ * });
578
+ * ```
579
+ *
580
+ * @since 0.2.0
581
+ */
582
+ repeating(repeating: ICalRepeatingOptions | null): this;
583
+
584
+ /**
585
+ * Set the event's repeating options by passing an [RRule object](https://github.com/jakubroztocil/rrule).
586
+ * @since 2.0.0-develop.5
587
+ */
588
+ repeating(repeating: ICalRRuleStub | null): this;
589
+
590
+ /**
591
+ * Set the events repeating options by passing a string which is inserted in the ical file.
592
+ * @since 2.0.0-develop.5
593
+ */
594
+ repeating(repeating: string | null): this;
595
+
596
+ /**
597
+ * @internal
598
+ */
599
+ repeating(repeating: ICalRepeatingOptions | ICalRRuleStub | string | null): this;
600
+ repeating(repeating?: ICalRepeatingOptions | ICalRRuleStub | string | null): this | ICalEventInternalRepeatingData | ICalRRuleStub | string | null {
601
+ if (repeating === undefined) {
602
+ return this.data.repeating;
603
+ }
604
+ if (!repeating) {
605
+ this.data.repeating = null;
606
+ return this;
607
+ }
608
+ if(isRRule(repeating) || typeof repeating === 'string') {
609
+ this.data.repeating = repeating;
610
+ return this;
611
+ }
612
+
613
+ this.data.repeating = {
614
+ freq: checkEnum(ICalEventRepeatingFreq, repeating.freq) as ICalEventRepeatingFreq
615
+ };
616
+
617
+ if (repeating.count) {
618
+ if (!isFinite(repeating.count)) {
619
+ throw new Error('`repeating.count` must be a finite number!');
620
+ }
621
+
622
+ this.data.repeating.count = repeating.count;
623
+ }
624
+
625
+ if (repeating.interval) {
626
+ if (!isFinite(repeating.interval)) {
627
+ throw new Error('`repeating.interval` must be a finite number!');
628
+ }
629
+
630
+ this.data.repeating.interval = repeating.interval;
631
+ }
632
+
633
+ if (repeating.until !== undefined) {
634
+ this.data.repeating.until = checkDate(repeating.until, 'repeating.until');
635
+ }
636
+
637
+ if (repeating.byDay) {
638
+ const byDayArray = Array.isArray(repeating.byDay) ? repeating.byDay : [repeating.byDay];
639
+ this.data.repeating.byDay = byDayArray.map(day => checkEnum(ICalWeekday, day) as ICalWeekday);
640
+ }
641
+
642
+ if (repeating.byMonth) {
643
+ const byMonthArray = Array.isArray(repeating.byMonth) ? repeating.byMonth : [repeating.byMonth];
644
+ this.data.repeating.byMonth = byMonthArray.map(month => {
645
+ if (typeof month !== 'number' || month < 1 || month > 12) {
646
+ throw new Error('`repeating.byMonth` contains invalid value `' + month + '`!');
647
+ }
648
+
649
+ return month;
650
+ });
651
+ }
652
+
653
+ if (repeating.byMonthDay) {
654
+ const byMonthDayArray = Array.isArray(repeating.byMonthDay) ? repeating.byMonthDay : [repeating.byMonthDay];
655
+
656
+
657
+ this.data.repeating.byMonthDay = byMonthDayArray.map(monthDay => {
658
+ if (typeof monthDay !== 'number' || monthDay < 1 || monthDay > 31) {
659
+ throw new Error('`repeating.byMonthDay` contains invalid value `' + monthDay + '`!');
660
+ }
661
+
662
+ return monthDay;
663
+ });
664
+ }
665
+
666
+ if (repeating.bySetPos) {
667
+ if (!this.data.repeating.byDay) {
668
+ throw '`repeating.bySetPos` must be used along with `repeating.byDay`!';
669
+ }
670
+ if (typeof repeating.bySetPos !== 'number' || repeating.bySetPos < -1 || repeating.bySetPos > 4) {
671
+ throw '`repeating.bySetPos` contains invalid value `' + repeating.bySetPos + '`!';
672
+ }
673
+
674
+ this.data.repeating.byDay.splice(1);
675
+ this.data.repeating.bySetPos = repeating.bySetPos;
676
+ }
677
+
678
+ if (repeating.exclude) {
679
+ const excludeArray = Array.isArray(repeating.exclude) ? repeating.exclude : [repeating.exclude];
680
+ this.data.repeating.exclude = excludeArray.map((exclude, i) => {
681
+ return checkDate(exclude, `repeating.exclude[${i}]`);
682
+ });
683
+ }
684
+
685
+ if (repeating.startOfWeek) {
686
+ this.data.repeating.startOfWeek = checkEnum(ICalWeekday, repeating.startOfWeek) as ICalWeekday;
687
+ }
688
+
689
+ return this;
690
+ }
691
+
692
+ /**
693
+ * Get the event's summary
694
+ * @since 0.2.0
695
+ */
696
+ summary(): string;
697
+
698
+ /**
699
+ * Set the event's summary.
700
+ * Defaults to an empty string if nothing is set.
701
+ *
702
+ * @since 0.2.0
703
+ */
704
+ summary(summary: string): this;
705
+ summary(summary?: string): this | string {
706
+ if (summary === undefined) {
707
+ return this.data.summary;
708
+ }
709
+
710
+ this.data.summary = summary ? String(summary) : '';
711
+ return this;
712
+ }
713
+
714
+
715
+ /**
716
+ * Get the event's location
717
+ * @since 0.2.0
718
+ */
719
+ location(): ICalLocation | null;
720
+
721
+ /**
722
+ * Set the event's location by passing a string (minimum) or
723
+ * an [[`ICalLocation`]] object which will also fill the iCal
724
+ * `GEO` attribute and Apple's `X-APPLE-STRUCTURED-LOCATION`.
725
+ *
726
+ * ```javascript
727
+ * event.location({
728
+ * title: 'Apple Store Kurfürstendamm',
729
+ * address: 'Kurfürstendamm 26, 10719 Berlin, Deutschland',
730
+ * radius: 141.1751386318387,
731
+ * geo: {
732
+ * lat: 52.503630,
733
+ * lon: 13.328650
734
+ * }
735
+ * });
736
+ * ```
737
+ *
738
+ * @since 0.2.0
739
+ */
740
+ location(location: ICalLocation | string | null): this;
741
+ location(location?: ICalLocation | string | null): this | ICalLocation | null {
742
+ if (location === undefined) {
743
+ return this.data.location;
744
+ }
745
+ if (typeof location === 'string') {
746
+ this.data.location = {
747
+ title: location
748
+ };
749
+ return this;
750
+ }
751
+ if (
752
+ (location && !location.title) ||
753
+ (location?.geo && (!isFinite(location.geo.lat) || !isFinite(location.geo.lon)))
754
+ ) {
755
+ throw new Error(
756
+ '`location` isn\'t formatted correctly. See https://sebbo2002.github.io/ical-generator/'+
757
+ 'develop/reference/classes/ICalEvent.html#location'
758
+ );
759
+ }
760
+
761
+ this.data.location = location || null;
762
+ return this;
763
+ }
764
+
765
+
766
+ /**
767
+ * Get the event's description as an [[`ICalDescription`]] object.
768
+ * @since 0.2.0
769
+ */
770
+ description(): ICalDescription | null;
771
+
772
+ /**
773
+ * Set the events description by passing a plaintext string or
774
+ * an object containing both a plaintext and a html description.
775
+ * Only a few calendar apps support html descriptions and like in
776
+ * emails, supported HTML tags and styling is limited.
777
+ *
778
+ * ```javascript
779
+ * event.description({
780
+ * plain: 'Hello World!';
781
+ * html: '<p>Hello World!</p>';
782
+ * });
783
+ * ```
784
+ *
785
+ * @since 0.2.0
786
+ */
787
+ description(description: ICalDescription | string | null): this;
788
+ description(description?: ICalDescription | string | null): this | ICalDescription | null {
789
+ if (description === undefined) {
790
+ return this.data.description;
791
+ }
792
+ if (description === null) {
793
+ this.data.description = null;
794
+ return this;
795
+ }
796
+
797
+ if (typeof description === 'string') {
798
+ this.data.description = {plain: description};
799
+ }
800
+ else {
801
+ this.data.description = description;
802
+ }
803
+ return this;
804
+ }
805
+
806
+
807
+ /**
808
+ * Get the event's organizer
809
+ * @since 0.2.0
810
+ */
811
+ organizer(): ICalOrganizer | null;
812
+
813
+ /**
814
+ * Set the event's organizer
815
+ *
816
+ * ```javascript
817
+ * event.organizer({
818
+ * name: 'Organizer\'s Name',
819
+ * email: 'organizer@example.com'
820
+ * });
821
+ *
822
+ * // OR
823
+ *
824
+ * event.organizer('Organizer\'s Name <organizer@example.com>');
825
+ * ```
826
+ *
827
+ * You can also add an explicit `mailto` email address or or the sentBy address.
828
+ *
829
+ * ```javascript
830
+ * event.organizer({
831
+ * name: 'Organizer\'s Name',
832
+ * email: 'organizer@example.com',
833
+ * mailto: 'explicit@mailto.com',
834
+ * sentBy: 'substitute@example.com'
835
+ * })
836
+ * ```
837
+ *
838
+ * @since 0.2.0
839
+ */
840
+ organizer(organizer: ICalOrganizer | string | null): this;
841
+ organizer(organizer?: ICalOrganizer | string | null): this | ICalOrganizer | null {
842
+ if (organizer === undefined) {
843
+ return this.data.organizer;
844
+ }
845
+ if (organizer === null) {
846
+ this.data.organizer = null;
847
+ return this;
848
+ }
849
+
850
+ this.data.organizer = checkNameAndMail('organizer', organizer);
851
+ return this;
852
+ }
853
+
854
+
855
+ /**
856
+ * Creates a new [[`ICalAttendee`]] and returns it. Use options to prefill
857
+ * the attendee's attributes. Calling this method without options will create
858
+ * an empty attendee.
859
+ *
860
+ * ```javascript
861
+ * const cal = ical();
862
+ * const event = cal.createEvent();
863
+ * const attendee = event.createAttendee({email: 'hui@example.com', name: 'Hui'});
864
+ *
865
+ * // add another attendee
866
+ * event.createAttendee('Buh <buh@example.net>');
867
+ * ```
868
+ *
869
+ * As with the organizer, you can also add an explicit `mailto` address.
870
+ *
871
+ * ```javascript
872
+ * event.createAttendee({email: 'hui@example.com', name: 'Hui', mailto: 'another@mailto.com'});
873
+ *
874
+ * // overwrite an attendee's mailto address
875
+ * attendee.mailto('another@mailto.net');
876
+ * ```
877
+ *
878
+ * @since 0.2.0
879
+ */
880
+ createAttendee(data: ICalAttendee | ICalAttendeeData | string = {}): ICalAttendee {
881
+ if (data instanceof ICalAttendee) {
882
+ this.data.attendees.push(data);
883
+ return data;
884
+ }
885
+ if (typeof data === 'string') {
886
+ data = checkNameAndMail('data', data);
887
+ }
888
+
889
+ const attendee = new ICalAttendee(data, this);
890
+ this.data.attendees.push(attendee);
891
+ return attendee;
892
+ }
893
+
894
+
895
+ /**
896
+ * Get all attendees
897
+ * @since 0.2.0
898
+ */
899
+ attendees(): ICalAttendee[];
900
+
901
+ /**
902
+ * Add multiple attendees to your event
903
+ *
904
+ * ```javascript
905
+ * const event = ical().createEvent();
906
+ *
907
+ * cal.attendees([
908
+ * {email: 'a@example.com', name: 'Person A'},
909
+ * {email: 'b@example.com', name: 'Person B'}
910
+ * ]);
911
+ *
912
+ * cal.attendees(); // --> [ICalAttendee, ICalAttendee]
913
+ * ```
914
+ *
915
+ * @since 0.2.0
916
+ */
917
+ attendees(attendees: (ICalAttendee | ICalAttendeeData | string)[]): this;
918
+ attendees(attendees?: (ICalAttendee | ICalAttendeeData | string)[]): this | ICalAttendee[] {
919
+ if (!attendees) {
920
+ return this.data.attendees;
921
+ }
922
+
923
+ attendees.forEach(attendee => this.createAttendee(attendee));
924
+ return this;
925
+ }
926
+
927
+
928
+ /**
929
+ * Creates a new [[`ICalAlarm`]] and returns it. Use options to prefill
930
+ * the alarm's attributes. Calling this method without options will create
931
+ * an empty alarm.
932
+ *
933
+ * ```javascript
934
+ * const cal = ical();
935
+ * const event = cal.createEvent();
936
+ * const alarm = event.createAlarm({type: ICalAlarmType.display, trigger: 300});
937
+ *
938
+ * // add another alarm
939
+ * event.createAlarm({
940
+ * type: ICalAlarmType.audio,
941
+ * trigger: 300, // 5min before event
942
+ * });
943
+ * ```
944
+ *
945
+ * @since 0.2.1
946
+ */
947
+ createAlarm(data: ICalAlarm | ICalAlarmData = {}): ICalAlarm {
948
+ const alarm = data instanceof ICalAlarm ? data : new ICalAlarm(data, this);
949
+ this.data.alarms.push(alarm);
950
+ return alarm;
951
+ }
952
+
953
+
954
+ /**
955
+ * Get all alarms
956
+ * @since 0.2.0
957
+ */
958
+ alarms(): ICalAlarm[];
959
+
960
+ /**
961
+ * Add one or multiple alarms
962
+ *
963
+ * ```javascript
964
+ * const event = ical().createEvent();
965
+ *
966
+ * cal.alarms([
967
+ * {type: ICalAlarmType.display, trigger: 600},
968
+ * {type: ICalAlarmType.audio, trigger: 300}
969
+ * ]);
970
+ *
971
+ * cal.alarms(); // --> [ICalAlarm, ICalAlarm]
972
+ ```
973
+ *
974
+ * @since 0.2.0
975
+ */
976
+ alarms(alarms: ICalAlarm[] | ICalAlarmData[]): this;
977
+ alarms(alarms?: ICalAlarm[] | ICalAlarmData[]): this | ICalAlarm[] {
978
+ if (!alarms) {
979
+ return this.data.alarms;
980
+ }
981
+
982
+ alarms.forEach((alarm: ICalAlarm | ICalAlarmData) => this.createAlarm(alarm));
983
+ return this;
984
+ }
985
+
986
+
987
+ /**
988
+ * Creates a new [[`ICalCategory`]] and returns it. Use options to prefill the categories' attributes.
989
+ * Calling this method without options will create an empty category.
990
+ *
991
+ * ```javascript
992
+ * const cal = ical();
993
+ * const event = cal.createEvent();
994
+ * const category = event.createCategory({name: 'APPOINTMENT'});
995
+ *
996
+ * // add another alarm
997
+ * event.createCategory({
998
+ * name: 'MEETING'
999
+ * });
1000
+ * ```
1001
+ *
1002
+ * @since 0.3.0
1003
+ */
1004
+ createCategory(data: ICalCategory | ICalCategoryData = {}): ICalCategory {
1005
+ const category = data instanceof ICalCategory ? data : new ICalCategory(data);
1006
+ this.data.categories.push(category);
1007
+ return category;
1008
+ }
1009
+
1010
+
1011
+ /**
1012
+ * Get all categories
1013
+ * @since 0.3.0
1014
+ */
1015
+ categories(): ICalCategory[];
1016
+
1017
+ /**
1018
+ * Add categories to the event or return all selected categories.
1019
+ *
1020
+ * ```javascript
1021
+ * const event = ical().createEvent();
1022
+ *
1023
+ * cal.categories([
1024
+ * {name: 'APPOINTMENT'},
1025
+ * {name: 'MEETING'}
1026
+ * ]);
1027
+ *
1028
+ * cal.categories(); // --> [ICalCategory, ICalCategory]
1029
+ * ```
1030
+ *
1031
+ * @since 0.3.0
1032
+ */
1033
+ categories(categories: (ICalCategory | ICalCategoryData)[]): this;
1034
+ categories(categories?: (ICalCategory | ICalCategoryData)[]): this | ICalCategory[] {
1035
+ if (!categories) {
1036
+ return this.data.categories;
1037
+ }
1038
+
1039
+ categories.forEach(category => this.createCategory(category));
1040
+ return this;
1041
+ }
1042
+
1043
+
1044
+ /**
1045
+ * Get the event's status
1046
+ * @since 0.2.0
1047
+ */
1048
+ status(): ICalEventStatus | null;
1049
+
1050
+ /**
1051
+ * Set the event's status
1052
+ *
1053
+ * ```javascript
1054
+ * import ical, {ICalEventStatus} from 'ical-generator';
1055
+ * event.status(ICalEventStatus.CONFIRMED);
1056
+ * ```
1057
+ *
1058
+ * @since 0.2.0
1059
+ */
1060
+ status(status: ICalEventStatus | null): this;
1061
+ status(status?: ICalEventStatus | null): this | ICalEventStatus | null {
1062
+ if (status === undefined) {
1063
+ return this.data.status;
1064
+ }
1065
+ if (status === null) {
1066
+ this.data.status = null;
1067
+ return this;
1068
+ }
1069
+
1070
+ this.data.status = checkEnum(ICalEventStatus, status) as ICalEventStatus;
1071
+ return this;
1072
+ }
1073
+
1074
+
1075
+ /**
1076
+ * Get the event's busy status
1077
+ * @since 1.0.2
1078
+ */
1079
+ busystatus(): ICalEventBusyStatus | null;
1080
+
1081
+ /**
1082
+ * Set the event's busy status. Will add the
1083
+ * [`X-MICROSOFT-CDO-BUSYSTATUS`](https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/cd68eae7-ed65-4dd3-8ea7-ad585c76c736)
1084
+ * attribute to your event.
1085
+ *
1086
+ * ```javascript
1087
+ * import ical, {ICalEventBusyStatus} from 'ical-generator';
1088
+ * event.busystatus(ICalEventStatus.BUSY);
1089
+ * ```
1090
+ *
1091
+ * @since 1.0.2
1092
+ */
1093
+ busystatus(busystatus: ICalEventBusyStatus | null): this;
1094
+ busystatus(busystatus?: ICalEventBusyStatus | null): this | ICalEventBusyStatus | null {
1095
+ if (busystatus === undefined) {
1096
+ return this.data.busystatus;
1097
+ }
1098
+ if (busystatus === null) {
1099
+ this.data.busystatus = null;
1100
+ return this;
1101
+ }
1102
+
1103
+ this.data.busystatus = checkEnum(ICalEventBusyStatus, busystatus) as ICalEventBusyStatus;
1104
+ return this;
1105
+ }
1106
+
1107
+
1108
+ /**
1109
+ * Get the event's priority. A value of 1 represents
1110
+ * the highest priority, 9 the lowest. 0 specifies an undefined
1111
+ * priority.
1112
+ *
1113
+ * @since v2.0.0-develop.7
1114
+ */
1115
+ priority(): number | null;
1116
+
1117
+ /**
1118
+ * Set the event's priority. A value of 1 represents
1119
+ * the highest priority, 9 the lowest. 0 specifies an undefined
1120
+ * priority.
1121
+ *
1122
+ * @since v2.0.0-develop.7
1123
+ */
1124
+ priority(priority: number | null): this;
1125
+ priority(priority?: number | null): this | number | null {
1126
+ if (priority === undefined) {
1127
+ return this.data.priority;
1128
+ }
1129
+ if (priority === null) {
1130
+ this.data.priority = null;
1131
+ return this;
1132
+ }
1133
+
1134
+ if(priority < 0 || priority > 9) {
1135
+ throw new Error('`priority` is invalid, musst be 0 ≤ priority ≤ 9.');
1136
+ }
1137
+
1138
+ this.data.priority = Math.round(priority);
1139
+ return this;
1140
+ }
1141
+
1142
+
1143
+ /**
1144
+ * Get the event's URL
1145
+ * @since 0.2.0
1146
+ */
1147
+ url(): string | null;
1148
+
1149
+ /**
1150
+ * Set the event's URL
1151
+ * @since 0.2.0
1152
+ */
1153
+ url(url: string | null): this;
1154
+ url(url?: string | null): this | string | null {
1155
+ if (url === undefined) {
1156
+ return this.data.url;
1157
+ }
1158
+
1159
+ this.data.url = url ? String(url) : null;
1160
+ return this;
1161
+ }
1162
+
1163
+ /**
1164
+ * Adds an attachment to the event by adding the file URL to the calendar.
1165
+ *
1166
+ * `ical-generator` only supports external attachments. File attachments that
1167
+ * are directly included in the file are not supported, because otherwise the
1168
+ * calendar file could easily become unfavourably large.
1169
+ *
1170
+ * ```javascript
1171
+ * const cal = ical();
1172
+ * const event = cal.createEvent();
1173
+ * event.createAttachment('https://files.sebbo.net/calendar/attachments/foo');
1174
+ * ```
1175
+ *
1176
+ * @since 3.2.0-develop.1
1177
+ */
1178
+ createAttachment(url: string): this {
1179
+ this.data.attachments.push(url);
1180
+ return this;
1181
+ }
1182
+
1183
+
1184
+ /**
1185
+ * Get all attachment urls
1186
+ * @since 3.2.0-develop.1
1187
+ */
1188
+ attachments(): string[];
1189
+
1190
+ /**
1191
+ * Add one or multiple alarms
1192
+ *
1193
+ * ```javascript
1194
+ * const event = ical().createEvent();
1195
+ *
1196
+ * cal.attachments([
1197
+ * 'https://files.sebbo.net/calendar/attachments/foo',
1198
+ * 'https://files.sebbo.net/calendar/attachments/bar'
1199
+ * ]);
1200
+ *
1201
+ * cal.attachments(); // --> [string, string]
1202
+ ```
1203
+ *
1204
+ * 3.2.0-develop.1
1205
+ */
1206
+ attachments(attachments: string[]): this;
1207
+ attachments(attachments?: string[]): this | string[] {
1208
+ if (!attachments) {
1209
+ return this.data.attachments;
1210
+ }
1211
+
1212
+ attachments.forEach((attachment: string) => this.createAttachment(attachment));
1213
+ return this;
1214
+ }
1215
+
1216
+ /**
1217
+ * Get the event's transparency
1218
+ * @since 1.7.3
1219
+ */
1220
+ transparency(): ICalEventTransparency | null;
1221
+
1222
+ /**
1223
+ * Set the event's transparency
1224
+ *
1225
+ * Set the field to `OPAQUE` if the person or resource is no longer
1226
+ * available due to this event. If the calendar entry has no influence
1227
+ * on availability, you can set the field to `TRANSPARENT`. This value
1228
+ * is mostly used to find out if a person has time on a certain date or
1229
+ * not (see `TRANSP` in iCal specification).
1230
+ *
1231
+ * ```javascript
1232
+ * import ical, {ICalEventTransparency} from 'ical-generator';
1233
+ * event.transparency(ICalEventTransparency.OPAQUE);
1234
+ * ```
1235
+ *
1236
+ * @since 1.7.3
1237
+ */
1238
+ transparency(transparency: ICalEventTransparency | null): this;
1239
+ transparency(transparency?: ICalEventTransparency | null): this | ICalEventTransparency | null {
1240
+ if (transparency === undefined) {
1241
+ return this.data.transparency;
1242
+ }
1243
+ if (!transparency) {
1244
+ this.data.transparency = null;
1245
+ return this;
1246
+ }
1247
+
1248
+ this.data.transparency = checkEnum(ICalEventTransparency, transparency) as ICalEventTransparency;
1249
+ return this;
1250
+ }
1251
+
1252
+
1253
+ /**
1254
+ * Get the event's creation date
1255
+ * @since 0.3.0
1256
+ */
1257
+ created(): ICalDateTimeValue | null;
1258
+
1259
+ /**
1260
+ * Set the event's creation date
1261
+ * @since 0.3.0
1262
+ */
1263
+ created(created: ICalDateTimeValue | null): this;
1264
+ created(created?: ICalDateTimeValue | null): this | ICalDateTimeValue | null {
1265
+ if (created === undefined) {
1266
+ return this.data.created;
1267
+ }
1268
+ if (created === null) {
1269
+ this.data.created = null;
1270
+ return this;
1271
+ }
1272
+
1273
+ this.data.created = checkDate(created, 'created');
1274
+ return this;
1275
+ }
1276
+
1277
+
1278
+ /**
1279
+ * Get the event's last modification date
1280
+ * @since 0.3.0
1281
+ */
1282
+ lastModified(): ICalDateTimeValue | null;
1283
+
1284
+ /**
1285
+ * Set the event's last modification date
1286
+ * @since 0.3.0
1287
+ */
1288
+ lastModified(lastModified: ICalDateTimeValue | null): this;
1289
+ lastModified(lastModified?: ICalDateTimeValue | null): this | ICalDateTimeValue | null {
1290
+ if (lastModified === undefined) {
1291
+ return this.data.lastModified;
1292
+ }
1293
+ if (lastModified === null) {
1294
+ this.data.lastModified = null;
1295
+ return this;
1296
+ }
1297
+
1298
+ this.data.lastModified = checkDate(lastModified, 'lastModified');
1299
+ return this;
1300
+ }
1301
+
1302
+ /**
1303
+ * Get the event's class
1304
+ * @since 2.0.0
1305
+ */
1306
+ class(): ICalEventClass | null;
1307
+
1308
+ /**
1309
+ * Set the event's class
1310
+ *
1311
+ * ```javascript
1312
+ * import ical, { ICalEventClass } from 'ical-generator';
1313
+ * event.class(ICalEventClass.PRIVATE);
1314
+ * ```
1315
+ *
1316
+ * @since 2.0.0
1317
+ */
1318
+ class(class_: ICalEventClass | null): this;
1319
+ class(class_?: ICalEventClass | null): this | ICalEventClass | null {
1320
+ if (class_ === undefined) {
1321
+ return this.data.class;
1322
+ }
1323
+ if (class_ === null) {
1324
+ this.data.class = null;
1325
+ return this;
1326
+ }
1327
+
1328
+ this.data.class = checkEnum(ICalEventClass, class_) as ICalEventClass;
1329
+ return this;
1330
+ }
1331
+
1332
+
1333
+ /**
1334
+ * Set X-* attributes. Woun't filter double attributes,
1335
+ * which are also added by another method (e.g. summary),
1336
+ * so these attributes may be inserted twice.
1337
+ *
1338
+ * ```javascript
1339
+ * event.x([
1340
+ * {
1341
+ * key: "X-MY-CUSTOM-ATTR",
1342
+ * value: "1337!"
1343
+ * }
1344
+ * ]);
1345
+ *
1346
+ * event.x([
1347
+ * ["X-MY-CUSTOM-ATTR", "1337!"]
1348
+ * ]);
1349
+ *
1350
+ * event.x({
1351
+ * "X-MY-CUSTOM-ATTR": "1337!"
1352
+ * });
1353
+ * ```
1354
+ *
1355
+ * @since 1.9.0
1356
+ */
1357
+ x (keyOrArray: {key: string, value: string}[] | [string, string][] | Record<string, string>): this;
1358
+
1359
+ /**
1360
+ * Set a X-* attribute. Woun't filter double attributes,
1361
+ * which are also added by another method (e.g. summary),
1362
+ * so these attributes may be inserted twice.
1363
+ *
1364
+ * ```javascript
1365
+ * event.x("X-MY-CUSTOM-ATTR", "1337!");
1366
+ * ```
1367
+ *
1368
+ * @since 1.9.0
1369
+ */
1370
+ x (keyOrArray: string, value: string): this;
1371
+
1372
+ /**
1373
+ * Get all custom X-* attributes.
1374
+ * @since 1.9.0
1375
+ */
1376
+ x (): {key: string, value: string}[];
1377
+ x(keyOrArray?: ({ key: string, value: string })[] | [string, string][] | Record<string, string> | string, value?: string): this | void | ({ key: string, value: string })[] {
1378
+ if (keyOrArray === undefined) {
1379
+ return addOrGetCustomAttributes(this.data);
1380
+ }
1381
+
1382
+ if (typeof keyOrArray === 'string' && typeof value === 'string') {
1383
+ addOrGetCustomAttributes(this.data, keyOrArray, value);
1384
+ }
1385
+ if (typeof keyOrArray === 'object') {
1386
+ addOrGetCustomAttributes(this.data, keyOrArray);
1387
+ }
1388
+
1389
+ return this;
1390
+ }
1391
+
1392
+
1393
+ /**
1394
+ * Return a shallow copy of the events's options for JSON stringification.
1395
+ * Third party objects like moment.js values or RRule objects are stringified
1396
+ * as well. Can be used for persistence.
1397
+ *
1398
+ * ```javascript
1399
+ * const event = ical().createEvent();
1400
+ * const json = JSON.stringify(event);
1401
+ *
1402
+ * // later: restore event data
1403
+ * const calendar = ical().createEvent(JSON.parse(json));
1404
+ * ```
1405
+ *
1406
+ * @since 0.2.4
1407
+ */
1408
+ toJSON(): ICalEventJSONData {
1409
+ let repeating: ICalEventInternalRepeatingData | string | null = null;
1410
+ if(isRRule(this.data.repeating) || typeof this.data.repeating === 'string') {
1411
+ repeating = this.data.repeating.toString();
1412
+ }
1413
+ else if(this.data.repeating) {
1414
+ repeating = Object.assign({}, this.data.repeating, {
1415
+ until: toJSON(this.data.repeating.until),
1416
+ exclude: this.data.repeating.exclude?.map(d => toJSON(d)),
1417
+ });
1418
+ }
1419
+
1420
+ return Object.assign({}, this.data, {
1421
+ start: toJSON(this.data.start) || null,
1422
+ end: toJSON(this.data.end) || null,
1423
+ recurrenceId: toJSON(this.data.recurrenceId) || null,
1424
+ stamp: toJSON(this.data.stamp) || null,
1425
+ created: toJSON(this.data.created) || null,
1426
+ lastModified: toJSON(this.data.lastModified) || null,
1427
+ repeating,
1428
+ x: this.x()
1429
+ });
1430
+ }
1431
+
1432
+
1433
+ /**
1434
+ * Return generated event as a string.
1435
+ *
1436
+ * ```javascript
1437
+ * const event = ical().createEvent();
1438
+ * console.log(event.toString()); // → BEGIN:VEVENT…
1439
+ * ```
1440
+ */
1441
+ toString(): string {
1442
+ let g = '';
1443
+
1444
+ if (!this.data.start) {
1445
+ throw new Error('No value for `start` in ICalEvent #' + this.data.id + ' given!');
1446
+ }
1447
+
1448
+ // DATE & TIME
1449
+ g += 'BEGIN:VEVENT\r\n';
1450
+ g += 'UID:' + this.data.id + '\r\n';
1451
+
1452
+ // SEQUENCE
1453
+ g += 'SEQUENCE:' + this.data.sequence + '\r\n';
1454
+
1455
+ g += 'DTSTAMP:' + formatDate(this.calendar.timezone(), this.data.stamp) + '\r\n';
1456
+ if (this.data.allDay) {
1457
+ g += 'DTSTART;VALUE=DATE:' + formatDate(this.calendar.timezone(), this.data.start, true) + '\r\n';
1458
+ if (this.data.end) {
1459
+ g += 'DTEND;VALUE=DATE:' + formatDate(this.calendar.timezone(), this.data.end, true) + '\r\n';
1460
+ }
1461
+
1462
+ g += 'X-MICROSOFT-CDO-ALLDAYEVENT:TRUE\r\n';
1463
+ g += 'X-MICROSOFT-MSNCALENDAR-ALLDAYEVENT:TRUE\r\n';
1464
+ }
1465
+ else {
1466
+ g += formatDateTZ(this.timezone(), 'DTSTART', this.data.start, this.data) + '\r\n';
1467
+ if (this.data.end) {
1468
+ g += formatDateTZ(this.timezone(), 'DTEND', this.data.end, this.data) + '\r\n';
1469
+ }
1470
+ }
1471
+
1472
+ // REPEATING
1473
+ if(isRRule(this.data.repeating) || typeof this.data.repeating === 'string') {
1474
+ g += this.data.repeating
1475
+ .toString()
1476
+ .replace(/\r\n/g, '\n')
1477
+ .split('\n')
1478
+ .filter(l => l && !l.startsWith('DTSTART:'))
1479
+ .join('\r\n') + '\r\n';
1480
+ }
1481
+ else if (this.data.repeating) {
1482
+ g += 'RRULE:FREQ=' + this.data.repeating.freq;
1483
+
1484
+ if (this.data.repeating.count) {
1485
+ g += ';COUNT=' + this.data.repeating.count;
1486
+ }
1487
+
1488
+ if (this.data.repeating.interval) {
1489
+ g += ';INTERVAL=' + this.data.repeating.interval;
1490
+ }
1491
+
1492
+ if (this.data.repeating.until) {
1493
+ g += ';UNTIL=' + formatDate(this.calendar.timezone(), this.data.repeating.until);
1494
+ }
1495
+
1496
+ if (this.data.repeating.byDay) {
1497
+ g += ';BYDAY=' + this.data.repeating.byDay.join(',');
1498
+ }
1499
+
1500
+ if (this.data.repeating.byMonth) {
1501
+ g += ';BYMONTH=' + this.data.repeating.byMonth.join(',');
1502
+ }
1503
+
1504
+ if (this.data.repeating.byMonthDay) {
1505
+ g += ';BYMONTHDAY=' + this.data.repeating.byMonthDay.join(',');
1506
+ }
1507
+
1508
+ if (this.data.repeating.bySetPos) {
1509
+ g += ';BYSETPOS=' + this.data.repeating.bySetPos;
1510
+ }
1511
+
1512
+ if (this.data.repeating.startOfWeek) {
1513
+ g += ';WKST=' + this.data.repeating.startOfWeek;
1514
+ }
1515
+
1516
+ g += '\r\n';
1517
+
1518
+ // REPEATING EXCLUSION
1519
+ if (this.data.repeating.exclude) {
1520
+ if (this.data.allDay) {
1521
+ g += 'EXDATE;VALUE=DATE:' + this.data.repeating.exclude.map(excludedDate => {
1522
+ return formatDate(this.calendar.timezone(), excludedDate, true);
1523
+ }).join(',') + '\r\n';
1524
+ }
1525
+ else {
1526
+ g += 'EXDATE';
1527
+ if (this.timezone()) {
1528
+ g += ';TZID=' + this.timezone() + ':' + this.data.repeating.exclude.map(excludedDate => {
1529
+ // This isn't a 'floating' event because it has a timezone;
1530
+ // but we use it to omit the 'Z' UTC specifier in formatDate()
1531
+ return formatDate(this.timezone(), excludedDate, false, true);
1532
+ }).join(',') + '\r\n';
1533
+ }
1534
+ else {
1535
+ g += ':' + this.data.repeating.exclude.map(excludedDate => {
1536
+ return formatDate(this.timezone(), excludedDate);
1537
+ }).join(',') + '\r\n';
1538
+ }
1539
+ }
1540
+ }
1541
+ }
1542
+
1543
+ // RECURRENCE
1544
+ if (this.data.recurrenceId) {
1545
+ g += formatDateTZ(this.timezone(), 'RECURRENCE-ID', this.data.recurrenceId, this.data) + '\r\n';
1546
+ }
1547
+
1548
+ // SUMMARY
1549
+ g += 'SUMMARY:' + escape(this.data.summary, false) + '\r\n';
1550
+
1551
+ // TRANSPARENCY
1552
+ if (this.data.transparency) {
1553
+ g += 'TRANSP:' + escape(this.data.transparency, false) + '\r\n';
1554
+ }
1555
+
1556
+ // LOCATION
1557
+ if (this.data.location?.title) {
1558
+ g += 'LOCATION:' + escape(
1559
+ this.data.location.title +
1560
+ (this.data.location.address ? '\n' + this.data.location.address : ''),
1561
+ false
1562
+ ) + '\r\n';
1563
+
1564
+ if (this.data.location.radius && this.data.location.geo) {
1565
+ g += 'X-APPLE-STRUCTURED-LOCATION;VALUE=URI;' +
1566
+ (this.data.location.address ? 'X-ADDRESS=' + escape(this.data.location.address, false) + ';' : '') +
1567
+ 'X-APPLE-RADIUS=' + escape(this.data.location.radius, false) + ';' +
1568
+ 'X-TITLE=' + escape(this.data.location.title, false) +
1569
+ ':geo:' + escape(this.data.location.geo?.lat, false) + ',' +
1570
+ escape(this.data.location.geo?.lon, false) + '\r\n';
1571
+ }
1572
+
1573
+ if (this.data.location.geo) {
1574
+ g += 'GEO:' + escape(this.data.location.geo?.lat, false) + ';' +
1575
+ escape(this.data.location.geo?.lon, false) + '\r\n';
1576
+ }
1577
+ }
1578
+
1579
+ // DESCRIPTION
1580
+ if (this.data.description) {
1581
+ g += 'DESCRIPTION:' + escape(this.data.description.plain, false) + '\r\n';
1582
+
1583
+ // HTML DESCRIPTION
1584
+ if (this.data.description.html) {
1585
+ g += 'X-ALT-DESC;FMTTYPE=text/html:' + escape(this.data.description.html, false) + '\r\n';
1586
+ }
1587
+ }
1588
+
1589
+ // ORGANIZER
1590
+ if (this.data.organizer) {
1591
+ g += 'ORGANIZER;CN="' + escape(this.data.organizer.name, true) + '"';
1592
+
1593
+ if (this.data.organizer.sentBy) {
1594
+ g += ';SENT-BY="mailto:' + escape(this.data.organizer.sentBy, true) + '"';
1595
+ }
1596
+ if (this.data.organizer.email && this.data.organizer.mailto) {
1597
+ g += ';EMAIL=' + escape(this.data.organizer.email, false);
1598
+ }
1599
+ if(this.data.organizer.email) {
1600
+ g += ':mailto:' + escape(this.data.organizer.mailto || this.data.organizer.email, false);
1601
+ }
1602
+ g += '\r\n';
1603
+ }
1604
+
1605
+ // ATTENDEES
1606
+ this.data.attendees.forEach(function (attendee) {
1607
+ g += attendee.toString();
1608
+ });
1609
+
1610
+ // ALARMS
1611
+ this.data.alarms.forEach(function (alarm) {
1612
+ g += alarm.toString();
1613
+ });
1614
+
1615
+ // CATEGORIES
1616
+ if (this.data.categories.length > 0) {
1617
+ g += 'CATEGORIES:' + this.data.categories.map(function (category) {
1618
+ return category.toString();
1619
+ }).join() + '\r\n';
1620
+ }
1621
+
1622
+ // URL
1623
+ if (this.data.url) {
1624
+ g += 'URL;VALUE=URI:' + escape(this.data.url, false) + '\r\n';
1625
+ }
1626
+
1627
+ // ATTACHMENT
1628
+ if (this.data.attachments.length > 0) {
1629
+ this.data.attachments.forEach(url => {
1630
+ g += 'ATTACH:' + escape(url, false) + '\r\n';
1631
+ });
1632
+ }
1633
+
1634
+ // STATUS
1635
+ if (this.data.status) {
1636
+ g += 'STATUS:' + this.data.status.toUpperCase() + '\r\n';
1637
+ }
1638
+
1639
+ // BUSYSTATUS
1640
+ if (this.data.busystatus) {
1641
+ g += 'X-MICROSOFT-CDO-BUSYSTATUS:' + this.data.busystatus.toUpperCase() + '\r\n';
1642
+ }
1643
+
1644
+ // PRIORITY
1645
+ if (this.data.priority !== null) {
1646
+ g += 'PRIORITY:' + this.data.priority + '\r\n';
1647
+ }
1648
+
1649
+ // CUSTOM X ATTRIBUTES
1650
+ g += generateCustomAttributes(this.data);
1651
+
1652
+ // CREATED
1653
+ if (this.data.created) {
1654
+ g += 'CREATED:' + formatDate(this.calendar.timezone(), this.data.created) + '\r\n';
1655
+ }
1656
+
1657
+ // LAST-MODIFIED
1658
+ if (this.data.lastModified) {
1659
+ g += 'LAST-MODIFIED:' + formatDate(this.calendar.timezone(), this.data.lastModified) + '\r\n';
1660
+ }
1661
+
1662
+ if (this.data.class) {
1663
+ g+= 'CLASS:' + this.data.class.toUpperCase() + '\r\n';
1664
+ }
1665
+
1666
+ g += 'END:VEVENT\r\n';
1667
+ return g;
1668
+ }
1669
+ }