UNPKG

ical-generator

Version:

ical-generator is a small piece of code which generates ical calendar files

1,694 lines (1,533 loc) 57.1 kB
'use strict'; import uuid from 'uuid-random'; import { addOrGetCustomAttributes, checkDate, checkEnum, checkNameAndMail, escape, formatDate, formatDateTZ, generateCustomAttributes, isRRule, toDate, toJSON } from './tools.ts'; import ICalAttendee, { type ICalAttendeeData } from './attendee.ts'; import ICalAlarm, { type ICalAlarmData } from './alarm.ts'; import ICalCategory, { type ICalCategoryData } from './category.ts'; import ICalCalendar from './calendar.ts'; import { ICalEventRepeatingFreq, ICalWeekday, type ICalDateTimeValue, type ICalDescription, type ICalLocation, type ICalOrganizer, type ICalRRuleStub, type ICalRepeatingOptions } from './types.ts'; export enum ICalEventStatus { CONFIRMED = 'CONFIRMED', TENTATIVE = 'TENTATIVE', CANCELLED = 'CANCELLED' } export enum ICalEventBusyStatus { FREE = 'FREE', TENTATIVE = 'TENTATIVE', BUSY = 'BUSY', OOF = 'OOF' } export enum ICalEventTransparency { TRANSPARENT = 'TRANSPARENT', OPAQUE = 'OPAQUE' } export enum ICalEventClass { PUBLIC = 'PUBLIC', PRIVATE = 'PRIVATE', CONFIDENTIAL = 'CONFIDENTIAL' } export interface ICalEventData { id?: string | number | null, sequence?: number, start: ICalDateTimeValue, end?: ICalDateTimeValue | null, recurrenceId?: ICalDateTimeValue | null, timezone?: string | null, stamp?: ICalDateTimeValue, allDay?: boolean, floating?: boolean, repeating?: ICalRepeatingOptions | ICalRRuleStub | string | null, summary?: string, location?: ICalLocation | string | null, description?: ICalDescription | string | null, organizer?: ICalOrganizer | string | null, attendees?: ICalAttendee[] | ICalAttendeeData[], alarms?: ICalAlarm[] | ICalAlarmData[], categories?: ICalCategory[] | ICalCategoryData[], status?: ICalEventStatus | null, busystatus?: ICalEventBusyStatus | null, priority?: number | null, url?: string | null, attachments?: string[], transparency?: ICalEventTransparency | null, created?: ICalDateTimeValue | null, lastModified?: ICalDateTimeValue | null, class?: ICalEventClass | null; x?: {key: string, value: string}[] | [string, string][] | Record<string, string>; } interface ICalEventInternalData { id: string, sequence: number, start: ICalDateTimeValue, end: ICalDateTimeValue | null, recurrenceId: ICalDateTimeValue | null, timezone: string | null, stamp: ICalDateTimeValue, allDay: boolean, floating: boolean, repeating: ICalEventJSONRepeatingData | ICalRRuleStub | string | null, summary: string, location: ICalLocation | null, description: ICalDescription | null, organizer: ICalOrganizer | null, attendees: ICalAttendee[], alarms: ICalAlarm[], categories: ICalCategory[], status: ICalEventStatus | null, busystatus: ICalEventBusyStatus | null, priority: number | null, url: string | null, attachments: string[], transparency: ICalEventTransparency | null, created: ICalDateTimeValue | null, lastModified: ICalDateTimeValue | null, class: ICalEventClass | null, x: [string, string][]; } export interface ICalEventJSONData { id: string, sequence: number, start: string, end: string | null, recurrenceId: string | null, timezone: string | null, stamp: string, allDay: boolean, floating: boolean, repeating: ICalEventJSONRepeatingData | string | null, summary: string, location: ICalLocation | null, description: ICalDescription | null, organizer: ICalOrganizer | null, attendees: ICalAttendee[], alarms: ICalAlarm[], categories: ICalCategory[], status: ICalEventStatus | null, busystatus: ICalEventBusyStatus | null, priority?: number | null, url: string | null, attachments: string[], transparency: ICalEventTransparency | null, created: string | null, lastModified: string | null, x: {key: string, value: string}[]; } export interface ICalEventJSONRepeatingData { freq: ICalEventRepeatingFreq; count?: number; interval?: number; until?: ICalDateTimeValue; byDay?: ICalWeekday[]; byMonth?: number[]; byMonthDay?: number[]; bySetPos?: number[]; exclude?: ICalDateTimeValue[]; startOfWeek?: ICalWeekday; } /** * Usually you get an {@link ICalEvent} object like this: * ```javascript * import ical from 'ical-generator'; * const calendar = ical(); * const event = calendar.createEvent(); * ``` */ export default class ICalEvent { private readonly data: ICalEventInternalData; private readonly calendar: ICalCalendar; /** * Constructor of [[`ICalEvent`]. The calendar reference is * required to query the calendar's timezone when required. * * @param data Calendar Event Data * @param calendar Reference to ICalCalendar object */ constructor(data: ICalEventData, calendar: ICalCalendar) { this.data = { id: uuid(), sequence: 0, start: new Date(), end: null, recurrenceId: null, timezone: null, stamp: new Date(), allDay: false, floating: false, repeating: null, summary: '', location: null, description: null, organizer: null, attendees: [], alarms: [], categories: [], status: null, busystatus: null, priority: null, url: null, attachments: [], transparency: null, created: null, lastModified: null, class: null, x: [] }; this.calendar = calendar; if (!calendar) { throw new Error('`calendar` option required!'); } if (data.id) this.id(data.id); if (data.sequence !== undefined) this.sequence(data.sequence); if (data.start) this.start(data.start); if (data.end !== undefined) this.end(data.end); if (data.recurrenceId !== undefined) this.recurrenceId(data.recurrenceId); if (data.timezone !== undefined) this.timezone(data.timezone); if (data.stamp !== undefined) this.stamp(data.stamp); if (data.allDay !== undefined) this.allDay(data.allDay); if (data.floating !== undefined) this.floating(data.floating); if (data.repeating !== undefined) this.repeating(data.repeating); if (data.summary !== undefined) this.summary(data.summary); if (data.location !== undefined) this.location(data.location); if (data.description !== undefined) this.description(data.description); if (data.organizer !== undefined) this.organizer(data.organizer); if (data.attendees !== undefined) this.attendees(data.attendees); if (data.alarms !== undefined) this.alarms(data.alarms); if (data.categories !== undefined) this.categories(data.categories); if (data.status !== undefined) this.status(data.status); if (data.busystatus !== undefined) this.busystatus(data.busystatus); if (data.priority !== undefined) this.priority(data.priority); if (data.url !== undefined) this.url(data.url); if (data.attachments !== undefined) this.attachments(data.attachments); if (data.transparency !== undefined) this.transparency(data.transparency); if (data.created !== undefined) this.created(data.created); if (data.lastModified !== undefined) this.lastModified(data.lastModified); if (data.class !== undefined) this.class(data.class); if (data.x !== undefined) this.x(data.x); } /** * Get the event's ID * @since 0.2.0 */ id(): string; /** * Use this method to set the event's ID. * If not set, a UUID will be generated randomly. * * @param id Event ID you want to set */ id(id: string | number): this; id(id?: string | number): this | string { if (id === undefined) { return this.data.id; } this.data.id = String(id); return this; } /** * Get the event's ID * @since 0.2.0 * @see {@link id} */ uid(): string; /** * Use this method to set the event's ID. * If not set, a UUID will be generated randomly. * * @param id Event ID you want to set */ uid(id: string | number): this; uid(id?: string | number): this | string { return id === undefined ? this.id() : this.id(id); } /** * Get the event's SEQUENCE number. Use this method to get the event's * revision sequence number of the calendar component within a sequence of revisions. * * @since 0.2.6 */ sequence(): number; /** * Set the event's SEQUENCE number. For a new event, this should be zero. * Each time the organizer makes a significant revision, the sequence * number should be incremented. * * @param sequence Sequence number or null to unset it */ sequence(sequence: number): this; sequence(sequence?: number): this | number { if (sequence === undefined) { return this.data.sequence; } const s = parseInt(String(sequence), 10); if (isNaN(s)) { throw new Error('`sequence` must be a number!'); } this.data.sequence = sequence; return this; } /** * Get the event start time which is currently * set. Can be any supported date object. * * @since 0.2.0 */ start(): ICalDateTimeValue; /** * Set the appointment date of beginning, which is required for all events. * You can use any supported date object, see * [Readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) * for details about supported values and timezone handling. * * ```typescript * import ical from 'ical-generator'; * * const cal = ical(); * * const event = cal.createEvent({ * start: new Date('2020-01-01') * }); * * // overwrites old start date * event.start(new Date('2024-02-01')); * * cal.toString(); * ``` * * ```text * BEGIN:VCALENDAR * VERSION:2.0 * PRODID:-//sebbo.net//ical-generator//EN * BEGIN:VEVENT * UID:7e2aee64-b07a-4256-9b3e-e9eaa452bac8 * SEQUENCE:0 * DTSTAMP:20240212T190915Z * DTSTART:20240201T000000Z * SUMMARY: * END:VEVENT * END:VCALENDAR * ``` * * @since 0.2.0 */ start(start: ICalDateTimeValue): this; start(start?: ICalDateTimeValue): this | ICalDateTimeValue { if (start === undefined) { this.swapStartAndEndIfRequired(); return this.data.start; } this.data.start = checkDate(start, 'start'); return this; } /** * Get the event end time which is currently * set. Can be any supported date object. * * @since 0.2.0 */ end(): ICalDateTimeValue | null; /** * Set the appointment date of end. You can use any supported date object, see * [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) * for details about supported values and timezone handling. * * @since 0.2.0 */ end(end: ICalDateTimeValue | null): this; end(end?: ICalDateTimeValue | null): this | ICalDateTimeValue | null { if (end === undefined) { this.swapStartAndEndIfRequired(); return this.data.end; } if (end === null) { this.data.end = null; return this; } this.data.end = checkDate(end, 'end'); return this; } /** * Checks if the start date is after the end date and swaps them if necessary. * @private */ private swapStartAndEndIfRequired(): void { if (this.data.start && this.data.end && toDate(this.data.start).getTime() > toDate(this.data.end).getTime()) { const t = this.data.start; this.data.start = this.data.end; this.data.end = t; } } /** * Get the event's recurrence id * @since 0.2.0 */ recurrenceId(): ICalDateTimeValue | null; /** * Set the event's recurrence id. You can use any supported date object, see * [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) * for details about supported values and timezone handling. * * @since 0.2.0 */ recurrenceId(recurrenceId: ICalDateTimeValue | null): this; recurrenceId(recurrenceId?: ICalDateTimeValue | null): this | ICalDateTimeValue | null { if (recurrenceId === undefined) { return this.data.recurrenceId; } if (recurrenceId === null) { this.data.recurrenceId = null; return this; } this.data.recurrenceId = checkDate(recurrenceId, 'recurrenceId'); return this; } /** * Get the event's timezone. * @since 0.2.6 */ timezone(): string | null; /** * Sets the time zone to be used for this event. If a time zone has been * defined in both the event and the calendar, the time zone of the event * is used. * * Please note that if the time zone is set, ical-generator assumes * that all times are already in the correct time zone. Alternatively, * a `moment-timezone` or a Luxon object can be passed with `setZone`, * ical-generator will then set the time zone itself. * * This and the 'floating' flag (see below) are mutually exclusive, and setting a timezone will unset the * 'floating' flag. If neither 'timezone' nor 'floating' are set, the date will be output with in UTC format * (see [date-time form #2 in section 3.3.5 of RFC 554](https://tools.ietf.org/html/rfc5545#section-3.3.5)). * * See [Readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) for details about * supported values and timezone handling. * * ```javascript * event.timezone('America/New_York'); * ``` * * @see https://github.com/sebbo2002/ical-generator#-date-time--timezones * @since 0.2.6 */ timezone(timezone: string | null): this; timezone(timezone?: string | null): this | string | null { if (timezone === undefined && this.data.timezone !== null) { return this.data.timezone; } if (timezone === undefined) { return this.calendar.timezone(); } this.data.timezone = timezone && timezone !== 'UTC' ? timezone.toString() : null; if (this.data.timezone) { this.data.floating = false; } return this; } /** * Get the event's timestamp * @since 0.2.0 * @see {@link timestamp} */ stamp(): ICalDateTimeValue; /** * Set the appointment date of creation. Defaults to the current time and date (`new Date()`). You can use * any supported date object, see [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) * for details about supported values and timezone handling. * * @since 0.2.0 * @see {@link timestamp} */ stamp(stamp: ICalDateTimeValue): this; stamp(stamp?: ICalDateTimeValue): this | ICalDateTimeValue { if (stamp === undefined) { return this.data.stamp; } this.data.stamp = checkDate(stamp, 'stamp'); return this; } /** * Get the event's timestamp * @since 0.2.0 * @see {@link stamp} */ timestamp(): ICalDateTimeValue; /** * Set the appointment date of creation. Defaults to the current time and date (`new Date()`). You can use * any supported date object, see [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) * for details about supported values and timezone handling. * * @since 0.2.0 * @see {@link stamp} */ timestamp(stamp: ICalDateTimeValue): this; timestamp(stamp?: ICalDateTimeValue): this | ICalDateTimeValue { if (stamp === undefined) { return this.stamp(); } return this.stamp(stamp); } /** * Get the event's allDay flag * @since 0.2.0 */ allDay(): boolean; /** * Set the event's allDay flag. * * ```javascript * event.allDay(true); // → appointment is for the whole day * ``` * * ```typescript * import ical from 'ical-generator'; * * const cal = ical(); * * cal.createEvent({ * start: new Date('2020-01-01'), * summary: 'Very Important Day', * allDay: true * }); * * cal.toString(); * ``` * * ```text * BEGIN:VCALENDAR * VERSION:2.0 * PRODID:-//sebbo.net//ical-generator//EN * BEGIN:VEVENT * UID:1964fe8d-32c5-4f2a-bd62-7d9d7de5992b * SEQUENCE:0 * DTSTAMP:20240212T191956Z * DTSTART;VALUE=DATE:20200101 * X-MICROSOFT-CDO-ALLDAYEVENT:TRUE * X-MICROSOFT-MSNCALENDAR-ALLDAYEVENT:TRUE * SUMMARY:Very Important Day * END:VEVENT * END:VCALENDAR * ``` * * @since 0.2.0 */ allDay(allDay: boolean): this; allDay(allDay?: boolean): this | boolean { if (allDay === undefined) { return this.data.allDay; } this.data.allDay = Boolean(allDay); return this; } /** * Get the event's floating flag. * @since 0.2.0 */ floating(): boolean; floating(floating: boolean): this; /** * Set the event's floating flag. This unsets the event's timezone. * Events whose floating flag is set to true always take place at the * same time, regardless of the time zone. * * ```typescript * import ical from 'ical-generator'; * * const cal = ical(); * * cal.createEvent({ * start: new Date('2020-01-01T20:00:00Z'), * summary: 'Always at 20:00 in every <Timezone', * floating: true * }); * * cal.toString(); * ``` * * ```text * BEGIN:VCALENDAR * VERSION:2.0 * PRODID:-//sebbo.net//ical-generator//EN * BEGIN:VEVENT * UID:5d7278f9-ada3-40ef-83d1-23c29ce0a763 * SEQUENCE:0 * DTSTAMP:20240212T192214Z * DTSTART:20200101T200000 * SUMMARY:Always at 20:00 in every <Timezone * END:VEVENT * END:VCALENDAR * ``` * * @since 0.2.0 */ floating(floating?: boolean): this | boolean { if (floating === undefined) { return this.data.floating; } this.data.floating = Boolean(floating); if (this.data.floating) { this.data.timezone = null; } return this; } /** * Get the event's repeating options * @since 0.2.0 */ repeating(): ICalEventJSONRepeatingData | ICalRRuleStub | string | null; /** * Set the event's repeating options by passing an {@link ICalRepeatingOptions} object. * * ```javascript * event.repeating({ * freq: 'MONTHLY', // required * count: 5, * interval: 2, * until: new Date('Jan 01 2014 00:00:00 UTC'), * byDay: ['su', 'mo'], // repeat only sunday and monday * byMonth: [1, 2], // repeat only in january and february, * byMonthDay: [1, 15], // repeat only on the 1st and 15th * bySetPos: 3, // repeat every 3rd sunday (will take the first element of the byDay array) * exclude: [new Date('Dec 25 2013 00:00:00 UTC')], // exclude these dates * excludeTimezone: 'Europe/Berlin', // timezone of exclude * wkst: 'SU' // Start the week on Sunday, default is Monday * }); * ``` * * **Example:** * *```typescript * import ical, { ICalEventRepeatingFreq } from 'ical-generator'; * * const cal = ical(); * * const event = cal.createEvent({ * start: new Date('2020-01-01T20:00:00Z'), * summary: 'Repeating Event' * }); * event.repeating({ * freq: ICalEventRepeatingFreq.WEEKLY, * count: 4 * }); * * cal.toString(); * ``` * * ```text * BEGIN:VCALENDAR * VERSION:2.0 * PRODID:-//sebbo.net//ical-generator//EN * BEGIN:VEVENT * UID:b80e6a68-c2cd-48f5-b94d-cecc7ce83871 * SEQUENCE:0 * DTSTAMP:20240212T193646Z * DTSTART:20200101T200000Z * RRULE:FREQ=WEEKLY;COUNT=4 * SUMMARY:Repeating Event * END:VEVENT * END:VCALENDAR * ``` * * @since 0.2.0 */ repeating(repeating: ICalRepeatingOptions | null): this; /** * Set the event's repeating options by passing an [RRule object](https://github.com/jakubroztocil/rrule). * @since 2.0.0-develop.5 * * ```typescript * import ical from 'ical-generator'; * import { datetime, RRule } from 'rrule'; * * const cal = ical(); * * const event = cal.createEvent({ * start: new Date('2020-01-01T20:00:00Z'), * summary: 'Repeating Event' * }); * * const rule = new RRule({ * freq: RRule.WEEKLY, * interval: 5, * byweekday: [RRule.MO, RRule.FR], * dtstart: datetime(2012, 2, 1, 10, 30), * until: datetime(2012, 12, 31) * }) * event.repeating(rule); * * cal.toString(); * ``` * * ```text * BEGIN:VCALENDAR * VERSION:2.0 * PRODID:-//sebbo.net//ical-generator//EN * BEGIN:VEVENT * UID:36585e40-8fa8-460d-af0c-88b6f434030b * SEQUENCE:0 * DTSTAMP:20240212T193827Z * DTSTART:20200101T200000Z * RRULE:FREQ=WEEKLY;INTERVAL=5;BYDAY=MO,FR;UNTIL=20121231T000000Z * SUMMARY:Repeating Event * END:VEVENT * END:VCALENDAR * ``` */ repeating(repeating: ICalRRuleStub | null): this; /** * Set the events repeating options by passing a string which is inserted in the ical file. * @since 2.0.0-develop.5 */ repeating(repeating: string | null): this; /** * @internal */ repeating(repeating: ICalRepeatingOptions | ICalRRuleStub | string | null): this; repeating(repeating?: ICalRepeatingOptions | ICalRRuleStub | string | null): this | ICalEventJSONRepeatingData | ICalRRuleStub | string | null { if (repeating === undefined) { return this.data.repeating; } if (!repeating) { this.data.repeating = null; return this; } if(isRRule(repeating) || typeof repeating === 'string') { this.data.repeating = repeating; return this; } this.data.repeating = { freq: checkEnum(ICalEventRepeatingFreq, repeating.freq) as ICalEventRepeatingFreq }; if (repeating.count) { if (!isFinite(repeating.count)) { throw new Error('`repeating.count` must be a finite number!'); } this.data.repeating.count = repeating.count; } if (repeating.interval) { if (!isFinite(repeating.interval)) { throw new Error('`repeating.interval` must be a finite number!'); } this.data.repeating.interval = repeating.interval; } if (repeating.until !== undefined) { this.data.repeating.until = checkDate(repeating.until, 'repeating.until'); } if (repeating.byDay) { const byDayArray = Array.isArray(repeating.byDay) ? repeating.byDay : [repeating.byDay]; this.data.repeating.byDay = byDayArray.map(day => checkEnum(ICalWeekday, day) as ICalWeekday); } if (repeating.byMonth) { const byMonthArray = Array.isArray(repeating.byMonth) ? repeating.byMonth : [repeating.byMonth]; this.data.repeating.byMonth = byMonthArray.map(month => { if (typeof month !== 'number' || month < 1 || month > 12) { throw new Error('`repeating.byMonth` contains invalid value `' + month + '`!'); } return month; }); } if (repeating.byMonthDay) { const byMonthDayArray = Array.isArray(repeating.byMonthDay) ? repeating.byMonthDay : [repeating.byMonthDay]; this.data.repeating.byMonthDay = byMonthDayArray.map(monthDay => { if (typeof monthDay !== 'number' || monthDay < -31 || monthDay > 31 || monthDay === 0) { throw new Error('`repeating.byMonthDay` contains invalid value `' + monthDay + '`!'); } return monthDay; }); } if (repeating.bySetPos) { if (!this.data.repeating.byDay) { throw '`repeating.bySetPos` must be used along with `repeating.byDay`!'; } const bySetPosArray = Array.isArray(repeating.bySetPos) ? repeating.bySetPos : [repeating.bySetPos]; this.data.repeating.bySetPos = bySetPosArray.map(bySetPos => { if (typeof bySetPos !== 'number' || bySetPos < -366 || bySetPos > 366 || bySetPos === 0) { throw '`repeating.bySetPos` contains invalid value `' + bySetPos + '`!'; } return bySetPos; }); } if (repeating.exclude) { const excludeArray = Array.isArray(repeating.exclude) ? repeating.exclude : [repeating.exclude]; this.data.repeating.exclude = excludeArray.map((exclude, i) => { return checkDate(exclude, `repeating.exclude[${i}]`); }); } if (repeating.startOfWeek) { this.data.repeating.startOfWeek = checkEnum(ICalWeekday, repeating.startOfWeek) as ICalWeekday; } return this; } /** * Get the event's summary * @since 0.2.0 */ summary(): string; /** * Set the event's summary. * Defaults to an empty string if nothing is set. * * @since 0.2.0 */ summary(summary: string): this; summary(summary?: string): this | string { if (summary === undefined) { return this.data.summary; } this.data.summary = summary ? String(summary) : ''; return this; } /** * Get the event's location * @since 0.2.0 */ location(): ICalLocation | null; /** * Set the event's location by passing a string (minimum) or * an {@link ICalLocationWithTitle} object which will also fill the iCal * `GEO` attribute and Apple's `X-APPLE-STRUCTURED-LOCATION`. * * ```javascript * event.location({ * title: 'Apple Store Kurfürstendamm', * address: 'Kurfürstendamm 26, 10719 Berlin, Deutschland', * radius: 141.1751386318387, * geo: { * lat: 52.503630, * lon: 13.328650 * } * }); * ``` * * ```text * LOCATION:Apple Store Kurfürstendamm\nKurfürstendamm 26\, 10719 Berlin\, * Deutschland * X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS=Kurfürstendamm 26\, 10719 * Berlin\, Deutschland;X-APPLE-RADIUS=141.1751386318387;X-TITLE=Apple Store * Kurfürstendamm:geo:52.50363,13.32865 * GEO:52.50363;13.32865 * ``` * * Since v6.1.0 you can also pass a {@link ICalLocationWithoutTitle} object to pass * the geolocation only. This will only fill the iCal `GEO` attribute. * * ```javascript * event.location({ * geo: { * lat: 52.503630, * lon: 13.328650 * } * }); * ``` * * ```text * GEO:52.50363;13.32865 * ``` * * @since 0.2.0 */ location(location: ICalLocation | string | null): this; location(location?: ICalLocation | string | null): this | ICalLocation | null { if (location === undefined) { return this.data.location; } if (typeof location === 'string') { this.data.location = { title: location }; return this; } if (location && ( ('title' in location && !location.title) || (location?.geo && (typeof location.geo.lat !== 'number' || !isFinite(location.geo.lat) || typeof location.geo.lon !== 'number' || !isFinite(location.geo.lon))) || (!('title' in location) && !location?.geo) )) { throw new Error( '`location` isn\'t formatted correctly. See https://sebbo2002.github.io/ical-generator/'+ 'develop/reference/classes/ICalEvent.html#location' ); } this.data.location = location || null; return this; } /** * Get the event's description as an {@link ICalDescription} object. * @since 0.2.0 */ description(): ICalDescription | null; /** * Set the events description by passing a plaintext string or * an object containing both a plaintext and a html description. * Only a few calendar apps support html descriptions and like in * emails, supported HTML tags and styling is limited. * * ```javascript * event.description({ * plain: 'Hello World!', * html: '<p>Hello World!</p>' * }); * ``` * * ```text * DESCRIPTION:Hello World! * X-ALT-DESC;FMTTYPE=text/html:<p>Hello World!</p> * ``` * * @since 0.2.0 */ description(description: ICalDescription | string | null): this; description(description?: ICalDescription | string | null): this | ICalDescription | null { if (description === undefined) { return this.data.description; } if (description === null) { this.data.description = null; return this; } if (typeof description === 'string') { this.data.description = {plain: description}; } else { this.data.description = description; } return this; } /** * Get the event's organizer * @since 0.2.0 */ organizer(): ICalOrganizer | null; /** * Set the event's organizer * * ```javascript * event.organizer({ * name: 'Organizer\'s Name', * email: 'organizer@example.com' * }); * * // OR * * event.organizer('Organizer\'s Name <organizer@example.com>'); * ``` * * You can also add an explicit `mailto` email address or or the sentBy address. * * ```javascript * event.organizer({ * name: 'Organizer\'s Name', * email: 'organizer@example.com', * mailto: 'explicit@mailto.com', * sentBy: 'substitute@example.com' * }) * ``` * * @since 0.2.0 */ organizer(organizer: ICalOrganizer | string | null): this; organizer(organizer?: ICalOrganizer | string | null): this | ICalOrganizer | null { if (organizer === undefined) { return this.data.organizer; } if (organizer === null) { this.data.organizer = null; return this; } this.data.organizer = checkNameAndMail('organizer', organizer); return this; } /** * Creates a new {@link ICalAttendee} and returns it. Use options to prefill * the attendee's attributes. Calling this method without options will create * an empty attendee. * * ```javascript * import ical from 'ical-generator'; * * const cal = ical(); * const event = cal.createEvent({ * start: new Date() * }); * * event.createAttendee({email: 'hui@example.com', name: 'Hui'}); * * // add another attendee * event.createAttendee('Buh <buh@example.net>'); * ``` * * ```text * BEGIN:VCALENDAR * VERSION:2.0 * PRODID:-//sebbo.net//ical-generator//EN * BEGIN:VEVENT * UID:b4944f07-98e4-4581-ac80-2589bb20273d * SEQUENCE:0 * DTSTAMP:20240212T194232Z * DTSTART:20240212T194232Z * SUMMARY: * ATTENDEE;ROLE=REQ-PARTICIPANT;CN="Hui":MAILTO:hui@example.com * ATTENDEE;ROLE=REQ-PARTICIPANT;CN="Buh":MAILTO:buh@example.net * END:VEVENT * END:VCALENDAR * ``` * * As with the organizer, you can also add an explicit `mailto` address. * * ```javascript * event.createAttendee({email: 'hui@example.com', name: 'Hui', mailto: 'another@mailto.com'}); * * // overwrite an attendee's mailto address * attendee.mailto('another@mailto.net'); * ``` * * @since 0.2.0 */ createAttendee(data: ICalAttendee | ICalAttendeeData | string): ICalAttendee { if (data instanceof ICalAttendee) { this.data.attendees.push(data); return data; } if (typeof data === 'string') { data = { email: data, ...checkNameAndMail('data', data) }; } const attendee = new ICalAttendee(data, this); this.data.attendees.push(attendee); return attendee; } /** * Get all attendees * @since 0.2.0 */ attendees(): ICalAttendee[]; /** * Add multiple attendees to your event * * ```javascript * const event = ical().createEvent(); * * cal.attendees([ * {email: 'a@example.com', name: 'Person A'}, * {email: 'b@example.com', name: 'Person B'} * ]); * * cal.attendees(); // --> [ICalAttendee, ICalAttendee] * ``` * * @since 0.2.0 */ attendees(attendees: (ICalAttendee | ICalAttendeeData | string)[]): this; attendees(attendees?: (ICalAttendee | ICalAttendeeData | string)[]): this | ICalAttendee[] { if (!attendees) { return this.data.attendees; } attendees.forEach(attendee => this.createAttendee(attendee)); return this; } /** * Creates a new {@link ICalAlarm} and returns it. Use options to prefill * the alarm's attributes. Calling this method without options will create * an empty alarm. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const alarm = event.createAlarm({type: ICalAlarmType.display, trigger: 300}); * * // add another alarm * event.createAlarm({ * type: ICalAlarmType.audio, * trigger: 300, // 5min before event * }); * ``` * * @since 0.2.1 */ createAlarm(data: ICalAlarm | ICalAlarmData): ICalAlarm { const alarm = data instanceof ICalAlarm ? data : new ICalAlarm(data, this); this.data.alarms.push(alarm); return alarm; } /** * Get all alarms * @since 0.2.0 */ alarms(): ICalAlarm[]; /** * Add one or multiple alarms * * ```javascript * const event = ical().createEvent(); * * cal.alarms([ * {type: ICalAlarmType.display, trigger: 600}, * {type: ICalAlarmType.audio, trigger: 300} * ]); * * cal.alarms(); // --> [ICalAlarm, ICalAlarm] ``` * * @since 0.2.0 */ alarms(alarms: ICalAlarm[] | ICalAlarmData[]): this; alarms(alarms?: ICalAlarm[] | ICalAlarmData[]): this | ICalAlarm[] { if (!alarms) { return this.data.alarms; } alarms.forEach((alarm: ICalAlarm | ICalAlarmData) => this.createAlarm(alarm)); return this; } /** * Creates a new {@link ICalCategory} and returns it. Use options to prefill the category's attributes. * Calling this method without options will create an empty category. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const category = event.createCategory({name: 'APPOINTMENT'}); * * // add another category * event.createCategory({ * name: 'MEETING' * }); * ``` * * @since 0.3.0 */ createCategory(data: ICalCategory | ICalCategoryData): ICalCategory { const category = data instanceof ICalCategory ? data : new ICalCategory(data); this.data.categories.push(category); return category; } /** * Get all categories * @since 0.3.0 */ categories(): ICalCategory[]; /** * Add categories to the event or return all selected categories. * * ```javascript * const event = ical().createEvent(); * * cal.categories([ * {name: 'APPOINTMENT'}, * {name: 'MEETING'} * ]); * * cal.categories(); // --> [ICalCategory, ICalCategory] * ``` * * @since 0.3.0 */ categories(categories: (ICalCategory | ICalCategoryData)[]): this; categories(categories?: (ICalCategory | ICalCategoryData)[]): this | ICalCategory[] { if (!categories) { return this.data.categories; } categories.forEach(category => this.createCategory(category)); return this; } /** * Get the event's status * @since 0.2.0 */ status(): ICalEventStatus | null; /** * Set the event's status * * ```javascript * import ical, {ICalEventStatus} from 'ical-generator'; * event.status(ICalEventStatus.CONFIRMED); * ``` * * @since 0.2.0 */ status(status: ICalEventStatus | null): this; status(status?: ICalEventStatus | null): this | ICalEventStatus | null { if (status === undefined) { return this.data.status; } if (status === null) { this.data.status = null; return this; } this.data.status = checkEnum(ICalEventStatus, status) as ICalEventStatus; return this; } /** * Get the event's busy status * @since 1.0.2 */ busystatus(): ICalEventBusyStatus | null; /** * Set the event's busy status. Will add the * [`X-MICROSOFT-CDO-BUSYSTATUS`](https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/cd68eae7-ed65-4dd3-8ea7-ad585c76c736) * attribute to your event. * * ```javascript * import ical, {ICalEventBusyStatus} from 'ical-generator'; * event.busystatus(ICalEventBusyStatus.BUSY); * ``` * * @since 1.0.2 */ busystatus(busystatus: ICalEventBusyStatus | null): this; busystatus(busystatus?: ICalEventBusyStatus | null): this | ICalEventBusyStatus | null { if (busystatus === undefined) { return this.data.busystatus; } if (busystatus === null) { this.data.busystatus = null; return this; } this.data.busystatus = checkEnum(ICalEventBusyStatus, busystatus) as ICalEventBusyStatus; return this; } /** * Get the event's priority. A value of 1 represents * the highest priority, 9 the lowest. 0 specifies an undefined * priority. * * @since v2.0.0-develop.7 */ priority(): number | null; /** * Set the event's priority. A value of 1 represents * the highest priority, 9 the lowest. 0 specifies an undefined * priority. * * @since v2.0.0-develop.7 */ priority(priority: number | null): this; priority(priority?: number | null): this | number | null { if (priority === undefined) { return this.data.priority; } if (priority === null) { this.data.priority = null; return this; } if(priority < 0 || priority > 9) { throw new Error('`priority` is invalid, musst be 0 ≤ priority ≤ 9.'); } this.data.priority = Math.round(priority); return this; } /** * Get the event's URL * @since 0.2.0 */ url(): string | null; /** * Set the event's URL * @since 0.2.0 */ url(url: string | null): this; url(url?: string | null): this | string | null { if (url === undefined) { return this.data.url; } this.data.url = url ? String(url) : null; return this; } /** * Adds an attachment to the event by adding the file URL to the calendar. * * `ical-generator` only supports external attachments. File attachments that * are directly included in the file are not supported, because otherwise the * calendar file could easily become unfavourably large. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * event.createAttachment('https://files.sebbo.net/calendar/attachments/foo'); * ``` * * @since 3.2.0-develop.1 */ createAttachment(url: string): this { this.data.attachments.push(url); return this; } /** * Get all attachment urls * @since 3.2.0-develop.1 */ attachments(): string[]; /** * Add one or multiple alarms * * ```javascript * const event = ical().createEvent(); * * cal.attachments([ * 'https://files.sebbo.net/calendar/attachments/foo', * 'https://files.sebbo.net/calendar/attachments/bar' * ]); * * cal.attachments(); // --> [string, string] ``` * * 3.2.0-develop.1 */ attachments(attachments: string[]): this; attachments(attachments?: string[]): this | string[] { if (!attachments) { return this.data.attachments; } attachments.forEach((attachment: string) => this.createAttachment(attachment)); return this; } /** * Get the event's transparency * @since 1.7.3 */ transparency(): ICalEventTransparency | null; /** * Set the event's transparency * * Set the field to `OPAQUE` if the person or resource is no longer * available due to this event. If the calendar entry has no influence * on availability, you can set the field to `TRANSPARENT`. This value * is mostly used to find out if a person has time on a certain date or * not (see `TRANSP` in iCal specification). * * ```javascript * import ical, {ICalEventTransparency} from 'ical-generator'; * event.transparency(ICalEventTransparency.OPAQUE); * ``` * * @since 1.7.3 */ transparency(transparency: ICalEventTransparency | null): this; transparency(transparency?: ICalEventTransparency | null): this | ICalEventTransparency | null { if (transparency === undefined) { return this.data.transparency; } if (!transparency) { this.data.transparency = null; return this; } this.data.transparency = checkEnum(ICalEventTransparency, transparency) as ICalEventTransparency; return this; } /** * Get the event's creation date * @since 0.3.0 */ created(): ICalDateTimeValue | null; /** * Set the event's creation date * @since 0.3.0 */ created(created: ICalDateTimeValue | null): this; created(created?: ICalDateTimeValue | null): this | ICalDateTimeValue | null { if (created === undefined) { return this.data.created; } if (created === null) { this.data.created = null; return this; } this.data.created = checkDate(created, 'created'); return this; } /** * Get the event's last modification date * @since 0.3.0 */ lastModified(): ICalDateTimeValue | null; /** * Set the event's last modification date * @since 0.3.0 */ lastModified(lastModified: ICalDateTimeValue | null): this; lastModified(lastModified?: ICalDateTimeValue | null): this | ICalDateTimeValue | null { if (lastModified === undefined) { return this.data.lastModified; } if (lastModified === null) { this.data.lastModified = null; return this; } this.data.lastModified = checkDate(lastModified, 'lastModified'); return this; } /** * Get the event's class * @since 2.0.0 */ class(): ICalEventClass | null; /** * Set the event's class * * ```javascript * import ical, { ICalEventClass } from 'ical-generator'; * event.class(ICalEventClass.PRIVATE); * ``` * * @since 2.0.0 */ class(class_: ICalEventClass | null): this; class(class_?: ICalEventClass | null): this | ICalEventClass | null { if (class_ === undefined) { return this.data.class; } if (class_ === null) { this.data.class = null; return this; } this.data.class = checkEnum(ICalEventClass, class_) as ICalEventClass; return this; } /** * Set X-* attributes. Woun't filter double attributes, * which are also added by another method (e.g. summary), * so these attributes may be inserted twice. * * ```javascript * event.x([ * { * key: "X-MY-CUSTOM-ATTR", * value: "1337!" * } * ]); * * event.x([ * ["X-MY-CUSTOM-ATTR", "1337!"] * ]); * * event.x({ * "X-MY-CUSTOM-ATTR": "1337!" * }); * ``` * * @since 1.9.0 */ x (keyOrArray: {key: string, value: string}[] | [string, string][] | Record<string, string>): this; /** * Set a X-* attribute. Woun't filter double attributes, * which are also added by another method (e.g. summary), * so these attributes may be inserted twice. * * ```javascript * event.x("X-MY-CUSTOM-ATTR", "1337!"); * ``` * * @since 1.9.0 */ x (keyOrArray: string, value: string): this; /** * Get all custom X-* attributes. * @since 1.9.0 */ x (): {key: string, value: string}[]; x(keyOrArray?: ({ key: string, value: string })[] | [string, string][] | Record<string, string> | string, value?: string): this | void | ({ key: string, value: string })[] { if (keyOrArray === undefined) { return addOrGetCustomAttributes(this.data); } if (typeof keyOrArray === 'string' && typeof value === 'string') { addOrGetCustomAttributes(this.data, keyOrArray, value); } if (typeof keyOrArray === 'object') { addOrGetCustomAttributes(this.data, keyOrArray); } return this; } /** * Return a shallow copy of the events's options for JSON stringification. * Third party objects like moment.js values or RRule objects are stringified * as well. Can be used for persistence. * * ```javascript * const event = ical().createEvent(); * const json = JSON.stringify(event); * * // later: restore event data * const calendar = ical().createEvent(JSON.parse(json)); * ``` * * @since 0.2.4 */ toJSON(): ICalEventJSONData { let repeating: ICalEventJSONRepeatingData | string | null = null; if(isRRule(this.data.repeating) || typeof this.data.repeating === 'string') { repeating = this.data.repeating.toString(); } else if(this.data.repeating) { repeating = Object.assign({}, this.data.repeating, { until: toJSON(this.data.repeating.until) || undefined, exclude: this.data.repeating.exclude?.map(d => toJSON(d)), }); } this.swapStartAndEndIfRequired(); return Object.assign({}, this.data, { start: toJSON(this.data.start) || null, end: toJSON(this.data.end) || null, recurrenceId: toJSON(this.data.recurrenceId) || null, stamp: toJSON(this.data.stamp) || null, created: toJSON(this.data.created) || null, lastModified: toJSON(this.data.lastModified) || null, repeating, x: this.x() }); } /** * Return generated event as a string. * * ```javascript * const event = ical().createEvent(); * console.log(event.toString()); // → BEGIN:VEVENT… * ``` */ toString(): string { let g = ''; // DATE & TIME g += 'BEGIN:VEVENT\r\n'; g += 'UID:' + this.data.id + '\r\n'; // SEQUENCE g += 'SEQUENCE:' + this.data.sequence + '\r\n'; this.swapStartAndEndIfRequired(); g += 'DTSTAMP:' + formatDate(this.calendar.timezone(), this.data.stamp) + '\r\n'; if (this.data.allDay) { g += 'DTSTART;VALUE=DATE:' + formatDate(this.timezone(), this.data.start, true) + '\r\n'; if (this.data.end) { g += 'DTEND;VALUE=DATE:' + formatDate(this.timezone(), this.data.end, true) + '\r\n'; } g += 'X-MICROSOFT-CDO-ALLDAYEVENT:TRUE\r\n'; g += 'X-MICROSOFT-MSNCALENDAR-ALLDAYEVENT:TRUE\r\n'; } else { g += formatDateTZ(this.timezone(), 'DTSTART', this.data.start, this.data) + '\r\n'; if (this.data.end) { g += formatDateTZ(this.timezone(), 'DTEND', this.data.end, this.data) + '\r\n'; } } // REPEATING if(isRRule(this.data.repeating) || typeof this.data.repeating === 'string') { let repeating = this.data.repeating .toString() .replace(/\r\n/g, '\n') .split('\n') .filter(l => l && !l.startsWith('DTSTART:')) .join('\r\n');