UNPKG

ical-generator

Version:

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

508 lines (449 loc) 14.2 kB
'use strict'; import { type ICalDateTimeValue, type ICalDayJsStub, type ICalLuxonDateTimeStub, type ICalMomentDurationStub, type ICalMomentStub, type ICalMomentTimezoneStub, type ICalOrganizer, type ICalRRuleStub, type ICalTZDateStub, } from './types.ts'; export function addOrGetCustomAttributes( data: { x: [string, string][] }, keyOrArray: | [string, string][] | Record<string, string> | { key: string; value: 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?: | [string, string][] | Record<string, string> | string | undefined | { key: string; value: string }[], value?: string | undefined, ): void | { key: string; value: string }[] { if (Array.isArray(keyOrArray)) { data.x = keyOrArray.map( (o: [string, string] | { key: string; value: 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], })); } } /** * 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!`); } /** * 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; } /** * 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: ICalOrganizer | string, ): ICalOrganizer { let result: ICalOrganizer | null = null; if (typeof value === 'string') { const match = value.match(/^(.+) ?<([^>]+)>$/); if (match) { result = { email: match[2].trim(), name: match[1].trim(), }; } else if (value.includes('@')) { result = { email: value.trim(), name: value.trim(), }; } } else if (typeof value === 'object') { result = { email: value.email, mailto: value.mailto, name: value.name, 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; } /** * 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'); } /** * Converts a valid date/time object supported by this library to a string. */ export function formatDate( timezone: null | string, d: ICalDateTimeValue, dateonly?: boolean, floating?: boolean, ): string { if (timezone?.startsWith('/')) { timezone = timezone.substr(1); } if (typeof d === 'string' || d instanceof Date) { // TZDate is an extension of the native Date object. // @see https://github.com/date-fns/tz for more information. const m = isTZDate(d) ? d.withTimeZone(timezone) : 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: null | string, property: string, date: Date | ICalDateTimeValue | string, eventData?: { floating?: boolean | null; timezone?: null | string }, ): 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) ); } 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' : ''; } 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 isMoment(value: ICalDateTimeValue): value is ICalMomentStub { // @ts-expect-error _isAMomentObject is a private property return value != null && value._isAMomentObject != null; } export function isMomentDuration( value: unknown, ): value is ICalMomentDurationStub { return ( value !== null && typeof value === 'object' && 'asSeconds' in value && typeof value.asSeconds === 'function' ); } export function isMomentTZ( value: ICalDateTimeValue, ): value is ICalMomentTimezoneStub { return isMoment(value) && 'tz' in value && typeof value.tz === '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 isTZDate(value: ICalDateTimeValue): value is ICalTZDateStub { return ( value instanceof Date && 'internal' in value && value.internal instanceof Date && 'withTimeZone' in value && typeof value.withTimeZone === 'function' && 'tzComponents' in value && typeof value.tzComponents === 'function' ); } 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 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; } export function toJSON( value: ICalDateTimeValue | null | undefined, ): null | string | undefined { if (!value) { return null; } if (typeof value === 'string') { return value; } return value.toJSON(); }