ical-generator
Version:
ical-generator is a small piece of code which generates ical calendar files
801 lines (748 loc) • 22.1 kB
text/typescript
'use strict';
import ICalEvent, {
type ICalEventData,
type ICalEventJSONData,
} from './event.ts';
import {
addOrGetCustomAttributes,
checkEnum,
foldLines,
generateCustomAttributes,
isMomentDuration,
toDurationString,
} from './tools.ts';
import { type ICalMomentDurationStub, type ICalTimezone } from './types.ts';
export enum ICalCalendarMethod {
ADD = 'ADD',
CANCEL = 'CANCEL',
COUNTER = 'COUNTER',
DECLINECOUNTER = 'DECLINECOUNTER',
PUBLISH = 'PUBLISH',
REFRESH = 'REFRESH',
REPLY = 'REPLY',
REQUEST = 'REQUEST',
}
export interface ICalCalendarData {
description?: null | string;
events?: (ICalEvent | ICalEventData)[];
method?: ICalCalendarMethod | null;
name?: null | string;
prodId?: ICalCalendarProdIdData | string;
scale?: null | string;
source?: null | string;
timezone?: ICalTimezone | null | string;
ttl?: ICalMomentDurationStub | null | number;
url?: null | string;
x?:
| [string, string][]
| Record<string, string>
| { key: string; value: string }[];
}
export interface ICalCalendarJSONData {
description: null | string;
events: ICalEventJSONData[];
method: ICalCalendarMethod | null;
name: null | string;
prodId: string;
scale: null | string;
source: null | string;
timezone: null | string;
ttl: null | number;
url: null | string;
x: { key: string; value: string }[];
}
export interface ICalCalendarProdIdData {
company: string;
language?: string;
product: string;
}
interface ICalCalendarInternalData {
description: null | string;
events: ICalEvent[];
method: ICalCalendarMethod | null;
name: null | string;
prodId: string;
scale: null | string;
source: null | string;
timezone: ICalTimezone | null;
ttl: null | number;
url: null | string;
x: [string, string][];
}
/**
* Usually you get an {@link ICalCalendar} object like this:
* ```javascript
* import ical from 'ical-generator';
* const calendar = ical();
* ```
*
* But you can also use the constructor directly like this:
* ```javascript
* import {ICalCalendar} from 'ical-generator';
* const calendar = new ICalCalendar();
* ```
*/
export default class ICalCalendar {
private readonly data: ICalCalendarInternalData;
/**
* You can pass options to set up your calendar or use setters to do this.
*
* ```javascript
* * import ical from 'ical-generator';
*
* // or use require:
* // const { default: ical } = require('ical-generator');
*
*
* const cal = ical({name: 'my first iCal'});
*
* // is the same as
*
* const cal = ical().name('my first iCal');
*
* // is the same as
*
* const cal = ical();
* cal.name('sebbo.net');
* ```
*
* `cal.toString()` would then produce the following string:
* ```text
* BEGIN:VCALENDAR
* VERSION:2.0
* PRODID:-//sebbo.net//ical-generator//EN
* NAME:sebbo.net
* X-WR-CALNAME:sebbo.net
* END:VCALENDAR
* ```
*
* @param data Calendar data
*/
constructor(data: ICalCalendarData = {}) {
this.data = {
description: null,
events: [],
method: null,
name: null,
prodId: '//sebbo.net//ical-generator//EN',
scale: null,
source: null,
timezone: null,
ttl: null,
url: null,
x: [],
};
if (data.prodId !== undefined) this.prodId(data.prodId);
if (data.method !== undefined) this.method(data.method);
if (data.name !== undefined) this.name(data.name);
if (data.description !== undefined) this.description(data.description);
if (data.timezone !== undefined) this.timezone(data.timezone);
if (data.source !== undefined) this.source(data.source);
if (data.url !== undefined) this.url(data.url);
if (data.scale !== undefined) this.scale(data.scale);
if (data.ttl !== undefined) this.ttl(data.ttl);
if (data.events !== undefined) this.events(data.events);
if (data.x !== undefined) this.x(data.x);
}
/**
* Remove all events from the calendar without
* touching any other data like name or prodId.
*
* @since 2.0.0-develop.1
*/
clear(): this {
this.data.events = [];
return this;
}
/**
* Creates a new {@link ICalEvent} and returns it. Use options to prefill the event's attributes.
* Calling this method without options will create an empty event.
*
* ```javascript
* import ical from 'ical-generator';
*
* // or use require:
* // const { default: ical } = require('ical-generator');
*
* const cal = ical();
* const event = cal.createEvent({summary: 'My Event'});
*
* // overwrite event summary
* event.summary('Your Event');
* ```
*
* @since 0.2.0
*/
createEvent(data: ICalEvent | ICalEventData): ICalEvent {
const event =
data instanceof ICalEvent ? data : new ICalEvent(data, this);
this.data.events.push(event);
return event;
}
/**
* Get your feed's description
* @since 0.2.7
*/
description(): null | string;
/**
* Set your feed's description
* @since 0.2.7
*/
description(description: null | string): this;
description(description?: null | string): null | string | this {
if (description === undefined) {
return this.data.description;
}
this.data.description = description ? String(description) : null;
return this;
}
/**
* Returns all events of this calendar.
*
* ```javascript
* const cal = ical();
*
* cal.events([
* {
* start: new Date(),
* end: new Date(new Date().getTime() + 3600000),
* summary: 'Example Event',
* description: 'It works ;)',
* url: 'http://sebbo.net/'
* }
* ]);
*
* cal.events(); // --> [ICalEvent]
* ```
*
* @since 0.2.0
*/
events(): ICalEvent[];
/**
* Add multiple events to your calendar.
*
* ```javascript
* const cal = ical();
*
* cal.events([
* {
* start: new Date(),
* end: new Date(new Date().getTime() + 3600000),
* summary: 'Example Event',
* description: 'It works ;)',
* url: 'http://sebbo.net/'
* }
* ]);
*
* cal.events(); // --> [ICalEvent]
* ```
*
* @since 0.2.0
*/
events(events: (ICalEvent | ICalEventData)[]): this;
events(events?: (ICalEvent | ICalEventData)[]): ICalEvent[] | this {
if (!events) {
return this.data.events;
}
events.forEach((e: ICalEvent | ICalEventData) => this.createEvent(e));
return this;
}
/**
* Get the number of events added to your calendar
*/
length(): number {
return this.data.events.length;
}
/**
* Get the feed method attribute.
* See {@link ICalCalendarMethod} for possible results.
*
* @since 0.2.8
*/
method(): ICalCalendarMethod | null;
/**
* Set the feed method attribute.
* See {@link ICalCalendarMethod} for available options.
*
* #### Typescript Example
* ```typescript
* import {ICalCalendarMethod} from 'ical-generator';
*
* // METHOD:PUBLISH
* calendar.method(ICalCalendarMethod.PUBLISH);
* ```
*
* @since 0.2.8
*/
method(method: ICalCalendarMethod | null): this;
method(
method?: ICalCalendarMethod | null,
): ICalCalendarMethod | null | this {
if (method === undefined) {
return this.data.method;
}
if (!method) {
this.data.method = null;
return this;
}
this.data.method = checkEnum(
ICalCalendarMethod,
method,
) as ICalCalendarMethod;
return this;
}
/**
* Get your feed's name
* @since 0.2.0
*/
name(): null | string;
/**
* Set your feed's name. Is used to fill `NAME`
* and `X-WR-CALNAME` in your iCal file.
*
* ```typescript
* import ical from 'ical-generator';
*
* const cal = ical();
* cal.name('Next Arrivals');
*
* cal.toString();
* ```
*
* ```text
* BEGIN:VCALENDAR
* VERSION:2.0
* PRODID:-//sebbo.net//ical-generator//EN
* NAME:Next Arrivals
* X-WR-CALNAME:Next Arrivals
* END:VCALENDAR
* ```
*
* @since 0.2.0
*/
name(name: null | string): this;
name(name?: null | string): null | string | this {
if (name === undefined) {
return this.data.name;
}
this.data.name = name ? String(name) : null;
return this;
}
/**
* Get your feed's prodid. Will always return a string.
* @since 0.2.0
*/
prodId(): string;
/**
* Set your feed's prodid. `prodid` can be either a
* string like `//sebbo.net//ical-generator//EN` or a
* valid {@link ICalCalendarProdIdData} object. `language`
* is optional and defaults to `EN`.
*
* ```javascript
* cal.prodId({
* company: 'My Company',
* product: 'My Product',
* language: 'EN' // optional, defaults to EN
* });
* ```
*
* `cal.toString()` would then produce the following string:
* ```text
* PRODID:-//My Company//My Product//EN
* ```
*
* @since 0.2.0
*/
prodId(prodId: ICalCalendarProdIdData | string): this;
prodId(prodId?: ICalCalendarProdIdData | string): string | this {
if (!prodId) {
return this.data.prodId;
}
if (typeof prodId === 'string') {
this.data.prodId = prodId;
return this;
}
if (typeof prodId !== 'object') {
throw new Error('`prodid` needs to be a string or an object!');
}
if (!prodId.company) {
throw new Error('`prodid.company` is a mandatory item!');
}
if (!prodId.product) {
throw new Error('`prodid.product` is a mandatory item!');
}
const language = (prodId.language || 'EN').toUpperCase();
this.data.prodId =
'//' + prodId.company + '//' + prodId.product + '//' + language;
return this;
}
/**
* Get current value of the `CALSCALE` attribute. It will
* return `null` if no value was set. The iCal standard
* specifies this as `GREGORIAN` if no value is present.
*
* @since 1.8.0
*/
scale(): null | string;
/**
* Use this method to set your feed's `CALSCALE` attribute. There is no
* default value for this property and it will not appear in your iCal
* file unless set. The iCal standard specifies this as `GREGORIAN` if
* no value is present.
*
* ```javascript
* cal.scale('gregorian');
* ```
*
* @since 1.8.0
*/
scale(scale: null | string): this;
scale(scale?: null | string): null | string | this {
if (scale === undefined) {
return this.data.scale;
}
if (scale === null) {
this.data.scale = null;
} else {
this.data.scale = scale.toUpperCase();
}
return this;
}
/**
* Get current value of the `SOURCE` attribute.
* @since 2.2.0-develop.1
*/
source(): null | string;
/**
* Use this method to set your feed's `SOURCE` attribute.
* This tells the client where to refresh your feed.
*
* ```javascript
* cal.source('http://example.com/my/original_source.ical');
* ```
*
* ```text
* SOURCE;VALUE=URI:http://example.com/my/original_source.ical
* ```
*
* @since 2.2.0-develop.1
*/
source(source: null | string): this;
source(source?: null | string): null | string | this {
if (source === undefined) {
return this.data.source;
}
this.data.source = source || null;
return this;
}
/**
* Get the current calendar timezone
* @since 0.2.0
*/
timezone(): null | string;
/**
* Use this method to set your feed's timezone. Is used
* to fill `TIMEZONE-ID` and `X-WR-TIMEZONE` in your iCal export.
* Please not that all date values are treaded differently, if
* a timezone was set. See {@link formatDate} for details. If no
* time zone is specified, all information is output as UTC.
*
* ```javascript
* cal.timezone('America/New_York');
* ```
*
* @see https://github.com/sebbo2002/ical-generator#-date-time--timezones
* @since 0.2.0
*/
timezone(timezone: null | string): this;
/**
* Sets the time zone to be used in this calendar file for all times of all
* events. 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.
*
* For the best support of time zones, a VTimezone entry in the calendar is
* recommended, which informs the client about the corresponding time zones
* (daylight saving time, deviation from UTC, etc.). `ical-generator` itself
* does not have a time zone database, so an external generator is needed here.
*
* A VTimezone generator is a function that takes a time zone as a string and
* returns a VTimezone component according to the ical standard. For example,
* ical-timezones can be used for this:
*
* ```typescript
* import ical from 'ical-generator';
* import {getVtimezoneComponent} from '@touch4it/ical-timezones';
*
* const cal = ical();
* cal.timezone({
* name: 'FOO',
* generator: getVtimezoneComponent
* });
* cal.createEvent({
* start: new Date(),
* timezone: 'Europe/London'
* });
* ```
*
* @see https://github.com/sebbo2002/ical-generator#-date-time--timezones
* @since 2.0.0
*/
timezone(timezone: ICalTimezone | null | string): this;
timezone(timezone?: ICalTimezone | null | string): null | string | this {
if (timezone === undefined) {
return this.data.timezone?.name || null;
}
if (timezone === 'UTC') {
this.data.timezone = null;
} else if (typeof timezone === 'string') {
this.data.timezone = { name: timezone };
} else if (timezone === null) {
this.data.timezone = null;
} else {
this.data.timezone = timezone;
}
return this;
}
/**
* Return a shallow copy of the calendar'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 cal = ical();
* const json = JSON.stringify(cal);
*
* // later: restore calendar data
* cal = ical(JSON.parse(json));
* ```
*
* @since 0.2.4
*/
toJSON(): ICalCalendarJSONData {
return Object.assign({}, this.data, {
events: this.data.events.map((event) => event.toJSON()),
timezone: this.timezone(),
x: this.x(),
});
}
/**
* Return generated calendar as a string.
*
* ```javascript
* const cal = ical();
* console.log(cal.toString()); // → BEGIN:VCALENDAR…
* ```
*/
toString(): string {
let g = '';
// VCALENDAR and VERSION
g = 'BEGIN:VCALENDAR\r\nVERSION:2.0\r\n';
// PRODID
g += 'PRODID:-' + this.data.prodId + '\r\n';
// URL
if (this.data.url) {
g += 'URL:' + this.data.url + '\r\n';
}
// SOURCE
if (this.data.source) {
g += 'SOURCE;VALUE=URI:' + this.data.source + '\r\n';
}
// CALSCALE
if (this.data.scale) {
g += 'CALSCALE:' + this.data.scale + '\r\n';
}
// METHOD
if (this.data.method) {
g += 'METHOD:' + this.data.method + '\r\n';
}
// NAME
if (this.data.name) {
g += 'NAME:' + this.data.name + '\r\n';
g += 'X-WR-CALNAME:' + this.data.name + '\r\n';
}
// Description
if (this.data.description) {
g += 'X-WR-CALDESC:' + this.data.description + '\r\n';
}
// Timezone
if (this.data.timezone?.generator) {
const timezones = [
...new Set([
this.timezone(),
...this.data.events.map((event) => event.timezone()),
]),
].filter((tz) => tz !== null && !tz.startsWith('/')) as string[];
timezones.forEach((tz) => {
if (!this.data.timezone?.generator) {
return;
}
const s = this.data.timezone.generator(tz);
if (!s) {
return;
}
g +=
s.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n').trim() +
'\r\n';
});
}
if (this.data.timezone?.name) {
g += 'TIMEZONE-ID:' + this.data.timezone.name + '\r\n';
g += 'X-WR-TIMEZONE:' + this.data.timezone.name + '\r\n';
}
// TTL
if (this.data.ttl) {
g +=
'REFRESH-INTERVAL;VALUE=DURATION:' +
toDurationString(this.data.ttl) +
'\r\n';
g += 'X-PUBLISHED-TTL:' + toDurationString(this.data.ttl) + '\r\n';
}
// Events
this.data.events.forEach((event) => (g += event.toString()));
// CUSTOM X ATTRIBUTES
g += generateCustomAttributes(this.data);
g += 'END:VCALENDAR';
return foldLines(g);
}
/**
* Get the current ttl duration in seconds
* @since 0.2.5
*/
ttl(): null | number;
/**
* Use this method to set your feed's time to live
* (in seconds). Is used to fill `REFRESH-INTERVAL` and
* `X-PUBLISHED-TTL` in your iCal.
*
* ```javascript
* const cal = ical().ttl(60 * 60 * 24); // 1 day
* ```
*
* You can also pass a moment.js duration object. Zero, null
* or negative numbers will reset the `ttl` attribute.
*
* @since 0.2.5
*/
ttl(ttl: ICalMomentDurationStub | null | number): this;
ttl(ttl?: ICalMomentDurationStub | null | number): null | number | this {
if (ttl === undefined) {
return this.data.ttl;
}
if (isMomentDuration(ttl)) {
this.data.ttl = ttl.asSeconds();
} else if (ttl && ttl > 0) {
this.data.ttl = ttl;
} else {
this.data.ttl = null;
}
return this;
}
/**
* Get your feed's URL
* @since 0.2.5
*/
url(): null | string;
/**
* Set your feed's URL
*
* ```javascript
* calendar.url('http://example.com/my/feed.ical');
* ```
*
* @since 0.2.5
*/
url(url: null | string): this;
url(url?: null | string): null | string | this {
if (url === undefined) {
return this.data.url;
}
this.data.url = url || null;
return this;
}
/**
* Set X-* attributes. Woun't filter double attributes,
* which are also added by another method (e.g. busystatus),
* so these attributes may be inserted twice.
*
* ```javascript
* calendar.x([
* {
* key: "X-MY-CUSTOM-ATTR",
* value: "1337!"
* }
* ]);
*
* calendar.x([
* ["X-MY-CUSTOM-ATTR", "1337!"]
* ]);
*
* calendar.x({
* "X-MY-CUSTOM-ATTR": "1337!"
* });
* ```
*
* ```text
* BEGIN:VCALENDAR
* VERSION:2.0
* PRODID:-//sebbo.net//ical-generator//EN
* X-MY-CUSTOM-ATTR:1337!
* END:VCALENDAR
* ```
*
* @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. busystatus),
* so these attributes may be inserted twice.
*
* ```javascript
* calendar.x("X-MY-CUSTOM-ATTR", "1337!");
* ```
*
* ```text
* BEGIN:VCALENDAR
* VERSION:2.0
* PRODID:-//sebbo.net//ical-generator//EN
* X-MY-CUSTOM-ATTR:1337!
* END:VCALENDAR
* ```
*
* @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;
}
}