From f4bc8e0535caecd1651b765f513722929d6b72b8 Mon Sep 17 00:00:00 2001 From: Sebastian Pekarek Date: Wed, 14 Apr 2021 16:41:12 +0200 Subject: [PATCH] feat(Calendar): Add support for external VTimezone generator Closes #122 --- .github/workflows/release-bot.yml | 2 - package-lock.json | 193 +++++++++++++++++++++++++++++- package.json | 2 + src/calendar.ts | 48 ++++++-- src/index.ts | 3 +- src/types.ts | 5 + test/calendar.ts | 43 +++++-- 7 files changed, 271 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release-bot.yml b/.github/workflows/release-bot.yml index 0e6b2207c..198c36ac8 100644 --- a/.github/workflows/release-bot.yml +++ b/.github/workflows/release-bot.yml @@ -13,8 +13,6 @@ jobs: steps: - name: ☁️ Checkout Project uses: actions/checkout@v2 - with: - fetch-depth: 0 - name: ☁️ Checkout ReleaseBot uses: actions/checkout@v2 with: diff --git a/package-lock.json b/package-lock.json index 5a6e59e8c..ae31a1b81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "ical-generator", "version": "v2.0.0-develop", "license": "MIT", "dependencies": { @@ -20,6 +19,7 @@ "@semantic-release/github": "^7.2.0", "@semantic-release/npm": "^7.1.0", "@semantic-release/release-notes-generator": "^9.0.2", + "@touch4it/ical-timezones": "^1.6.0", "@types/luxon": "^1.26.3", "@types/mocha": "^8.2.2", "@types/node": "^14.14.35", @@ -1521,6 +1521,15 @@ "node": ">= 6" } }, + "node_modules/@touch4it/ical-timezones": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@touch4it/ical-timezones/-/ical-timezones-1.6.0.tgz", + "integrity": "sha512-C613mSzA0oZP6qIx/y/SOvxpOAViBurysLTKERfyWDw83y1pLFAIE4H5G6iPamREBaKiZ//Fthk90SDzjKllHQ==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@types/aggregate-error": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/aggregate-error/-/aggregate-error-1.0.1.tgz", @@ -5875,7 +5884,181 @@ "treeverse", "validate-npm-package-name", "which", - "write-file-atomic" + "write-file-atomic", + "@npmcli/disparity-colors", + "@npmcli/git", + "@npmcli/installed-package-contents", + "@npmcli/map-workspaces", + "@npmcli/metavuln-calculator", + "@npmcli/move-file", + "@npmcli/name-from-folder", + "@npmcli/node-gyp", + "@npmcli/promise-spawn", + "@tootallnate/once", + "agent-base", + "agentkeepalive", + "aggregate-error", + "ajv", + "ansi-regex", + "ansi-styles", + "aproba", + "are-we-there-yet", + "asap", + "asn1", + "assert-plus", + "asynckit", + "aws-sign2", + "aws4", + "balanced-match", + "bcrypt-pbkdf", + "bin-links", + "binary-extensions", + "brace-expansion", + "builtins", + "caseless", + "cidr-regex", + "clean-stack", + "clone", + "cmd-shim", + "code-point-at", + "color-convert", + "color-name", + "colors", + "combined-stream", + "common-ancestor-path", + "concat-map", + "console-control-strings", + "core-util-is", + "dashdash", + "debug", + "debuglog", + "defaults", + "delayed-stream", + "delegates", + "depd", + "dezalgo", + "diff", + "ecc-jsbn", + "emoji-regex", + "encoding", + "env-paths", + "err-code", + "extend", + "extsprintf", + "fast-deep-equal", + "fast-json-stable-stringify", + "forever-agent", + "form-data", + "fs-minipass", + "fs.realpath", + "function-bind", + "gauge", + "getpass", + "har-schema", + "har-validator", + "has", + "has-flag", + "has-unicode", + "http-cache-semantics", + "http-proxy-agent", + "http-signature", + "https-proxy-agent", + "humanize-ms", + "iconv-lite", + "ignore-walk", + "imurmurhash", + "indent-string", + "infer-owner", + "inflight", + "inherits", + "ip", + "ip-regex", + "is-core-module", + "is-fullwidth-code-point", + "is-lambda", + "is-typedarray", + "isarray", + "isexe", + "isstream", + "jsbn", + "json-schema", + "json-schema-traverse", + "json-stringify-nice", + "json-stringify-safe", + "jsonparse", + "jsprim", + "just-diff", + "just-diff-apply", + "lru-cache", + "mime-db", + "mime-types", + "minimatch", + "minipass-collect", + "minipass-fetch", + "minipass-flush", + "minipass-json-stream", + "minipass-sized", + "minizlib", + "mute-stream", + "normalize-package-data", + "npm-bundled", + "npm-install-checks", + "npm-normalize-package-bin", + "npm-packlist", + "number-is-nan", + "oauth-sign", + "object-assign", + "once", + "p-map", + "path-is-absolute", + "path-parse", + "performance-now", + "process-nextick-args", + "promise-all-reject-late", + "promise-call-limit", + "promise-inflight", + "promise-retry", + "promzard", + "psl", + "punycode", + "qs", + "read-cmd-shim", + "readable-stream", + "request", + "resolve", + "retry", + "safe-buffer", + "safer-buffer", + "set-blocking", + "signal-exit", + "smart-buffer", + "socks", + "socks-proxy-agent", + "spdx-correct", + "spdx-exceptions", + "spdx-expression-parse", + "spdx-license-ids", + "sshpk", + "string_decoder", + "string-width", + "stringify-package", + "strip-ansi", + "supports-color", + "tunnel-agent", + "tweetnacl", + "typedarray-to-buffer", + "unique-filename", + "unique-slug", + "uri-js", + "util-deprecate", + "uuid", + "validate-npm-package-license", + "verror", + "walk-up-path", + "wcwidth", + "wide-align", + "wrappy", + "yallist" ], "dev": true, "dependencies": { @@ -12635,6 +12818,12 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@touch4it/ical-timezones": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@touch4it/ical-timezones/-/ical-timezones-1.6.0.tgz", + "integrity": "sha512-C613mSzA0oZP6qIx/y/SOvxpOAViBurysLTKERfyWDw83y1pLFAIE4H5G6iPamREBaKiZ//Fthk90SDzjKllHQ==", + "dev": true + }, "@types/aggregate-error": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/aggregate-error/-/aggregate-error-1.0.1.tgz", diff --git a/package.json b/package.json index 36f7d0eb8..5bd97881d 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@semantic-release/github": "^7.2.0", "@semantic-release/npm": "^7.1.0", "@semantic-release/release-notes-generator": "^9.0.2", + "@touch4it/ical-timezones": "^1.6.0", "@types/luxon": "^1.26.3", "@types/mocha": "^8.2.2", "@types/node": "^14.14.35", @@ -60,6 +61,7 @@ "typescript": "^4.2.3" }, "peerDependencies": { + "@touch4it/ical-timezones": "^1.6.0", "@types/luxon": "^1.26.0", "@types/mocha": "^8.2.1", "@types/node": "^14.14.31", diff --git a/src/calendar.ts b/src/calendar.ts index 1c1cee71c..1c184a9fc 100755 --- a/src/calendar.ts +++ b/src/calendar.ts @@ -13,6 +13,7 @@ import ICalEvent, {ICalEventData, ICalEventJSONData} from './event'; import {writeFile, writeFileSync} from 'fs'; import {promises as fsPromises} from 'fs'; import {ServerResponse} from 'http'; +import {ICalTimezone} from './types'; export interface ICalCalendarData { @@ -20,7 +21,7 @@ export interface ICalCalendarData { method?: ICalCalendarMethod | null; name?: string | null; description?: string | null; - timezone?: string | null; + timezone?: ICalTimezone | string | null; url?: string | null; scale?: string | null; ttl?: number | Duration | null; @@ -33,7 +34,7 @@ interface ICalCalendarInternalData { method: ICalCalendarMethod | null; name: string | null; description: string | null; - timezone: string | null; + timezone: ICalTimezone | null; url: string | null; scale: string | null; ttl: number | null; @@ -289,13 +290,22 @@ export default class ICalCalendar { * * @since 0.2.0 */ - timezone(timezone: string | null): this; - timezone(timezone?: string | null): this | string | null { + timezone(timezone: ICalTimezone | string | null): this; + timezone(timezone?: ICalTimezone | string | null): this | string | null { if (timezone === undefined) { - return this.data.timezone; + return this.data.timezone?.name || null; + } + + if(typeof timezone === 'string') { + this.data.timezone = {name: timezone}; + } + else if(timezone === null) { + this.data.timezone = null; + } + else { + this.data.timezone = timezone; } - this.data.timezone = timezone ? String(timezone) : null; return this; } @@ -665,6 +675,7 @@ export default class ICalCalendar { */ toJSON(): ICalCalendarJSONData { return Object.assign({}, this.data, { + timezone: this.timezone(), events: this.data.events.map(event => event.toJSON()), x: this.x() }); @@ -723,9 +734,28 @@ export default class ICalCalendar { } // Timezone - if (this.data.timezone) { - g += 'TIMEZONE-ID:' + this.data.timezone + '\r\n'; - g += 'X-WR-TIMEZONE:' + this.data.timezone + '\r\n'; + 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(/\n/g, '\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 diff --git a/src/index.ts b/src/index.ts index 38a271545..e575b6f43 100755 --- a/src/index.ts +++ b/src/index.ts @@ -85,5 +85,6 @@ export { ICalOrganizer, ICalDescription, ICalEventRepeatingFreq, - ICalWeekday + ICalWeekday, + ICalTimezone } from './types'; diff --git a/src/types.ts b/src/types.ts index 312c5aaef..d787e2100 100644 --- a/src/types.ts +++ b/src/types.ts @@ -48,6 +48,11 @@ export interface ICalDescription { html?: string; } +export interface ICalTimezone { + name: string | null; + generator?: (timezone: string) => string|null; +} + export enum ICalEventRepeatingFreq { SECONDLY = 'SECONDLY', MINUTELY = 'MINUTELY', diff --git a/test/calendar.ts b/test/calendar.ts index 9762fbe76..4608a07fa 100644 --- a/test/calendar.ts +++ b/test/calendar.ts @@ -8,6 +8,7 @@ import {join} from 'path'; import {getPortPromise} from 'portfinder'; import ICalCalendar, {ICalCalendarJSONData, ICalCalendarMethod} from '../src/calendar'; import ICalEvent from '../src/event'; +import {getVtimezoneComponent} from '@touch4it/ical-timezones'; describe('ical-generator Calendar', function () { describe('constructor()', function () { @@ -188,6 +189,11 @@ describe('ical-generator Calendar', function () { it('setter should return this', function () { const cal = new ICalCalendar(); assert.deepStrictEqual(cal, cal.timezone('Europe/Berlin')); + assert.deepStrictEqual(cal, cal.timezone(null)); + assert.deepStrictEqual(cal, cal.timezone({ + name: 'Europe/Berlin', + generator: getVtimezoneComponent + })); }); it('getter should return value', function () { @@ -196,17 +202,9 @@ describe('ical-generator Calendar', function () { cal.timezone(null); assert.strictEqual(cal.timezone(), null); - }); - it('should make a difference to iCal output', function () { - const cal = new ICalCalendar().timezone('Europe/London'); - cal.createEvent({ - start: new Date(), - end: new Date(new Date().getTime() + 3600000), - summary: 'Example Event' - }); - - assert.strictEqual(cal.timezone(), 'Europe/London'); + cal.timezone({name: 'Europe/Berlin'}); + assert.strictEqual(cal.timezone(), 'Europe/Berlin'); }); }); @@ -645,7 +643,30 @@ describe('ical-generator Calendar', function () { assert.ok(cal.toString().indexOf('X-WR-TIMEZONE:TEST') > -1); }); - it('should include the timezone', function () { + it('should include VTimezone objects if generator was supplied', function () { + const cal = new ICalCalendar(); + cal.timezone({name: 'Europe/Berlin', generator: getVtimezoneComponent}); + cal.createEvent({ + start: new Date(), + timezone: 'Europe/London' + }); + + assert.ok(cal.toString().includes('BEGIN:VTIMEZONE\r\n'), 'BEGIN:VTIMEZONE'); + assert.ok(cal.toString().includes('TZID:Europe/Berlin\r\n'), 'TZID:Europe/Berlin'); + assert.ok(cal.toString().includes('TZID:Europe/London\r\n'), 'TZID:Europe/London'); + }); + it('should also work if VTimezone was not found', function () { + const cal = new ICalCalendar(); + cal.timezone({name: 'FOO', generator: getVtimezoneComponent}); + assert.ok(!cal.toString().includes('TZID:Foo\r\n')); + }); + it('should ignore global timezone ids', function () { + const cal = new ICalCalendar(); + cal.timezone({name: '/Europe/Berlin', generator: getVtimezoneComponent}); + assert.ok(!cal.toString().includes('TZID:/Europe/Berlin\r\n')); + }); + + it('should include the ttl', function () { const cal = new ICalCalendar(); cal.ttl(moment.duration(3, 'days')); assert.ok(cal.toString().indexOf('REFRESH-INTERVAL;VALUE=DURATION:P3D') > -1);