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.
@@ -0,0 +1,864 @@
1
+ 'use strict';
2
+
3
+ import {
4
+ addOrGetCustomAttributes,
5
+ checkEnum,
6
+ foldLines,
7
+ generateCustomAttributes,
8
+ isMomentDuration,
9
+ toDurationString
10
+ } from './tools';
11
+ import ICalEvent, {ICalEventData, ICalEventJSONData} from './event';
12
+ import {writeFile, writeFileSync, promises as fsPromises} from 'fs';
13
+ import {ServerResponse} from 'http';
14
+ import { ICalMomentDurationStub, ICalTimezone } from './types';
15
+
16
+
17
+ export interface ICalCalendarData {
18
+ prodId?: ICalCalendarProdIdData | string;
19
+ method?: ICalCalendarMethod | null;
20
+ name?: string | null;
21
+ description?: string | null;
22
+ timezone?: ICalTimezone | string | null;
23
+ source?: string | null;
24
+ url?: string | null;
25
+ scale?: string | null;
26
+ ttl?: number | ICalMomentDurationStub | null;
27
+ events?: (ICalEvent | ICalEventData)[];
28
+ x?: {key: string, value: string}[] | [string, string][] | Record<string, string>;
29
+ }
30
+
31
+ interface ICalCalendarInternalData {
32
+ prodId: string;
33
+ method: ICalCalendarMethod | null;
34
+ name: string | null;
35
+ description: string | null;
36
+ timezone: ICalTimezone | null;
37
+ source: string | null;
38
+ url: string | null;
39
+ scale: string | null;
40
+ ttl: number | null;
41
+ events: ICalEvent[];
42
+ x: [string, string][];
43
+ }
44
+
45
+ export interface ICalCalendarJSONData {
46
+ prodId: string;
47
+ method: ICalCalendarMethod | null;
48
+ name: string | null;
49
+ description: string | null;
50
+ timezone: string | null;
51
+ source: string | null;
52
+ url: string | null;
53
+ scale: string | null;
54
+ ttl: number | null;
55
+ events: ICalEventJSONData[];
56
+ x: {key: string, value: string}[];
57
+ }
58
+
59
+ export interface ICalCalendarProdIdData {
60
+ company: string;
61
+ product: string;
62
+ language?: string;
63
+ }
64
+
65
+ export enum ICalCalendarMethod {
66
+ PUBLISH = 'PUBLISH',
67
+ REQUEST = 'REQUEST',
68
+ REPLY = 'REPLY',
69
+ ADD = 'ADD',
70
+ CANCEL = 'CANCEL',
71
+ REFRESH = 'REFRESH',
72
+ COUNTER = 'COUNTER',
73
+ DECLINECOUNTER = 'DECLINECOUNTER'
74
+ }
75
+
76
+
77
+ /**
78
+ * Usually you get an `ICalCalendar` object like this:
79
+ * ```javascript
80
+ * import ical from 'ical-generator';
81
+ * const calendar = ical();
82
+ * ```
83
+ *
84
+ * But you can also use the constructor directly like this:
85
+ * ```javascript
86
+ * import {ICalCalendar} from 'ical-generator';
87
+ * const calendar = new ICalCalendar();
88
+ * ```
89
+ */
90
+ export default class ICalCalendar {
91
+ private readonly data: ICalCalendarInternalData;
92
+
93
+ /**
94
+ * You can pass options to setup your calendar or use setters to do this.
95
+ *
96
+ * ```javascript
97
+ * * import ical from 'ical-generator';
98
+ *
99
+ * // or use require:
100
+ * // const ical = require('ical-generator');
101
+ *
102
+ *
103
+ * const cal = ical({name: 'my first iCal'});
104
+ *
105
+ * // is the same as
106
+ *
107
+ * const cal = ical().name('my first iCal');
108
+ *
109
+ * // is the same as
110
+ *
111
+ * const cal = ical();
112
+ * cal.name('sebbo.net');
113
+ * ```
114
+ *
115
+ * @param data Calendar data
116
+ */
117
+ constructor(data: ICalCalendarData = {}) {
118
+ this.data = {
119
+ prodId: '//sebbo.net//ical-generator//EN',
120
+ method: null,
121
+ name: null,
122
+ description: null,
123
+ timezone: null,
124
+ source: null,
125
+ url: null,
126
+ scale: null,
127
+ ttl: null,
128
+ events: [],
129
+ x: []
130
+ };
131
+
132
+ data.prodId !== undefined && this.prodId(data.prodId);
133
+ data.method !== undefined && this.method(data.method);
134
+ data.name !== undefined && this.name(data.name);
135
+ data.description !== undefined && this.description(data.description);
136
+ data.timezone !== undefined && this.timezone(data.timezone);
137
+ data.source !== undefined && this.source(data.source);
138
+ data.url !== undefined && this.url(data.url);
139
+ data.scale !== undefined && this.scale(data.scale);
140
+ data.ttl !== undefined && this.ttl(data.ttl);
141
+ data.events !== undefined && this.events(data.events);
142
+ data.x !== undefined && this.x(data.x);
143
+ }
144
+
145
+
146
+ /**
147
+ * Get your feed's prodid. Will always return a string.
148
+ * @since 0.2.0
149
+ */
150
+ prodId(): string;
151
+
152
+ /**
153
+ * Set your feed's prodid. `prodid` can be either a
154
+ * string like `//sebbo.net//ical-generator//EN` or a
155
+ * valid [[`ICalCalendarProdIdData`]] object. `language`
156
+ * is optional and defaults to `EN`.
157
+ *
158
+ * ```javascript
159
+ * cal.prodId({
160
+ * company: 'My Company',
161
+ * product: 'My Product',
162
+ * language: 'EN' // optional, defaults to EN
163
+ * });
164
+ * ```
165
+ *
166
+ * @since 0.2.0
167
+ */
168
+ prodId(prodId: ICalCalendarProdIdData | string): this;
169
+ prodId(prodId?: ICalCalendarProdIdData | string): this | string {
170
+ if (!prodId) {
171
+ return this.data.prodId;
172
+ }
173
+
174
+ const prodIdRegEx = /^\/\/(.+)\/\/(.+)\/\/([A-Z]{1,4})$/;
175
+
176
+ if (typeof prodId === 'string' && prodIdRegEx.test(prodId)) {
177
+ this.data.prodId = prodId;
178
+ return this;
179
+ }
180
+ if (typeof prodId === 'string') {
181
+ throw new Error(
182
+ '`prodId` isn\'t formated correctly. See https://sebbo2002.github.io/ical-generator/develop/reference/'+
183
+ 'classes/ICalCalendar.html#prodId'
184
+ );
185
+ }
186
+
187
+ if (typeof prodId !== 'object') {
188
+ throw new Error('`prodid` needs to be a valid formed string or an object!');
189
+ }
190
+
191
+ if (!prodId.company) {
192
+ throw new Error('`prodid.company` is a mandatory item!');
193
+ }
194
+ if (!prodId.product) {
195
+ throw new Error('`prodid.product` is a mandatory item!');
196
+ }
197
+
198
+ const language = (prodId.language || 'EN').toUpperCase();
199
+ this.data.prodId = '//' + prodId.company + '//' + prodId.product + '//' + language;
200
+ return this;
201
+ }
202
+
203
+
204
+ /**
205
+ * Get the feed method attribute.
206
+ * See [[`ICalCalendarMethod`]] for possible results.
207
+ *
208
+ * @since 0.2.8
209
+ */
210
+ method(): ICalCalendarMethod | null;
211
+
212
+ /**
213
+ * Set the feed method attribute.
214
+ * See [[`ICalCalendarMethod`]] for available options.
215
+ *
216
+ * #### Typescript Example
217
+ * ```typescript
218
+ * import {ICalCalendarMethod} from 'ical-generator';
219
+ * calendar.method(ICalCalendarMethod.PUBLISH);
220
+ * ```
221
+ *
222
+ * @since 0.2.8
223
+ */
224
+ method(method: ICalCalendarMethod | null): this;
225
+ method(method?: ICalCalendarMethod | null): this | ICalCalendarMethod | null {
226
+ if (method === undefined) {
227
+ return this.data.method;
228
+ }
229
+ if (!method) {
230
+ this.data.method = null;
231
+ return this;
232
+ }
233
+
234
+ this.data.method = checkEnum(ICalCalendarMethod, method) as ICalCalendarMethod;
235
+ return this;
236
+ }
237
+
238
+
239
+ /**
240
+ * Get your feed's name
241
+ * @since 0.2.0
242
+ */
243
+ name(): string | null;
244
+
245
+ /**
246
+ * Set your feed's name. Is used to fill `NAME`
247
+ * and `X-WR-CALNAME` in your iCal file.
248
+ *
249
+ * @since 0.2.0
250
+ */
251
+ name(name: string | null): this;
252
+ name(name?: string | null): this | string | null {
253
+ if (name === undefined) {
254
+ return this.data.name;
255
+ }
256
+
257
+ this.data.name = name ? String(name) : null;
258
+ return this;
259
+ }
260
+
261
+
262
+ /**
263
+ * Get your feed's description
264
+ * @since 0.2.7
265
+ */
266
+ description(): string | null;
267
+
268
+ /**
269
+ * Set your feed's description
270
+ * @since 0.2.7
271
+ */
272
+ description(description: string | null): this;
273
+ description(description?: string | null): this | string | null {
274
+ if (description === undefined) {
275
+ return this.data.description;
276
+ }
277
+
278
+ this.data.description = description ? String(description) : null;
279
+ return this;
280
+ }
281
+
282
+
283
+ /**
284
+ * Get the current calendar timezone
285
+ * @since 0.2.0
286
+ */
287
+ timezone(): string | null;
288
+
289
+ /**
290
+ * Use this method to set your feed's timezone. Is used
291
+ * to fill `TIMEZONE-ID` and `X-WR-TIMEZONE` in your iCal export.
292
+ * Please not that all date values are treaded differently, if
293
+ * a timezone was set. See [[`formatDate`]] for details. If no
294
+ * time zone is specified, all information is output as UTC.
295
+ *
296
+ * ```javascript
297
+ * cal.timezone('America/New_York');
298
+ * ```
299
+ *
300
+ * @see https://github.com/sebbo2002/ical-generator#-date-time--timezones
301
+ * @since 0.2.0
302
+ */
303
+ timezone(timezone: string | null): this;
304
+
305
+ /**
306
+ * Sets the time zone to be used in this calendar file for all times of all
307
+ * events. Please note that if the time zone is set, ical-generator assumes
308
+ * that all times are already in the correct time zone. Alternatively, a
309
+ * `moment-timezone` or a Luxon object can be passed with `setZone`,
310
+ * ical-generator will then set the time zone itself.
311
+ *
312
+ * For the best support of time zones, a VTimezone entry in the calendar is
313
+ * recommended, which informs the client about the corresponding time zones
314
+ * (daylight saving time, deviation from UTC, etc.). `ical-generator` itself
315
+ * does not have a time zone database, so an external generator is needed here.
316
+ *
317
+ * A VTimezone generator is a function that takes a time zone as a string and
318
+ * returns a VTimezone component according to the ical standard. For example,
319
+ * ical-timezones can be used for this:
320
+ *
321
+ * ```typescript
322
+ * import ical from 'ical-generator';
323
+ * import {getVtimezoneComponent} from '@touch4it/ical-timezones';
324
+ *
325
+ * const cal = new ICalCalendar();
326
+ * cal.timezone({
327
+ * name: 'FOO',
328
+ * generator: getVtimezoneComponent
329
+ * });
330
+ * cal.createEvent({
331
+ * start: new Date(),
332
+ * timezone: 'Europe/London'
333
+ * });
334
+ * ```
335
+ *
336
+ * @see https://github.com/sebbo2002/ical-generator#-date-time--timezones
337
+ * @since 2.0.0
338
+ */
339
+ timezone(timezone: ICalTimezone | string | null): this;
340
+ timezone(timezone?: ICalTimezone | string | null): this | string | null {
341
+ if (timezone === undefined) {
342
+ return this.data.timezone?.name || null;
343
+ }
344
+
345
+ if(timezone === 'UTC') {
346
+ this.data.timezone = null;
347
+ }
348
+ else if(typeof timezone === 'string') {
349
+ this.data.timezone = {name: timezone};
350
+ }
351
+ else if(timezone === null) {
352
+ this.data.timezone = null;
353
+ }
354
+ else {
355
+ this.data.timezone = timezone;
356
+ }
357
+
358
+ return this;
359
+ }
360
+
361
+
362
+ /**
363
+ * Get current value of the `SOURCE` attribute.
364
+ * @since 2.2.0-develop.1
365
+ */
366
+ source(): string | null;
367
+
368
+ /**
369
+ * Use this method to set your feed's `SOURCE` attribute.
370
+ * This tells the client where to refresh your feed.
371
+ *
372
+ * ```javascript
373
+ * cal.source('http://example.com/my/original_source.ical');
374
+ * ```
375
+ *
376
+ * @since 2.2.0-develop.1
377
+ */
378
+ source(source: string | null): this;
379
+ source(source?: string | null): this | string | null {
380
+ if (source === undefined) {
381
+ return this.data.source;
382
+ }
383
+
384
+ this.data.source = source || null;
385
+ return this;
386
+ }
387
+
388
+
389
+ /**
390
+ * Get your feed's URL
391
+ * @since 0.2.5
392
+ */
393
+ url(): string | null;
394
+
395
+ /**
396
+ * Set your feed's URL
397
+ *
398
+ * ```javascript
399
+ * calendar.url('http://example.com/my/feed.ical');
400
+ * ```
401
+ *
402
+ * @since 0.2.5
403
+ */
404
+ url(url: string | null): this;
405
+ url(url?: string | null): this | string | null {
406
+ if (url === undefined) {
407
+ return this.data.url;
408
+ }
409
+
410
+ this.data.url = url || null;
411
+ return this;
412
+ }
413
+
414
+
415
+ /**
416
+ * Get current value of the `CALSCALE` attribute. It will
417
+ * return `null` if no value was set. The iCal standard
418
+ * specifies this as `GREGORIAN` if no value is present.
419
+ *
420
+ * @since 1.8.0
421
+ */
422
+ scale(): string | null;
423
+
424
+ /**
425
+ * Use this method to set your feed's `CALSCALE` attribute. There is no
426
+ * default value for this property and it will not appear in your iCal
427
+ * file unless set. The iCal standard specifies this as `GREGORIAN` if
428
+ * no value is present.
429
+ *
430
+ * ```javascript
431
+ * cal.scale('gregorian');
432
+ * ```
433
+ *
434
+ * @since 1.8.0
435
+ */
436
+ scale(scale: string | null): this;
437
+ scale(scale?: string | null): this | string | null {
438
+ if (scale === undefined) {
439
+ return this.data.scale;
440
+ }
441
+
442
+ if (scale === null) {
443
+ this.data.scale = null;
444
+ }
445
+ else {
446
+ this.data.scale = scale.toUpperCase();
447
+ }
448
+
449
+ return this;
450
+ }
451
+
452
+
453
+ /**
454
+ * Get the current ttl duration in seconds
455
+ * @since 0.2.5
456
+ */
457
+ ttl(): number | null;
458
+
459
+ /**
460
+ * Use this method to set your feed's time to live
461
+ * (in seconds). Is used to fill `REFRESH-INTERVAL` and
462
+ * `X-PUBLISHED-TTL` in your iCal.
463
+ *
464
+ * ```javascript
465
+ * const cal = ical().ttl(60 * 60 * 24); // 1 day
466
+ * ```
467
+ *
468
+ * You can also pass a moment.js duration object. Zero, null
469
+ * or negative numbers will reset the `ttl` attribute.
470
+ *
471
+ * @since 0.2.5
472
+ */
473
+ ttl(ttl: number | ICalMomentDurationStub | null): this;
474
+ ttl(ttl?: number | ICalMomentDurationStub | null): this | number | null {
475
+ if (ttl === undefined) {
476
+ return this.data.ttl;
477
+ }
478
+
479
+ if (isMomentDuration(ttl)) {
480
+ this.data.ttl = ttl.asSeconds();
481
+ }
482
+ else if (ttl && ttl > 0) {
483
+ this.data.ttl = ttl;
484
+ }
485
+ else {
486
+ this.data.ttl = null;
487
+ }
488
+
489
+ return this;
490
+ }
491
+
492
+
493
+ /**
494
+ * Creates a new [[`ICalEvent`]] and returns it. Use options to prefill the event's attributes.
495
+ * Calling this method without options will create an empty event.
496
+ *
497
+ * ```javascript
498
+ * import ical from 'ical-generator';
499
+ *
500
+ * // or use require:
501
+ * // const ical = require('ical-generator');
502
+ *
503
+ * const cal = ical();
504
+ * const event = cal.createEvent({summary: 'My Event'});
505
+ *
506
+ * // overwrite event summary
507
+ * event.summary('Your Event');
508
+ * ```
509
+ *
510
+ * @since 0.2.0
511
+ */
512
+ createEvent(data: ICalEvent | ICalEventData): ICalEvent {
513
+ const event = data instanceof ICalEvent ? data : new ICalEvent(data, this);
514
+ this.data.events.push(event);
515
+ return event;
516
+ }
517
+
518
+
519
+ /**
520
+ * Returns all events of this calendar.
521
+ *
522
+ * ```javascript
523
+ * const cal = ical();
524
+ *
525
+ * cal.events([
526
+ * {
527
+ * start: new Date(),
528
+ * end: new Date(new Date().getTime() + 3600000),
529
+ * summary: 'Example Event',
530
+ * description: 'It works ;)',
531
+ * url: 'http://sebbo.net/'
532
+ * }
533
+ * ]);
534
+ *
535
+ * cal.events(); // --> [ICalEvent]
536
+ * ```
537
+ *
538
+ * @since 0.2.0
539
+ */
540
+ events(): ICalEvent[];
541
+
542
+ /**
543
+ * Add multiple events to your calendar.
544
+ *
545
+ * ```javascript
546
+ * const cal = ical();
547
+ *
548
+ * cal.events([
549
+ * {
550
+ * start: new Date(),
551
+ * end: new Date(new Date().getTime() + 3600000),
552
+ * summary: 'Example Event',
553
+ * description: 'It works ;)',
554
+ * url: 'http://sebbo.net/'
555
+ * }
556
+ * ]);
557
+ *
558
+ * cal.events(); // --> [ICalEvent]
559
+ * ```
560
+ *
561
+ * @since 0.2.0
562
+ */
563
+ events(events: (ICalEvent | ICalEventData)[]): this;
564
+ events(events?: (ICalEvent | ICalEventData)[]): this | ICalEvent[] {
565
+ if (!events) {
566
+ return this.data.events;
567
+ }
568
+
569
+ events.forEach((e: ICalEvent | ICalEventData) => this.createEvent(e));
570
+ return this;
571
+ }
572
+
573
+
574
+ /**
575
+ * Remove all events from the calendar without
576
+ * touching any other data like name or prodId.
577
+ *
578
+ * @since 2.0.0-develop.1
579
+ */
580
+ clear(): this {
581
+ this.data.events = [];
582
+ return this;
583
+ }
584
+
585
+
586
+ /**
587
+ * Save ical file using [`fs/promises`](https://nodejs.org/api/fs.html#fs_fspromises_writefile_file_data_options).
588
+ * Only works in node.js environments.
589
+ *
590
+ * ```javascript
591
+ * await calendar.save('./calendar.ical');
592
+ * ```
593
+ */
594
+ save(path: string): Promise<void>;
595
+
596
+ /**
597
+ * Save ical file with [`fs.writeFile`](http://nodejs.org/api/fs.html#fs_fs_writefile_filename_data_options_callback).
598
+ * Only works in node.js environments.
599
+ *
600
+ * ```javascript
601
+ * calendar.save('./calendar.ical', err => {
602
+ * console.log(err);
603
+ * });
604
+ * ```
605
+ */
606
+ save(path: string, cb?: (err: NodeJS.ErrnoException | null) => void): this;
607
+ save(path: string, cb?: (err: NodeJS.ErrnoException | null) => void): this | Promise<void> {
608
+ if (cb) {
609
+ writeFile(path, this.toString(), cb);
610
+ return this;
611
+ }
612
+
613
+ return fsPromises.writeFile(path, this.toString());
614
+ }
615
+
616
+
617
+ /**
618
+ * Save Calendar to disk synchronously using
619
+ * [fs.writeFileSync](http://nodejs.org/api/fs.html#fs_fs_writefilesync_filename_data_options).
620
+ * Only works in node.js environments.
621
+ *
622
+ * ```javascript
623
+ * calendar.saveSync('./calendar.ical');
624
+ * ```
625
+ */
626
+ saveSync(path: string): this {
627
+ writeFileSync(path, this.toString());
628
+ return this;
629
+ }
630
+
631
+
632
+ /**
633
+ * Send calendar to the user when using HTTP using the passed `ServerResponse` object.
634
+ * Use second parameter `filename` to change the filename, which defaults to `'calendar.ics'`.
635
+ *
636
+ * @param response HTTP Response object which is used to send the calendar
637
+ * @param [filename = 'calendar.ics'] Filename of the calendar file
638
+ */
639
+ serve(response: ServerResponse, filename = 'calendar.ics'): this {
640
+ response.writeHead(200, {
641
+ 'Content-Type': 'text/calendar; charset=utf-8',
642
+ 'Content-Disposition': `attachment; filename="${filename}"`
643
+ });
644
+
645
+ response.end(this.toString());
646
+ return this;
647
+ }
648
+
649
+
650
+ /**
651
+ * Generates a blob to use for downloads or to generate a download URL.
652
+ * Only supported in browsers supporting the Blob API.
653
+ *
654
+ * Unfortunately, because node.js has no Blob implementation (they have Buffer
655
+ * instead), this method is currently untested. Sorry Dave…
656
+ *
657
+ * @since 1.9.0
658
+ */
659
+ toBlob(): Blob {
660
+ return new Blob([this.toString()], {type: 'text/calendar'});
661
+ }
662
+
663
+
664
+ /**
665
+ * Returns a URL to download the ical file. Uses the Blob object internally,
666
+ * so it's only supported in browsers supporting the Blob API.
667
+ *
668
+ * Unfortunately, because node.js has no Blob implementation (they have Buffer
669
+ * instead), this can't be tested right now. Sorry Dave…
670
+ *
671
+ * @since 1.9.0
672
+ */
673
+ toURL(): string {
674
+ return URL.createObjectURL(this.toBlob());
675
+ }
676
+
677
+
678
+ /**
679
+ * Set X-* attributes. Woun't filter double attributes,
680
+ * which are also added by another method (e.g. busystatus),
681
+ * so these attributes may be inserted twice.
682
+ *
683
+ * ```javascript
684
+ * calendar.x([
685
+ * {
686
+ * key: "X-MY-CUSTOM-ATTR",
687
+ * value: "1337!"
688
+ * }
689
+ * ]);
690
+ *
691
+ * calendar.x([
692
+ * ["X-MY-CUSTOM-ATTR", "1337!"]
693
+ * ]);
694
+ *
695
+ * calendar.x({
696
+ * "X-MY-CUSTOM-ATTR": "1337!"
697
+ * });
698
+ * ```
699
+ *
700
+ * @since 1.9.0
701
+ */
702
+ x (keyOrArray: {key: string, value: string}[] | [string, string][] | Record<string, string>): this;
703
+
704
+ /**
705
+ * Set a X-* attribute. Woun't filter double attributes,
706
+ * which are also added by another method (e.g. busystatus),
707
+ * so these attributes may be inserted twice.
708
+ *
709
+ * ```javascript
710
+ * calendar.x("X-MY-CUSTOM-ATTR", "1337!");
711
+ * ```
712
+ *
713
+ * @since 1.9.0
714
+ */
715
+ x (keyOrArray: string, value: string): this;
716
+
717
+ /**
718
+ * Get all custom X-* attributes.
719
+ * @since 1.9.0
720
+ */
721
+ x (): {key: string, value: string}[];
722
+ x (keyOrArray?: {key: string, value: string}[] | [string, string][] | Record<string, string> | string, value?: string): this | void | ({key: string, value: string})[] {
723
+ if(keyOrArray === undefined) {
724
+ return addOrGetCustomAttributes (this.data);
725
+ }
726
+
727
+ if(typeof keyOrArray === 'string' && typeof value === 'string') {
728
+ addOrGetCustomAttributes (this.data, keyOrArray, value);
729
+ }
730
+ else if(typeof keyOrArray === 'object') {
731
+ addOrGetCustomAttributes (this.data, keyOrArray);
732
+ }
733
+ else {
734
+ throw new Error('Either key or value is not a string!');
735
+ }
736
+
737
+ return this;
738
+ }
739
+
740
+
741
+ /**
742
+ * Return a shallow copy of the calendar's options for JSON stringification.
743
+ * Third party objects like moment.js values or RRule objects are stringified
744
+ * as well. Can be used for persistence.
745
+ *
746
+ * ```javascript
747
+ * const cal = ical();
748
+ * const json = JSON.stringify(cal);
749
+ *
750
+ * // later: restore calendar data
751
+ * cal = ical(JSON.parse(json));
752
+ * ```
753
+ *
754
+ * @since 0.2.4
755
+ */
756
+ toJSON(): ICalCalendarJSONData {
757
+ return Object.assign({}, this.data, {
758
+ timezone: this.timezone(),
759
+ events: this.data.events.map(event => event.toJSON()),
760
+ x: this.x()
761
+ });
762
+ }
763
+
764
+
765
+ /**
766
+ * Get the number of events added to your calendar
767
+ */
768
+ length(): number {
769
+ return this.data.events.length;
770
+ }
771
+
772
+
773
+ /**
774
+ * Return generated calendar as a string.
775
+ *
776
+ * ```javascript
777
+ * const cal = ical();
778
+ * console.log(cal.toString()); // → BEGIN:VCALENDAR…
779
+ * ```
780
+ */
781
+ toString(): string {
782
+ let g = '';
783
+
784
+ // VCALENDAR and VERSION
785
+ g = 'BEGIN:VCALENDAR\r\nVERSION:2.0\r\n';
786
+
787
+ // PRODID
788
+ g += 'PRODID:-' + this.data.prodId + '\r\n';
789
+
790
+ // URL
791
+ if (this.data.url) {
792
+ g += 'URL:' + this.data.url + '\r\n';
793
+ }
794
+
795
+ // SOURCE
796
+ if (this.data.source) {
797
+ g += 'SOURCE;VALUE=URI:' + this.data.source + '\r\n';
798
+ }
799
+
800
+ // CALSCALE
801
+ if (this.data.scale) {
802
+ g += 'CALSCALE:' + this.data.scale + '\r\n';
803
+ }
804
+
805
+ // METHOD
806
+ if (this.data.method) {
807
+ g += 'METHOD:' + this.data.method + '\r\n';
808
+ }
809
+
810
+ // NAME
811
+ if (this.data.name) {
812
+ g += 'NAME:' + this.data.name + '\r\n';
813
+ g += 'X-WR-CALNAME:' + this.data.name + '\r\n';
814
+ }
815
+
816
+ // Description
817
+ if (this.data.description) {
818
+ g += 'X-WR-CALDESC:' + this.data.description + '\r\n';
819
+ }
820
+
821
+ // Timezone
822
+ if(this.data.timezone?.generator) {
823
+ const timezones = [...new Set([
824
+ this.timezone(),
825
+ ...this.data.events.map(event => event.timezone())
826
+ ])].filter(tz => tz !== null && !tz.startsWith('/')) as string[];
827
+
828
+ timezones.forEach(tz => {
829
+ if(!this.data.timezone?.generator) {
830
+ return;
831
+ }
832
+
833
+ const s = this.data.timezone.generator(tz);
834
+ if(!s) {
835
+ return;
836
+ }
837
+
838
+ g += s.replace(/\r\n/g, '\n')
839
+ .replace(/\n/g, '\r\n')
840
+ .trim() + '\r\n';
841
+ });
842
+ }
843
+ if (this.data.timezone?.name) {
844
+ g += 'TIMEZONE-ID:' + this.data.timezone.name + '\r\n';
845
+ g += 'X-WR-TIMEZONE:' + this.data.timezone.name + '\r\n';
846
+ }
847
+
848
+ // TTL
849
+ if (this.data.ttl) {
850
+ g += 'REFRESH-INTERVAL;VALUE=DURATION:' + toDurationString(this.data.ttl) + '\r\n';
851
+ g += 'X-PUBLISHED-TTL:' + toDurationString(this.data.ttl) + '\r\n';
852
+ }
853
+
854
+ // Events
855
+ this.data.events.forEach(event => g += event.toString());
856
+
857
+ // CUSTOM X ATTRIBUTES
858
+ g += generateCustomAttributes(this.data);
859
+
860
+ g += 'END:VCALENDAR';
861
+
862
+ return foldLines(g);
863
+ }
864
+ }