UNPKG

ical-generator

Version:

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

794 lines (736 loc) 23.5 kB
'use strict'; import type ICalEvent from './event.ts'; import ICalAttendee, { type ICalAttendeeData } from './attendee.ts'; import { addOrGetCustomAttributes, checkDate, checkNameAndMail, escape, formatDate, generateCustomAttributes, toDurationString, toJSON, } from './tools.ts'; import { type ICalDateTimeValue } from './types.ts'; export enum ICalAlarmType { audio = 'audio', display = 'display', email = 'email', } export const ICalAlarmRelatesTo = { end: 'END', start: 'START', } as const; export interface ICalAlarmBaseData { attach?: ICalAttachment | null | string; attendees?: ICalAttendee[] | ICalAttendeeData[]; description?: null | string; relatesTo?: ICalAlarmRelatesTo | null; repeat?: ICalAlarmRepeatData | null; summary?: null | string; type?: ICalAlarmType; x?: | [string, string][] | Record<string, string> | { key: string; value: string }[]; } export type ICalAlarmData = | ICalAlarmBaseData | ICalAlarmTriggerAfterData | ICalAlarmTriggerBeforeData | ICalAlarmTriggerData; export interface ICalAlarmJSONData { attach: ICalAttachment | null; attendees: ICalAttendee[]; description: null | string; interval: null | number; relatesTo: ICalAlarmRelatesTo | null; repeat: ICalAlarmRepeatData | null; summary: null | string; trigger: number | string; type: ICalAlarmType; x: { key: string; value: string }[]; } export type ICalAlarmRelatesTo = (typeof ICalAlarmRelatesTo)[keyof typeof ICalAlarmRelatesTo]; export interface ICalAlarmRepeatData { interval: number; times: number; } export type ICalAlarmTriggerAfterData = ICalAlarmBaseData & { triggerAfter: ICalDateTimeValue | number; }; export type ICalAlarmTriggerBeforeData = ICalAlarmBaseData & { triggerBefore: ICalDateTimeValue | number; }; export type ICalAlarmTriggerData = ICalAlarmBaseData & { trigger: ICalDateTimeValue | number; }; export type ICalAlarmTypeValue = keyof ICalAlarmType; export interface ICalAttachment { mime: null | string; uri: string; } interface ICalInternalAlarmData { attach: ICalAttachment | null; attendees: ICalAttendee[]; description: null | string; interval: null | number; relatesTo: ICalAlarmRelatesTo | null; repeat: ICalAlarmRepeatData | null; summary: null | string; trigger: ICalDateTimeValue | number; type: ICalAlarmType; x: [string, string][]; } /** * Usually you get an {@link ICalAlarm} object like this: * * ```javascript * import ical from 'ical-generator'; * const calendar = ical(); * const event = calendar.createEvent(); * const alarm = event.createAlarm(); * ``` * * You can also use the {@link ICalAlarm} object directly: * * ```javascript * import ical, {ICalAlarm} from 'ical-generator'; * const alarm = new ICalAlarm(); * event.alarms([alarm]); * ``` */ export default class ICalAlarm { private readonly data: ICalInternalAlarmData; private readonly event: ICalEvent; /** * Constructor of {@link ICalAttendee}. The event reference is required * to query the calendar's timezone and summary when required. * * @param data Alarm Data * @param event Reference to ICalEvent object */ constructor(data: ICalAlarmData, event: ICalEvent) { this.data = { attach: null, attendees: [], description: null, interval: null, relatesTo: null, repeat: null, summary: null, trigger: -600, type: ICalAlarmType.display, x: [], }; this.event = event; if (!event) { throw new Error('`event` option required!'); } if (data.type !== undefined) this.type(data.type); if ('trigger' in data && data.trigger !== undefined) this.trigger(data.trigger); if ('triggerBefore' in data && data.triggerBefore !== undefined) this.triggerBefore(data.triggerBefore); if ('triggerAfter' in data && data.triggerAfter !== undefined) this.triggerAfter(data.triggerAfter); if (data.repeat) this.repeat(data.repeat); if (data.attach !== undefined) this.attach(data.attach); if (data.description !== undefined) this.description(data.description); if (data.summary !== undefined) this.summary(data.summary); if (data.attendees !== undefined) this.attendees(data.attendees); if (data.x !== undefined) this.x(data.x); } /** * Get Attachment * @since 0.2.1 */ attach(): null | { mime: null | string; uri: string }; /** * Set Alarm attachment. Used to set the alarm sound * if alarm type is audio. Defaults to "Basso". * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * * event.createAlarm({ * attach: 'https://example.com/notification.aud' * }); * * // OR * * event.createAlarm({ * attach: { * uri: 'https://example.com/notification.aud', * mime: 'audio/basic' * } * }); * ``` * * @since 0.2.1 */ attach( attachment: null | string | { mime?: null | string; uri: string }, ): this; attach( attachment?: null | string | { mime?: null | string; uri: string }, ): null | this | { mime: null | string; uri: string } { if (attachment === undefined) { return this.data.attach; } if (!attachment) { this.data.attach = null; return this; } let _attach: ICalAttachment | undefined; if (typeof attachment === 'string') { _attach = { mime: null, uri: attachment, }; } else if (typeof attachment === 'object') { _attach = { mime: attachment.mime || null, uri: attachment.uri, }; } else { throw new Error( '`attachment` needs to be a valid formed string or an object. See https://sebbo2002.github.io/' + 'ical-generator/develop/reference/classes/ICalAlarm.html#attach', ); } if (!_attach.uri) { throw new Error('`attach.uri` is empty!'); } this.data.attach = { mime: _attach.mime, uri: _attach.uri, }; return this; } /** * Get all attendees * @since 7.0.0 */ attendees(): ICalAttendee[]; /** * Add multiple attendees to your event * * @since 7.0.0 */ attendees(attendees: (ICalAttendee | ICalAttendeeData | string)[]): this; attendees( attendees?: (ICalAttendee | ICalAttendeeData | string)[], ): ICalAttendee[] | this { if (!attendees) { return this.data.attendees; } attendees.forEach((attendee) => this.createAttendee(attendee)); 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. * * @since 7.0.0 */ createAttendee( data: ICalAttendee | ICalAttendeeData | string, ): ICalAttendee { if (data instanceof ICalAttendee) { this.data.attendees.push(data); return data; } if (typeof data === 'string') { data = checkNameAndMail('data', data); } const attendee = new ICalAttendee(data, this); this.data.attendees.push(attendee); return attendee; } /** * Get the alarm description. Used to set the alarm message * if alarm type is `display`. If the alarm type is `email`, it's * used to set the email body. Defaults to the event's summary. * * @since 0.2.1 */ description(): null | string; /** * Set the alarm description. Used to set the alarm message * if alarm type is `display`. If the alarm type is `email`, it's * used to set the email body. Defaults to the event's summary. * * @since 0.2.1 */ description(description: null | string): this; description(description?: null | string): null | string | this { if (description === undefined) { return this.data.description; } if (!description) { this.data.description = null; return this; } this.data.description = description; return this; } /** * Get to which time alarm trigger relates to. * Can be either `START` or `END`. If the value is * `START` the alarm is triggerd relative to the event start time. * If the value is `END` the alarm is triggerd relative to the event end time * * @since 4.0.1 */ relatesTo(): ICalAlarmRelatesTo | null; /** * Use this method to set to which time alarm trigger relates to. * Works only if trigger is a `number` * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const alarm = cal.createAlarm(); * * alarm.trigger(600); // -> 10 minutes before event starts * * alarm.relatesTo('START'); // -> 10 minutes before event starts * alarm.relatesTo('END'); // -> 10 minutes before event ends * * alarm.trigger(-600); // -> 10 minutes after event starts * * alarm.relatesTo('START'); // -> 10 minutes after event starts * alarm.relatesTo('END'); // -> 10 minutes after event ends * ``` * @since 4.0.1 */ relatesTo(relatesTo: ICalAlarmRelatesTo | null): this; relatesTo( relatesTo?: ICalAlarmRelatesTo | null, ): ICalAlarmRelatesTo | null | this { if (relatesTo === undefined) { return this.data.relatesTo; } if (!relatesTo) { this.data.relatesTo = null; return this; } if (!Object.values(ICalAlarmRelatesTo).includes(relatesTo)) { throw new Error( '`relatesTo` is not correct, must be either `START` or `END`!', ); } this.data.relatesTo = relatesTo; return this; } /** * Get Alarm Repetitions * @since 0.2.1 */ repeat(): ICalAlarmRepeatData | null; /** * Set Alarm Repetitions. Use this to repeat the alarm. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * * // repeat the alarm 4 times every 5 minutes… * cal.createAlarm({ * repeat: { * times: 4, * interval: 300 * } * }); * ``` * * @since 0.2.1 */ repeat(repeat: ICalAlarmRepeatData | null): this; repeat( repeat?: ICalAlarmRepeatData | null, ): ICalAlarmRepeatData | null | this { if (repeat === undefined) { return this.data.repeat; } if (!repeat) { this.data.repeat = null; return this; } if (typeof repeat !== 'object') { throw new Error('`repeat` is not correct, must be an object!'); } if (typeof repeat.times !== 'number' || !isFinite(repeat.times)) { throw new Error('`repeat.times` is not correct, must be numeric!'); } if (typeof repeat.interval !== 'number' || !isFinite(repeat.interval)) { throw new Error( '`repeat.interval` is not correct, must be numeric!', ); } this.data.repeat = repeat; return this; } /** * Get the alarm summary. Used to set the email subject * if alarm type is `email`. Defaults to the event's summary. * * @since 7.0.0 */ summary(): null | string; /** * Set the alarm summary. Used to set the email subject * if alarm type is display. Defaults to the event's summary. * * @since 0.2.1 */ summary(summary: null | string): this; summary(summary?: null | string): null | string | this { if (summary === undefined) { return this.data.summary; } if (!summary) { this.data.summary = null; return this; } this.data.summary = summary; return this; } /** * Return a shallow copy of the alarm's options for JSON stringification. * Third party objects like moment.js values are stringified as well. Can * be used for persistence. * * @since 0.2.4 */ toJSON(): ICalAlarmJSONData { const trigger = this.trigger(); return Object.assign({}, this.data, { trigger: typeof trigger === 'number' ? trigger : toJSON(trigger), x: this.x(), }); } /** * Return generated event as a string. * * ```javascript * const alarm = event.createAlarm(); * console.log(alarm.toString()); // → BEGIN:VALARM… * ``` */ toString(): string { let g = 'BEGIN:VALARM\r\n'; // ACTION g += 'ACTION:' + this.data.type.toUpperCase() + '\r\n'; if ( typeof this.data.trigger === 'number' && this.data.relatesTo === null ) { if (this.data.trigger > 0) { g += 'TRIGGER;RELATED=END:' + toDurationString(this.data.trigger) + '\r\n'; } else { g += 'TRIGGER:' + toDurationString(this.data.trigger) + '\r\n'; } } else if (typeof this.data.trigger === 'number') { g += 'TRIGGER;RELATED=' + this.data.relatesTo?.toUpperCase() + ':' + toDurationString(this.data.trigger) + '\r\n'; } else { g += 'TRIGGER;VALUE=DATE-TIME:' + formatDate(this.event.timezone(), this.data.trigger) + '\r\n'; } // REPEAT if (this.data.repeat) { if (!this.data.repeat.times) { throw new Error( 'No value for `repeat.times` in ICalAlarm given, but required for `interval`!', ); } if (!this.data.repeat.interval) { throw new Error( 'No value for `repeat.interval` in ICalAlarm given, but required for `repeat`!', ); } g += 'REPEAT:' + this.data.repeat.times + '\r\n'; g += 'DURATION:' + toDurationString(this.data.repeat.interval) + '\r\n'; } // ATTACH if ( this.data.type === 'audio' && this.data.attach && this.data.attach.mime ) { g += 'ATTACH;FMTTYPE=' + escape(this.data.attach.mime, false) + ':' + escape(this.data.attach.uri, false) + '\r\n'; } else if (this.data.type === 'audio' && this.data.attach) { g += 'ATTACH;VALUE=URI:' + escape(this.data.attach.uri, false) + '\r\n'; } else if (this.data.type === 'audio') { g += 'ATTACH;VALUE=URI:Basso\r\n'; } // DESCRIPTION if (this.data.type !== 'audio' && this.data.description) { g += 'DESCRIPTION:' + escape(this.data.description, false) + '\r\n'; } else if (this.data.type !== 'audio') { g += 'DESCRIPTION:' + escape(this.event.summary(), false) + '\r\n'; } // SUMMARY if (this.data.type === 'email' && this.data.summary) { g += 'SUMMARY:' + escape(this.data.summary, false) + '\r\n'; } else if (this.data.type === 'email') { g += 'SUMMARY:' + escape(this.event.summary(), false) + '\r\n'; } // ATTENDEES if (this.data.type === 'email') { this.data.attendees.forEach((attendee) => { g += attendee.toString(); }); } // CUSTOM X ATTRIBUTES g += generateCustomAttributes(this.data); g += 'END:VALARM\r\n'; return g; } /** * Get the trigger time for the alarm. Can either * be a date and time value ({@link ICalDateTimeValue}) or * a number, which will represent the seconds between * alarm and event start. The number is negative, if the * alarm is triggered after the event started. * * @since 0.2.1 */ trigger(): ICalDateTimeValue | number; /** * Use this method to set the alarm time. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const alarm = cal.createAlarm(); * * alarm.trigger(600); // -> 10 minutes before event starts * alarm.trigger(new Date()); // -> now * ``` * * 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.1 */ trigger(trigger: Date | ICalDateTimeValue | number): this; trigger( trigger?: Date | ICalDateTimeValue | number, ): ICalDateTimeValue | number | this { // Getter if (trigger === undefined && typeof this.data.trigger === 'number') { return -1 * this.data.trigger; } if (trigger === undefined) { return this.data.trigger; } // Setter if (typeof trigger === 'number' && isFinite(trigger)) { this.data.trigger = -1 * trigger; } else if (!trigger || typeof trigger === 'number') { throw new Error( '`trigger` is not correct, must be a finite number or a supported date!', ); } else { this.data.trigger = checkDate(trigger, 'trigger'); } return this; } /** * Get the trigger time for the alarm. Can either * be a date and time value ({@link ICalDateTimeValue}) or * a number, which will represent the seconds between * alarm and event start. The number is negative, if the * alarm is triggered before the event started. * * @since 0.2.1 */ triggerAfter(): ICalDateTimeValue | number; /** * Use this method to set the alarm time. Unlike `trigger`, this time * the alarm takes place after the event has started. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const alarm = cal.createAlarm(); * * alarm.trigger(600); // -> 10 minutes after event starts * ``` * * 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.1 */ triggerAfter(trigger: ICalDateTimeValue | number): this; triggerAfter( trigger?: ICalDateTimeValue | number, ): ICalDateTimeValue | number | this { if (trigger === undefined) { return this.data.trigger; } return this.trigger( typeof trigger === 'number' ? -1 * trigger : trigger, ); } /** * Get the trigger time for the alarm. Can either * be a date and time value ({@link ICalDateTimeValue}) or * a number, which will represent the seconds between * alarm and event start. The number is negative, if the * alarm is triggered after the event started. * * @since 0.2.1 * @see {@link trigger} * @see {@link triggerAfter} */ triggerBefore(trigger: ICalDateTimeValue | number): this; /** * Use this method to set the alarm time. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const alarm = cal.createAlarm(); * * alarm.trigger(600); // -> 10 minutes before event starts * alarm.trigger(new Date()); // -> now * ``` * * 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.1 * @see {@link trigger} * @see {@link triggerAfter} */ triggerBefore(): ICalDateTimeValue | number; triggerBefore( trigger?: ICalDateTimeValue | number, ): ICalDateTimeValue | number | this { if (trigger === undefined) { return this.trigger(); } return this.trigger(trigger); } /** * Get the alarm type * @since 0.2.1 */ type(type: ICalAlarmType): this; /** * Set the alarm type. See {@link ICalAlarmType} * for available status options. * @since 0.2.1 */ type(): ICalAlarmType; type(type?: ICalAlarmType): ICalAlarmType | this { if (type === undefined) { return this.data.type; } if (!type || !Object.keys(ICalAlarmType).includes(type)) { throw new Error( '`type` is not correct, must be either `display` or `audio`!', ); } this.data.type = type; return this; } /** * Set X-* attributes. Woun't filter double attributes, * which are also added by another method (e.g. type), * so these attributes may be inserted twice. * * ```javascript * alarm.x([ * { * key: "X-MY-CUSTOM-ATTR", * value: "1337!" * } * ]); * * alarm.x([ * ["X-MY-CUSTOM-ATTR", "1337!"] * ]); * * alarm.x({ * "X-MY-CUSTOM-ATTR": "1337!" * }); * ``` * * @since 1.9.0 */ x( keyOrArray: | [string, string][] | Record<string, string> | { key: string; value: string }[], ): this; /** * Set a X-* attribute. Woun't filter double attributes, * which are also added by another method (e.g. type), * so these attributes may be inserted twice. * * ```javascript * alarm.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?: | [string, string][] | Record<string, string> | string | { key: string; value: 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); } else if (typeof keyOrArray === 'object') { addOrGetCustomAttributes(this.data, keyOrArray); } else { throw new Error('Either key or value is not a string!'); } return this; } }