ical-generator
Version:
ical-generator is a small piece of code which generates ical calendar files
403 lines (342 loc) • 12.7 kB
text/typescript
;
import {
type ICalDateTimeValue,
type ICalDayJsStub,
type ICalLuxonDateTimeStub,
type ICalMomentDurationStub,
type ICalMomentStub,
type ICalMomentTimezoneStub,
type ICalOrganizer,
type ICalRRuleStub
} from './types.ts';
/**
* Converts a valid date/time object supported by this library to a string.
*/
export function formatDate (timezone: string | null, d: ICalDateTimeValue, dateonly?: boolean, floating?: boolean): string {
if(timezone?.startsWith('/')) {
timezone = timezone.substr(1);
}
if(typeof d === 'string' || d instanceof Date) {
const m = new Date(d);
// (!dateonly && !floating) || !timezone => utc
let s = m.getUTCFullYear() +
String(m.getUTCMonth() + 1).padStart(2, '0') +
m.getUTCDate().toString().padStart(2, '0');
// (dateonly || floating) && timezone => tz
if(timezone) {
s = m.getFullYear() +
String(m.getMonth() + 1).padStart(2, '0') +
m.getDate().toString().padStart(2, '0');
}
if(dateonly) {
return s;
}
if(timezone) {
s += 'T' + m.getHours().toString().padStart(2, '0') +
m.getMinutes().toString().padStart(2, '0') +
m.getSeconds().toString().padStart(2, '0');
return s;
}
s += 'T' + m.getUTCHours().toString().padStart(2, '0') +
m.getUTCMinutes().toString().padStart(2, '0') +
m.getUTCSeconds().toString().padStart(2, '0') +
(floating ? '' : 'Z');
return s;
}
else if(isMoment(d)) {
// @see https://momentjs.com/timezone/docs/#/using-timezones/parsing-in-zone/
const m = timezone
? (isMomentTZ(d) && !d.tz() ? d.clone().tz(timezone) : d)
: (floating || (dateonly && isMomentTZ(d) && d.tz()) ? d : d.utc());
return m.format('YYYYMMDD') + (!dateonly ? (
'T' + m.format('HHmmss') + (floating || timezone ? '' : 'Z')
) : '');
}
else if(isLuxonDate(d)) {
const m = timezone
? d.setZone(timezone)
: (floating || (dateonly && d.zone.type !== 'system') ? d : d.setZone('utc'));
return m.toFormat('yyyyLLdd') + (!dateonly ? (
'T' + m.toFormat('HHmmss') + (floating || timezone ? '' : 'Z')
) : '');
}
else {
// @see https://day.js.org/docs/en/plugin/utc
let m = d;
if(timezone) {
m = typeof d.tz === 'function' ? d.tz(timezone) : d;
}
else if(floating) {
// m = d;
}
else if (typeof d.utc === 'function') {
m = d.utc();
}
else {
throw new Error('Unable to convert dayjs object to UTC value: UTC plugin is not available!');
}
return m.format('YYYYMMDD') + (!dateonly ? (
'T' + m.format('HHmmss') + (floating || timezone ? '' : 'Z')
) : '');
}
}
/**
* Converts a valid date/time object supported by this library to a string.
* For information about this format, see RFC 5545, section 3.3.5
* https://tools.ietf.org/html/rfc5545#section-3.3.5
*/
export function formatDateTZ (timezone: string | null, property: string, date: ICalDateTimeValue | Date | string, eventData?: {floating?: boolean | null, timezone?: string | null}): string {
let tzParam = '';
let floating = eventData?.floating || false;
if (eventData?.timezone) {
tzParam = ';TZID=' + eventData.timezone;
// This isn't a 'floating' event because it has a timezone;
// but we use it to omit the 'Z' UTC specifier in formatDate()
floating = true;
}
return property + tzParam + ':' + formatDate(timezone, date, false, floating);
}
/**
* Escapes special characters in the given string
*/
export function escape (str: string | unknown, inQuotes: boolean): string {
return String(str).replace(inQuotes ? /[\\"]/g : /[\\;,]/g, function (match) {
return '\\' + match;
}).replace(/(?:\r\n|\r|\n)/g, '\\n');
}
/**
* Trim line length of given string
*/
export function foldLines (input: string): string {
return input.split('\r\n').map(function (line) {
let result = '';
let c = 0;
for (let i = 0; i < line.length; i++) {
let ch = line.charAt(i);
// surrogate pair, see https://mathiasbynens.be/notes/javascript-encoding#surrogate-pairs
if (ch >= '\ud800' && ch <= '\udbff') {
ch += line.charAt(++i);
}
// TextEncoder is available in browsers and node.js >= 11.0.0
const charsize = new TextEncoder().encode(ch).length;
c += charsize;
if (c > 74) {
result += '\r\n ';
c = charsize;
}
result += ch;
}
return result;
}).join('\r\n');
}
export function addOrGetCustomAttributes (data: {x: [string, string][]}, keyOrArray: ({key: string, value: string})[] | [string, string][] | Record<string, string>): void;
export function addOrGetCustomAttributes (data: {x: [string, string][]}, keyOrArray: string, value: string): void;
export function addOrGetCustomAttributes (data: {x: [string, string][]}): ({key: string, value: string})[];
export function addOrGetCustomAttributes (data: {x: [string, string][]}, keyOrArray?: ({key: string, value: string})[] | [string, string][] | Record<string, string> | string | undefined, value?: string | undefined): void | ({key: string, value: string})[] {
if (Array.isArray(keyOrArray)) {
data.x = keyOrArray.map((o: {key: string, value: string} | [string, string]) => {
if(Array.isArray(o)) {
return o;
}
if (typeof o.key !== 'string' || typeof o.value !== 'string') {
throw new Error('Either key or value is not a string!');
}
if (o.key.substr(0, 2) !== 'X-') {
throw new Error('Key has to start with `X-`!');
}
return [o.key, o.value] as [string, string];
});
}
else if (typeof keyOrArray === 'object') {
data.x = Object.entries(keyOrArray).map(([key, value]) => {
if (typeof key !== 'string' || typeof value !== 'string') {
throw new Error('Either key or value is not a string!');
}
if (key.substr(0, 2) !== 'X-') {
throw new Error('Key has to start with `X-`!');
}
return [key, value];
});
}
else if (typeof keyOrArray === 'string' && typeof value === 'string') {
if (keyOrArray.substr(0, 2) !== 'X-') {
throw new Error('Key has to start with `X-`!');
}
data.x.push([keyOrArray, value]);
}
else {
return data.x.map(a => ({
key: a[0],
value: a[1]
}));
}
}
export function generateCustomAttributes (data: {x: [string, string][]}): string {
const str = data.x
.map(([key, value]) => key.toUpperCase() + ':' + escape(value, false))
.join('\r\n');
return str.length ? str + '\r\n' : '';
}
/**
* Check the given string or ICalOrganizer. Parses
* the string for name and email address if possible.
*
* @param attribute Attribute name for error messages
* @param value Value to parse name/email from
*/
export function checkNameAndMail (attribute: string, value: string | ICalOrganizer): ICalOrganizer {
let result: ICalOrganizer | null = null;
if (typeof value === 'string') {
const match = value.match(/^(.+) ?<([^>]+)>$/);
if (match) {
result = {
name: match[1].trim(),
email: match[2].trim()
};
}
else if(value.includes('@')) {
result = {
name: value.trim(),
email: value.trim()
};
}
}
else if (typeof value === 'object') {
result = {
name: value.name,
email: value.email,
mailto: value.mailto,
sentBy: value.sentBy
};
}
if (!result && typeof value === 'string') {
throw new Error(
'`' + attribute + '` isn\'t formated correctly. See https://sebbo2002.github.io/ical-generator/develop/'+
'reference/interfaces/ICalOrganizer.html'
);
}
else if (!result) {
throw new Error(
'`' + attribute + '` needs to be a valid formed string or an object. See https://sebbo2002.github.io/'+
'ical-generator/develop/reference/interfaces/ICalOrganizer.html'
);
}
if (!result.name) {
throw new Error('`' + attribute + '.name` is empty!');
}
return result;
}
/**
* Checks if the given string `value` is a
* valid one for the type `type`
*/
export function checkEnum(type: Record<string, string>, value: unknown): unknown {
const allowedValues = Object.values(type);
const valueStr = String(value).toUpperCase();
if (!valueStr || !allowedValues.includes(valueStr)) {
throw new Error(`Input must be one of the following: ${allowedValues.join(', ')}`);
}
return valueStr;
}
/**
* Checks if the given input is a valid date and
* returns the internal representation (= moment object)
*/
export function checkDate(value: ICalDateTimeValue, attribute: string): ICalDateTimeValue {
// Date & String
if(
(value instanceof Date && isNaN(value.getTime())) ||
(typeof value === 'string' && isNaN(new Date(value).getTime()))
) {
throw new Error(`\`${attribute}\` has to be a valid date!`);
}
if(value instanceof Date || typeof value === 'string') {
return value;
}
// Luxon
if(isLuxonDate(value) && value.isValid === true) {
return value;
}
// Moment / Moment Timezone
if((isMoment(value) || isDayjs(value)) && value.isValid()) {
return value;
}
throw new Error(`\`${attribute}\` has to be a valid date!`);
}
export function toDate(value: ICalDateTimeValue): Date {
if(typeof value === 'string' || value instanceof Date) {
return new Date(value);
}
if(isLuxonDate(value)) {
return value.toJSDate();
}
return value.toDate();
}
export function isMoment(value: ICalDateTimeValue): value is ICalMomentStub {
// @ts-expect-error _isAMomentObject is a private property
return value != null && value._isAMomentObject != null;
}
export function isMomentTZ(value: ICalDateTimeValue): value is ICalMomentTimezoneStub {
return isMoment(value) && 'tz' in value && typeof value.tz === 'function';
}
export function isDayjs(value: ICalDateTimeValue): value is ICalDayJsStub {
return typeof value === 'object' &&
value !== null &&
!(value instanceof Date) &&
!isMoment(value) &&
!isLuxonDate(value);
}
export function isLuxonDate(value: ICalDateTimeValue): value is ICalLuxonDateTimeStub {
return typeof value === 'object' && value !== null && 'toJSDate' in value && typeof value.toJSDate === 'function';
}
export function isMomentDuration(value: unknown): value is ICalMomentDurationStub {
return value !== null && typeof value === 'object' && 'asSeconds' in value && typeof value.asSeconds === 'function';
}
export function isRRule(value: unknown): value is ICalRRuleStub {
return value !== null && typeof value === 'object' && 'between' in value && typeof value.between === 'function' && typeof value.toString === 'function';
}
export function toJSON(value: ICalDateTimeValue | null | undefined): string | null | undefined {
if(!value) {
return null;
}
if(typeof value === 'string') {
return value;
}
return value.toJSON();
}
export function toDurationString(seconds: number): string {
let string = '';
// < 0
if(seconds < 0) {
string = '-';
seconds *= -1;
}
string += 'P';
// DAYS
if(seconds >= 86400) {
string += Math.floor(seconds / 86400) + 'D';
seconds %= 86400;
}
if(!seconds && string.length > 1) {
return string;
}
string += 'T';
// HOURS
if(seconds >= 3600) {
string += Math.floor(seconds / 3600) + 'H';
seconds %= 3600;
}
// MINUTES
if(seconds >= 60) {
string += Math.floor(seconds / 60) + 'M';
seconds %= 60;
}
// SECONDS
if(seconds > 0) {
string += seconds + 'S';
}
else if(string.length <= 2) {
string += '0S';
}
return string;
}