@hebcal/icalendar 5.1.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # hebcal-icalendar
1
+ # @hebcal/icalendar
2
2
  Jewish holidays and Hebrew calendar as iCalendar RFC 2445
3
3
 
4
4
  ## Installation
@@ -10,68 +10,25 @@ $ npm install @hebcal/icalendar
10
10
  ```javascript
11
11
  import {HebrewCalendar, Location} from '@hebcal/core';
12
12
  import {eventsToIcalendar} from '@hebcal/icalendar';
13
+ import fs from 'fs';
13
14
 
14
15
  const options = {
15
16
  year: 2020,
16
17
  month: 2,
17
18
  sedrot: true,
18
19
  candlelighting: true,
19
- location: Location.lookup('Hawaii'),
20
+ location: Location.lookup('Tel Aviv'),
20
21
  };
21
22
  const events = HebrewCalendar.calendar(options);
22
- console.log(await eventsToIcalendar(ev, options));
23
- ```
24
-
25
- ## Functions
26
-
27
- <dl>
28
- <dt><a href="#eventToIcal">eventToIcal(e, options)</a> ⇒ <code>string</code></dt>
29
- <dd><p>Transforms a single Event into a VEVENT string</p>
30
- </dd>
31
- <dt><a href="#eventsToIcalendarStream">eventsToIcalendarStream(readable, events, options)</a> ⇒ <code>stream.Readable</code></dt>
32
- <dd><p>Generates an RFC 2445 iCalendar stream from an array of events</p>
33
- </dd>
34
- <dt><a href="#eventsToIcalendar">eventsToIcalendar(events, options)</a> ⇒ <code>string</code></dt>
35
- <dd><p>Renders an array of events as a full RFC 2445 iCalendar string</p>
36
- </dd>
37
- </dl>
38
-
39
- <a name="eventToIcal"></a>
40
-
41
- ## eventToIcal(e, options) ⇒ <code>string</code>
42
- Transforms a single Event into a VEVENT string
43
-
44
- **Kind**: global function
45
- **Returns**: <code>string</code> - multi-line result, delimited by \r\n
46
-
47
- | Param | Type |
48
- | --- | --- |
49
- | e | <code>Event</code> |
50
- | options | <code>HebcalOptions</code> |
51
23
 
52
- <a name="eventsToIcalendarStream"></a>
24
+ const str = await eventsToIcalendar(events, {
25
+ locale: 'he',
26
+ ...options,
27
+ })
53
28
 
54
- ## eventsToIcalendarStream(readable, events, options) ⇒ <code>stream.Readable</code>
55
- Generates an RFC 2445 iCalendar stream from an array of events
56
-
57
- **Kind**: global function
58
-
59
- | Param | Type |
60
- | --- | --- |
61
- | readable | <code>stream.Readable</code> |
62
- | events | <code>Array.&lt;Event&gt;</code> |
63
- | options | <code>HebcalOptions</code> |
64
-
65
- <a name="eventsToIcalendar"></a>
66
-
67
- ## eventsToIcalendar(events, options) ⇒ <code>string</code>
68
- Renders an array of events as a full RFC 2445 iCalendar string
69
-
70
- **Kind**: global function
71
- **Returns**: <code>string</code> - multi-line result, delimited by \r\n
72
-
73
- | Param | Type |
74
- | --- | --- |
75
- | events | <code>Array.&lt;Event&gt;</code> |
76
- | options | <code>HebcalOptions</code> |
29
+ const icalStream = fs.createWriteStream('feed.ics');
30
+ icalStream.write(str);
31
+ icalStream.close();
32
+ ```
77
33
 
34
+ ## [API Documentation](https://hebcal.github.io/api/icalendar/index.html)
@@ -15,6 +15,7 @@ export type ICalEventOptions = {
15
15
  relcalid?: string;
16
16
  yahrzeit?: boolean;
17
17
  subscribe?: string | boolean;
18
+ url?: boolean;
18
19
  };
19
20
  export type ICalOptions = CalOptions & ICalEventOptions;
20
21
  /**
@@ -41,51 +42,28 @@ export declare class IcalEvent {
41
42
  */
42
43
  constructor(ev: Event, options?: ICalOptions);
43
44
  getAlarm(): string | null;
44
- /**
45
- * @return {string}
46
- */
47
45
  getUid(): string;
48
- /**
49
- * @return {string[]}
50
- */
51
46
  getLongLines(): string[];
52
- /**
53
- * @return {string}
54
- */
55
47
  toString(): string;
56
48
  /**
57
49
  * fold lines to 75 characters
58
- * @return {string[]}
59
50
  */
60
51
  getLines(): string[];
61
52
  /**
62
53
  * fold line to 75 characters
63
- * @param {string} line
64
- * @return {string}
65
54
  */
66
55
  static fold(line: string): string;
67
- /**
68
- * @param {string} str
69
- * @return {string}
70
- */
71
56
  static escape(str: string): string;
72
- /**
73
- * @param {Date} dt
74
- * @return {string}
75
- */
76
57
  static formatYYYYMMDD(dt: Date): string;
77
58
  /**
78
59
  * Returns UTC string for iCalendar
79
- * @param {Date} dt
80
- * @return {string}
81
60
  */
82
61
  static makeDtstamp(dt: Date): string;
83
- /** @return {string} */
84
62
  static version(): string;
85
63
  }
86
64
  /**
87
65
  * Transforms a single Event into a VEVENT string
88
- * @return {string} multi-line result, delimited by \r\n
66
+ * @returns multi-line result, delimited by \r\n
89
67
  */
90
68
  export declare function eventToIcal(ev: Event, options: ICalOptions): string;
91
69
  /**
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
- /*! @hebcal/icalendar v5.1.0 */
2
- import { flags, Locale, greg } from '@hebcal/core';
1
+ /*! @hebcal/icalendar v6.0.1 */
2
+ import { Locale, pad2, greg, pad4 } from '@hebcal/hdate';
3
+ import { flags } from '@hebcal/core';
3
4
  import { murmur32HexSync } from 'murmurhash3';
4
- import { shouldRenderBrief, pad2, getEventCategories, makeAnchor, pad4, getHolidayDescription, getCalendarTitle, appendIsraelAndTracking, makeTorahMemoText } from '@hebcal/rest-api';
5
+ import { shouldRenderBrief, getEventCategories, makeAnchor, getHolidayDescription, getCalendarTitle, appendIsraelAndTracking, makeTorahMemoText } from '@hebcal/rest-api';
5
6
  import { promises } from 'fs';
6
7
 
7
8
  // DO NOT EDIT THIS AUTO-GENERATED FILE!
8
- const version = '5.1.0';
9
+ const version = '6.0.1';
9
10
 
10
11
  const vtimezoneCache = new Map();
11
12
  const CATEGORY = {
@@ -48,8 +49,11 @@ function appendTrackingToUrl(url, options) {
48
49
  }
49
50
  const encoder = new TextEncoder();
50
51
  const char74re = /(.{1,74})/g;
51
- const DAILY_LEARNING = flags.DAILY_LEARNING | flags.DAF_YOMI |
52
- flags.MISHNA_YOMI | flags.YERUSHALMI_YOMI | flags.NACH_YOMI;
52
+ const DAILY_LEARNING = flags.DAILY_LEARNING |
53
+ flags.DAF_YOMI |
54
+ flags.MISHNA_YOMI |
55
+ flags.YERUSHALMI_YOMI |
56
+ flags.NACH_YOMI;
53
57
  /**
54
58
  * Represents an RFC 2445 iCalendar VEVENT
55
59
  */
@@ -69,10 +73,12 @@ class IcalEvent {
69
73
  else if (typeof opts.sequence === 'number') {
70
74
  this.sequence = opts.sequence;
71
75
  }
72
- const timed = this.timed = Boolean(ev.eventTime);
76
+ const timed = (this.timed = Boolean(ev.eventTime));
73
77
  const locale = opts.locale;
74
78
  const location = opts.location;
75
- let subj = shouldRenderBrief(ev) ? ev.renderBrief(locale) : ev.render(locale);
79
+ let subj = shouldRenderBrief(ev)
80
+ ? ev.renderBrief(locale)
81
+ : ev.render(locale);
76
82
  const mask = ev.getFlags();
77
83
  if (ev.locationName) {
78
84
  this.locationName = ev.locationName;
@@ -80,7 +86,7 @@ class IcalEvent {
80
86
  else if (timed && location) {
81
87
  this.locationName = location.getShortName();
82
88
  }
83
- else if ((mask & DAILY_LEARNING) && ev.category) {
89
+ else if (mask & DAILY_LEARNING && ev.category) {
84
90
  this.locationName = Locale.gettext(ev.category, locale);
85
91
  }
86
92
  const date = IcalEvent.formatYYYYMMDD(ev.getDate().greg());
@@ -129,7 +135,8 @@ class IcalEvent {
129
135
  }
130
136
  }
131
137
  this.subj = subj;
132
- this.category = ev.category || CATEGORY[(_a = getEventCategories(ev)) === null || _a === void 0 ? void 0 : _a[0]];
138
+ this.category =
139
+ ev.category || CATEGORY[(_a = getEventCategories(ev)) === null || _a === void 0 ? void 0 : _a[0]];
133
140
  }
134
141
  getAlarm() {
135
142
  const ev = this.ev;
@@ -157,9 +164,6 @@ class IcalEvent {
157
164
  }
158
165
  return null;
159
166
  }
160
- /**
161
- * @return {string}
162
- */
163
167
  getUid() {
164
168
  if (this.ev.uid) {
165
169
  return this.ev.uid;
@@ -178,9 +182,6 @@ class IcalEvent {
178
182
  }
179
183
  return uid;
180
184
  }
181
- /**
182
- * @return {string[]}
183
- */
184
185
  getLongLines() {
185
186
  if (this.lines)
186
187
  return this.lines;
@@ -189,17 +190,16 @@ class IcalEvent {
189
190
  if (this.sequence) {
190
191
  categoryLine.unshift(`SEQUENCE:${this.sequence}`);
191
192
  }
192
- const arr = this.lines = [
193
- 'BEGIN:VEVENT',
194
- `DTSTAMP:${this.dtstamp}`,
195
- ].concat(categoryLine).concat([
193
+ const arr = (this.lines = ['BEGIN:VEVENT', `DTSTAMP:${this.dtstamp}`]
194
+ .concat(categoryLine)
195
+ .concat([
196
196
  `SUMMARY:${this.subj}`,
197
197
  `DTSTART${this.dtargs}:${this.startDate}`,
198
198
  `DTEND${this.dtargs}:${this.endDate}`,
199
199
  `UID:${uid}`,
200
200
  `TRANSP:${this.transp}`,
201
201
  `X-MICROSOFT-CDO-BUSYSTATUS:${this.busyStatus}`,
202
- ]);
202
+ ]));
203
203
  if (!this.timed) {
204
204
  arr.push('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE');
205
205
  }
@@ -218,6 +218,14 @@ class IcalEvent {
218
218
  if (this.timed && loc) {
219
219
  arr.push('GEO:' + loc.getLatitude() + ';' + loc.getLongitude());
220
220
  }
221
+ // In addition to the URL being part of the DESCRIPTION field,
222
+ // should we also generate an RFC 5545 URL property?
223
+ if (options.url) {
224
+ const url = ev.url();
225
+ if (url) {
226
+ arr.push('URL:' + appendTrackingToUrl(url, options));
227
+ }
228
+ }
221
229
  const trigger = this.getAlarm();
222
230
  if (trigger) {
223
231
  arr.push('BEGIN:VALARM', 'ACTION:DISPLAY', 'DESCRIPTION:Event reminder', `${trigger}`, 'END:VALARM');
@@ -225,23 +233,17 @@ class IcalEvent {
225
233
  arr.push('END:VEVENT');
226
234
  return arr;
227
235
  }
228
- /**
229
- * @return {string}
230
- */
231
236
  toString() {
232
237
  return this.getLines().join('\r\n');
233
238
  }
234
239
  /**
235
240
  * fold lines to 75 characters
236
- * @return {string[]}
237
241
  */
238
242
  getLines() {
239
243
  return this.getLongLines().map(IcalEvent.fold);
240
244
  }
241
245
  /**
242
246
  * fold line to 75 characters
243
- * @param {string} line
244
- * @return {string}
245
247
  */
246
248
  static fold(line) {
247
249
  let isASCII = true;
@@ -284,10 +286,6 @@ class IcalEvent {
284
286
  }
285
287
  return result + current;
286
288
  }
287
- /**
288
- * @param {string} str
289
- * @return {string}
290
- */
291
289
  static escape(str) {
292
290
  if (str.indexOf(',') !== -1) {
293
291
  str = str.replace(/,/g, '\\,');
@@ -297,46 +295,45 @@ class IcalEvent {
297
295
  }
298
296
  return str;
299
297
  }
300
- /**
301
- * @param {Date} dt
302
- * @return {string}
303
- */
304
298
  static formatYYYYMMDD(dt) {
305
- return pad4(dt.getFullYear()) +
306
- pad2(dt.getMonth() + 1) + pad2(dt.getDate());
299
+ return (pad4(dt.getFullYear()) + pad2(dt.getMonth() + 1) + pad2(dt.getDate()));
307
300
  }
308
301
  /**
309
302
  * Returns UTC string for iCalendar
310
- * @param {Date} dt
311
- * @return {string}
312
303
  */
313
304
  static makeDtstamp(dt) {
314
305
  const s = dt.toISOString();
315
- return s.slice(0, 4) + s.slice(5, 7) + s.slice(8, 13) +
316
- s.slice(14, 16) + s.slice(17, 19) + 'Z';
306
+ return (s.slice(0, 4) +
307
+ s.slice(5, 7) +
308
+ s.slice(8, 13) +
309
+ s.slice(14, 16) +
310
+ s.slice(17, 19) +
311
+ 'Z');
317
312
  }
318
- /** @return {string} */
319
313
  static version() {
320
314
  return version;
321
315
  }
322
316
  }
323
317
  /**
324
318
  * Transforms a single Event into a VEVENT string
325
- * @return {string} multi-line result, delimited by \r\n
319
+ * @returns multi-line result, delimited by \r\n
326
320
  */
327
321
  function eventToIcal(ev, options) {
328
322
  const ical = new IcalEvent(ev, options);
329
323
  return ical.toString();
330
324
  }
331
325
  const torahMemoCache = new Map();
332
- const HOLIDAY_IGNORE_MASK = DAILY_LEARNING | flags.OMER_COUNT |
333
- flags.SHABBAT_MEVARCHIM | flags.MOLAD | flags.USER_EVENT |
326
+ const HOLIDAY_IGNORE_MASK = DAILY_LEARNING |
327
+ flags.OMER_COUNT |
328
+ flags.SHABBAT_MEVARCHIM |
329
+ flags.MOLAD |
330
+ flags.USER_EVENT |
334
331
  flags.HEBREW_DATE;
335
332
  /**
336
333
  * @private
337
334
  */
338
335
  function makeTorahMemo(ev, il) {
339
- if ((ev.getFlags() & HOLIDAY_IGNORE_MASK) || ev.eventTime) {
336
+ if (ev.getFlags() & HOLIDAY_IGNORE_MASK || ev.eventTime) {
340
337
  return '';
341
338
  }
342
339
  const hd = ev.getDate();
@@ -367,8 +364,16 @@ function createMemo(ev, options) {
367
364
  const mask = ev.getFlags();
368
365
  if (mask & flags.OMER_COUNT) {
369
366
  const omerEv = ev;
370
- const sefira = [omerEv.sefira('en'), omerEv.sefira('he'), omerEv.sefira('translit')].join('\\n');
371
- return omerEv.getTodayIs('en') + '\\n\\n' + omerEv.getTodayIs('he') + '\\n\\n' + sefira;
367
+ const sefira = [
368
+ omerEv.sefira('en'),
369
+ omerEv.sefira('he'),
370
+ omerEv.sefira('translit'),
371
+ ].join('\\n');
372
+ return (omerEv.getTodayIs('en') +
373
+ '\\n\\n' +
374
+ omerEv.getTodayIs('he') +
375
+ '\\n\\n' +
376
+ sefira);
372
377
  }
373
378
  const url = appendTrackingToUrl(ev.url(), options);
374
379
  const torahMemo = makeTorahMemo(ev, options.il);
@@ -398,6 +403,8 @@ function createMemo(ev, options) {
398
403
  * Generates an RFC 2445 iCalendar string from an array of events
399
404
  */
400
405
  async function eventsToIcalendar(events, options) {
406
+ if (!events.length)
407
+ throw new RangeError('Events can not be empty');
401
408
  if (!options)
402
409
  throw new TypeError('Invalid options object');
403
410
  const opts = Object.assign({}, options);
@@ -405,24 +412,30 @@ async function eventsToIcalendar(events, options) {
405
412
  if (!opts.title) {
406
413
  opts.title = getCalendarTitle(events, opts);
407
414
  }
408
- const icals = events.map((ev) => new IcalEvent(ev, opts));
415
+ const icals = events.map(ev => new IcalEvent(ev, opts));
409
416
  return icalEventsToString(icals, opts);
410
417
  }
411
418
  /**
412
419
  * Generates an RFC 2445 iCalendar string from an array of IcalEvents
413
420
  */
414
421
  async function icalEventsToString(icals, options) {
422
+ if (!icals.length)
423
+ throw new RangeError('Events can not be empty');
424
+ if (!options)
425
+ throw new TypeError('Invalid options object');
415
426
  const stream = [];
416
427
  const locale = options.locale || Locale.getLocaleName();
417
428
  const uclang = locale.toUpperCase();
418
429
  const opts = Object.assign({}, options);
419
430
  opts.dtstamp = opts.dtstamp || IcalEvent.makeDtstamp(new Date());
420
431
  const title = opts.title ? IcalEvent.escape(opts.title) : 'Untitled';
421
- const caldesc = opts.caldesc ? IcalEvent.escape(opts.caldesc) :
422
- opts.yahrzeit ?
423
- 'Yahrzeits + Anniversaries from www.hebcal.com' :
424
- 'Jewish Holidays from www.hebcal.com';
425
- const prodid = opts.prodid || `-//hebcal.com/NONSGML Hebcal Calendar v1${version}//${uclang}`;
432
+ const caldesc = opts.caldesc
433
+ ? IcalEvent.escape(opts.caldesc)
434
+ : opts.yahrzeit
435
+ ? 'Yahrzeits + Anniversaries from www.hebcal.com'
436
+ : 'Jewish Holidays from www.hebcal.com';
437
+ const prodid = opts.prodid ||
438
+ `-//hebcal.com/NONSGML Hebcal Calendar v1${version}//${uclang}`;
426
439
  const preamble = [
427
440
  'BEGIN:VCALENDAR',
428
441
  'VERSION:2.0',
@@ -1 +1 @@
1
- export declare const version = "5.1.0";
1
+ export declare const version = "6.0.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hebcal/icalendar",
3
- "version": "5.1.0",
3
+ "version": "6.0.1",
4
4
  "author": "Michael J. Radwin (https://github.com/mjradwin)",
5
5
  "keywords": [
6
6
  "ical",
@@ -9,15 +9,16 @@
9
9
  "hebcal"
10
10
  ],
11
11
  "description": "Jewish holidays and Hebrew calendar as iCalendar RFC 2445",
12
- "main": "dist/index.cjs",
13
12
  "module": "dist/index.mjs",
14
13
  "type": "module",
15
14
  "exports": {
16
15
  "import": "./dist/index.mjs",
17
- "require": "./dist/index.cjs",
18
16
  "types": "./dist/icalendar.d.ts"
19
17
  },
20
18
  "typings": "dist/icalendar.d.ts",
19
+ "engines": {
20
+ "node": ">= 18.0.0"
21
+ },
21
22
  "files": [
22
23
  "dist"
23
24
  ],
@@ -29,10 +30,11 @@
29
30
  "url": "https://github.com/hebcal/hebcal-icalendar/issues"
30
31
  },
31
32
  "dependencies": {
32
- "@hebcal/core": "^5.4.5",
33
- "@hebcal/rest-api": "^5.0.5",
33
+ "@hebcal/core": "^5.8.0",
34
+ "@hebcal/hdate": "^0.12.0",
35
+ "@hebcal/rest-api": "^5.2.0",
34
36
  "murmurhash3": "^0.5.0",
35
- "tslib": "^2.6.3"
37
+ "tslib": "^2.8.1"
36
38
  },
37
39
  "scripts": {
38
40
  "build:rollup": "rollup -c",
@@ -40,27 +42,28 @@
40
42
  "build": "npm run build:version && npm run build:rollup",
41
43
  "prepare": "npm run build",
42
44
  "pretest": "npm run build",
43
- "readme": "npx jsdoc2md dist/index.ts",
45
+ "docs": "typedoc",
44
46
  "coverage": "jest --coverage",
45
- "test": "jest"
47
+ "test": "jest",
48
+ "lint": "gts lint",
49
+ "clean": "gts clean",
50
+ "compile": "tsc",
51
+ "fix": "gts fix"
46
52
  },
47
53
  "license": "BSD-2-Clause",
48
54
  "devDependencies": {
49
- "@babel/preset-env": "^7.24.7",
50
- "@babel/preset-typescript": "^7.24.7",
51
- "@hebcal/learning": "^5.0.8",
52
- "@rollup/plugin-commonjs": "^26.0.1",
55
+ "@babel/preset-env": "^7.26.0",
56
+ "@babel/preset-typescript": "^7.26.0",
57
+ "@hebcal/learning": "^5.1.2",
58
+ "@rollup/plugin-commonjs": "^28.0.1",
53
59
  "@rollup/plugin-json": "^6.1.0",
54
- "@rollup/plugin-typescript": "^11.1.6",
55
- "@types/jest": "^29.5.12",
56
- "eslint": "^8.57.0",
57
- "eslint-config-google": "^0.14.0",
60
+ "@rollup/plugin-typescript": "^12.1.1",
61
+ "@types/jest": "^29.5.14",
62
+ "@types/node": "22.9.0",
58
63
  "gts": "^5.3.1",
59
64
  "jest": "^29.7.0",
60
- "jsdoc": "^4.0.3",
61
- "jsdoc-to-markdown": "^8.0.1",
62
- "rollup": "^4.18.0",
63
- "ts-jest": "^29.1.5",
64
- "typescript": "^5.5.2"
65
+ "rollup": "^4.25.0",
66
+ "typedoc": "^0.26.11",
67
+ "typescript": "^5.6.3"
65
68
  }
66
69
  }
package/dist/index.cjs DELETED
@@ -1,492 +0,0 @@
1
- /*! @hebcal/icalendar v5.1.0 */
2
- 'use strict';
3
-
4
- var core = require('@hebcal/core');
5
- var murmurhash3 = require('murmurhash3');
6
- var restApi = require('@hebcal/rest-api');
7
- var fs = require('fs');
8
-
9
- // DO NOT EDIT THIS AUTO-GENERATED FILE!
10
- const version = '5.1.0';
11
-
12
- const vtimezoneCache = new Map();
13
- const CATEGORY = {
14
- candles: 'Holiday',
15
- dafyomi: 'Daf Yomi',
16
- mishnayomi: 'Mishna Yomi',
17
- nachyomi: 'Nach Yomi',
18
- yerushalmi: 'Yerushalmi Yomi',
19
- havdalah: 'Holiday',
20
- hebdate: null,
21
- holiday: 'Holiday',
22
- mevarchim: null,
23
- molad: null,
24
- omer: null,
25
- parashat: 'Parsha',
26
- roshchodesh: 'Holiday',
27
- user: 'Personal',
28
- zmanim: null,
29
- };
30
- /**
31
- * @private
32
- */
33
- function addOptional(arr, key, val) {
34
- if (val) {
35
- const str = IcalEvent.escape(val);
36
- arr.push(key + ':' + str);
37
- }
38
- }
39
- /**
40
- * @private
41
- */
42
- function appendTrackingToUrl(url, options) {
43
- if (!url) {
44
- return null;
45
- }
46
- const utmSource = options.utmSource || 'js';
47
- const utmMedium = options.utmMedium || 'icalendar';
48
- const utmCampaign = options.utmCampaign;
49
- return restApi.appendIsraelAndTracking(url, options.il, utmSource, utmMedium, utmCampaign);
50
- }
51
- const encoder = new TextEncoder();
52
- const char74re = /(.{1,74})/g;
53
- const DAILY_LEARNING = core.flags.DAILY_LEARNING | core.flags.DAF_YOMI |
54
- core.flags.MISHNA_YOMI | core.flags.YERUSHALMI_YOMI | core.flags.NACH_YOMI;
55
- /**
56
- * Represents an RFC 2445 iCalendar VEVENT
57
- */
58
- class IcalEvent {
59
- /**
60
- * Builds an IcalEvent object from a Hebcal Event
61
- */
62
- constructor(ev, options = {}) {
63
- var _a;
64
- this.ev = ev;
65
- const opts = Object.assign({}, options);
66
- this.options = opts;
67
- this.dtstamp = opts.dtstamp || IcalEvent.makeDtstamp(new Date());
68
- if (typeof ev.sequence === 'number') {
69
- this.sequence = ev.sequence;
70
- }
71
- else if (typeof opts.sequence === 'number') {
72
- this.sequence = opts.sequence;
73
- }
74
- const timed = this.timed = Boolean(ev.eventTime);
75
- const locale = opts.locale;
76
- const location = opts.location;
77
- let subj = restApi.shouldRenderBrief(ev) ? ev.renderBrief(locale) : ev.render(locale);
78
- const mask = ev.getFlags();
79
- if (ev.locationName) {
80
- this.locationName = ev.locationName;
81
- }
82
- else if (timed && location) {
83
- this.locationName = location.getShortName();
84
- }
85
- else if ((mask & DAILY_LEARNING) && ev.category) {
86
- this.locationName = core.Locale.gettext(ev.category, locale);
87
- }
88
- const date = IcalEvent.formatYYYYMMDD(ev.getDate().greg());
89
- this.startDate = this.isoDateOnly = date;
90
- this.dtargs = '';
91
- this.transp = 'TRANSPARENT';
92
- this.busyStatus = 'FREE';
93
- if (timed) {
94
- let [hour, minute] = ev.eventTimeStr.split(':');
95
- hour = +hour;
96
- minute = +minute;
97
- this.startDate += 'T' + restApi.pad2(hour) + restApi.pad2(minute) + '00';
98
- this.endDate = this.startDate;
99
- if (location === null || location === void 0 ? void 0 : location.getTzid()) {
100
- this.dtargs = `;TZID=${location.getTzid()}`;
101
- }
102
- }
103
- else {
104
- this.endDate = IcalEvent.formatYYYYMMDD(ev.getDate().next().greg());
105
- // for all-day untimed, use DTEND;VALUE=DATE intsead of DURATION:P1D.
106
- // It's more compatible with everthing except ancient versions of
107
- // Lotus Notes circa 2004
108
- this.dtargs = ';VALUE=DATE';
109
- if (mask & core.flags.CHAG) {
110
- this.transp = 'OPAQUE';
111
- this.busyStatus = 'OOF';
112
- }
113
- }
114
- if (opts.emoji) {
115
- const prefix = ev.getEmoji();
116
- if (prefix) {
117
- if (mask & core.flags.OMER_COUNT) {
118
- subj = subj + ' ' + prefix;
119
- }
120
- else {
121
- subj = prefix + ' ' + subj;
122
- }
123
- }
124
- }
125
- // make subject safe for iCalendar
126
- subj = IcalEvent.escape(subj);
127
- if (opts.appendHebrewToSubject) {
128
- const hebrew = ev.renderBrief('he');
129
- if (hebrew) {
130
- subj += ` / ${hebrew}`;
131
- }
132
- }
133
- this.subj = subj;
134
- this.category = ev.category || CATEGORY[(_a = restApi.getEventCategories(ev)) === null || _a === void 0 ? void 0 : _a[0]];
135
- }
136
- getAlarm() {
137
- const ev = this.ev;
138
- const mask = ev.getFlags();
139
- const evAlarm = ev.alarm;
140
- if (typeof evAlarm === 'string') {
141
- return 'TRIGGER:' + evAlarm;
142
- }
143
- else if (typeof evAlarm === 'boolean' && !evAlarm) {
144
- return null;
145
- }
146
- else if (core.greg.isDate(evAlarm)) {
147
- const alarmDt = evAlarm;
148
- alarmDt.setSeconds(0);
149
- return 'TRIGGER;VALUE=DATE-TIME:' + IcalEvent.makeDtstamp(alarmDt);
150
- }
151
- else if (mask & core.flags.OMER_COUNT) {
152
- return 'TRIGGER:-P0DT3H30M0S'; // 8:30pm Omer alarm evening before
153
- }
154
- else if (mask & core.flags.USER_EVENT) {
155
- return 'TRIGGER:-P0DT12H0M0S'; // noon the day before
156
- }
157
- else if (this.timed && ev.getDesc().startsWith('Candle lighting')) {
158
- return 'TRIGGER:-P0DT0H10M0S';
159
- }
160
- return null;
161
- }
162
- /**
163
- * @return {string}
164
- */
165
- getUid() {
166
- if (this.ev.uid) {
167
- return this.ev.uid;
168
- }
169
- const options = this.options;
170
- const digest = murmurhash3.murmur32HexSync(this.ev.getDesc());
171
- let uid = `hebcal-${this.isoDateOnly}-${digest}`;
172
- const loc = options.location;
173
- if (this.timed && loc) {
174
- if (loc.getGeoId()) {
175
- uid += `-${loc.getGeoId()}`;
176
- }
177
- else if (loc.getName()) {
178
- uid += '-' + restApi.makeAnchor(loc.getName());
179
- }
180
- }
181
- return uid;
182
- }
183
- /**
184
- * @return {string[]}
185
- */
186
- getLongLines() {
187
- if (this.lines)
188
- return this.lines;
189
- const categoryLine = this.category ? [`CATEGORIES:${this.category}`] : [];
190
- const uid = this.getUid();
191
- if (this.sequence) {
192
- categoryLine.unshift(`SEQUENCE:${this.sequence}`);
193
- }
194
- const arr = this.lines = [
195
- 'BEGIN:VEVENT',
196
- `DTSTAMP:${this.dtstamp}`,
197
- ].concat(categoryLine).concat([
198
- `SUMMARY:${this.subj}`,
199
- `DTSTART${this.dtargs}:${this.startDate}`,
200
- `DTEND${this.dtargs}:${this.endDate}`,
201
- `UID:${uid}`,
202
- `TRANSP:${this.transp}`,
203
- `X-MICROSOFT-CDO-BUSYSTATUS:${this.busyStatus}`,
204
- ]);
205
- if (!this.timed) {
206
- arr.push('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE');
207
- }
208
- const ev = this.ev;
209
- const mask = ev.getFlags();
210
- const isUserEvent = Boolean(mask & core.flags.USER_EVENT);
211
- if (!isUserEvent) {
212
- arr.push('CLASS:PUBLIC');
213
- }
214
- const options = this.options;
215
- // create memo (holiday descr, Torah, etc)
216
- const memo = createMemo(ev, options);
217
- addOptional(arr, 'DESCRIPTION', memo);
218
- addOptional(arr, 'LOCATION', this.locationName);
219
- const loc = options.location;
220
- if (this.timed && loc) {
221
- arr.push('GEO:' + loc.getLatitude() + ';' + loc.getLongitude());
222
- }
223
- const trigger = this.getAlarm();
224
- if (trigger) {
225
- arr.push('BEGIN:VALARM', 'ACTION:DISPLAY', 'DESCRIPTION:Event reminder', `${trigger}`, 'END:VALARM');
226
- }
227
- arr.push('END:VEVENT');
228
- return arr;
229
- }
230
- /**
231
- * @return {string}
232
- */
233
- toString() {
234
- return this.getLines().join('\r\n');
235
- }
236
- /**
237
- * fold lines to 75 characters
238
- * @return {string[]}
239
- */
240
- getLines() {
241
- return this.getLongLines().map(IcalEvent.fold);
242
- }
243
- /**
244
- * fold line to 75 characters
245
- * @param {string} line
246
- * @return {string}
247
- */
248
- static fold(line) {
249
- let isASCII = true;
250
- for (let i = 0; i < line.length; i++) {
251
- if (line.charCodeAt(i) > 255) {
252
- isASCII = false;
253
- break;
254
- }
255
- }
256
- if (isASCII) {
257
- if (line.length <= 74) {
258
- return line;
259
- }
260
- const matches = line.match(char74re);
261
- return matches.join('\r\n ');
262
- }
263
- if (encoder.encode(line).length <= 74) {
264
- return line;
265
- }
266
- // iterate unicode character by character, making sure
267
- // that adding a new character would keep the line <= 75 octets
268
- let result = '';
269
- let current = '';
270
- let len = 0;
271
- for (let i = 0; i < line.length; i++) {
272
- const char = line[i];
273
- const octets = char.charCodeAt(0) < 256 ? 1 : encoder.encode(char).length;
274
- const newlen = len + octets;
275
- if (newlen < 75) {
276
- current += char;
277
- len = newlen;
278
- }
279
- else {
280
- result += current + '\r\n ';
281
- line = line.substring(i);
282
- current = '';
283
- len = 0;
284
- i = -1;
285
- }
286
- }
287
- return result + current;
288
- }
289
- /**
290
- * @param {string} str
291
- * @return {string}
292
- */
293
- static escape(str) {
294
- if (str.indexOf(',') !== -1) {
295
- str = str.replace(/,/g, '\\,');
296
- }
297
- if (str.indexOf(';') !== -1) {
298
- str = str.replace(/;/g, '\\;');
299
- }
300
- return str;
301
- }
302
- /**
303
- * @param {Date} dt
304
- * @return {string}
305
- */
306
- static formatYYYYMMDD(dt) {
307
- return restApi.pad4(dt.getFullYear()) +
308
- restApi.pad2(dt.getMonth() + 1) + restApi.pad2(dt.getDate());
309
- }
310
- /**
311
- * Returns UTC string for iCalendar
312
- * @param {Date} dt
313
- * @return {string}
314
- */
315
- static makeDtstamp(dt) {
316
- const s = dt.toISOString();
317
- return s.slice(0, 4) + s.slice(5, 7) + s.slice(8, 13) +
318
- s.slice(14, 16) + s.slice(17, 19) + 'Z';
319
- }
320
- /** @return {string} */
321
- static version() {
322
- return version;
323
- }
324
- }
325
- /**
326
- * Transforms a single Event into a VEVENT string
327
- * @return {string} multi-line result, delimited by \r\n
328
- */
329
- function eventToIcal(ev, options) {
330
- const ical = new IcalEvent(ev, options);
331
- return ical.toString();
332
- }
333
- const torahMemoCache = new Map();
334
- const HOLIDAY_IGNORE_MASK = DAILY_LEARNING | core.flags.OMER_COUNT |
335
- core.flags.SHABBAT_MEVARCHIM | core.flags.MOLAD | core.flags.USER_EVENT |
336
- core.flags.HEBREW_DATE;
337
- /**
338
- * @private
339
- */
340
- function makeTorahMemo(ev, il) {
341
- if ((ev.getFlags() & HOLIDAY_IGNORE_MASK) || ev.eventTime) {
342
- return '';
343
- }
344
- const hd = ev.getDate();
345
- const yy = hd.getFullYear();
346
- const mm = hd.getMonth();
347
- const dd = hd.getDate();
348
- const key = [yy, mm, dd, il ? '1' : '0', ev.getDesc()].join('-');
349
- let memo = torahMemoCache.get(key);
350
- if (typeof memo === 'string') {
351
- return memo;
352
- }
353
- memo = restApi.makeTorahMemoText(ev, il).replace(/\n/g, '\\n');
354
- torahMemoCache.set(key, memo);
355
- return memo;
356
- }
357
- /**
358
- * @private
359
- */
360
- function createMemo(ev, options) {
361
- let memo = ev.memo;
362
- if (typeof memo === 'string' && memo.length && memo.indexOf('\n') !== -1) {
363
- memo = memo.replace(/\n/g, '\\n');
364
- }
365
- const desc = ev.getDesc();
366
- if (desc === 'Havdalah' || desc === 'Candle lighting') {
367
- return memo || '';
368
- }
369
- const mask = ev.getFlags();
370
- if (mask & core.flags.OMER_COUNT) {
371
- const omerEv = ev;
372
- const sefira = [omerEv.sefira('en'), omerEv.sefira('he'), omerEv.sefira('translit')].join('\\n');
373
- return omerEv.getTodayIs('en') + '\\n\\n' + omerEv.getTodayIs('he') + '\\n\\n' + sefira;
374
- }
375
- const url = appendTrackingToUrl(ev.url(), options);
376
- const torahMemo = makeTorahMemo(ev, options.il);
377
- if (!memo) {
378
- if (typeof ev.linkedEvent !== 'undefined') {
379
- memo = ev.linkedEvent.render(options.locale);
380
- }
381
- else {
382
- memo = restApi.getHolidayDescription(ev);
383
- }
384
- }
385
- if (torahMemo) {
386
- if (memo.length) {
387
- memo += '\\n\\n';
388
- }
389
- memo += torahMemo;
390
- }
391
- if (url) {
392
- if (memo.length) {
393
- memo += '\\n\\n';
394
- }
395
- memo += url;
396
- }
397
- return memo;
398
- }
399
- /**
400
- * Generates an RFC 2445 iCalendar string from an array of events
401
- */
402
- async function eventsToIcalendar(events, options) {
403
- if (!options)
404
- throw new TypeError('Invalid options object');
405
- const opts = Object.assign({}, options);
406
- opts.dtstamp = opts.dtstamp || IcalEvent.makeDtstamp(new Date());
407
- if (!opts.title) {
408
- opts.title = restApi.getCalendarTitle(events, opts);
409
- }
410
- const icals = events.map((ev) => new IcalEvent(ev, opts));
411
- return icalEventsToString(icals, opts);
412
- }
413
- /**
414
- * Generates an RFC 2445 iCalendar string from an array of IcalEvents
415
- */
416
- async function icalEventsToString(icals, options) {
417
- const stream = [];
418
- const locale = options.locale || core.Locale.getLocaleName();
419
- const uclang = locale.toUpperCase();
420
- const opts = Object.assign({}, options);
421
- opts.dtstamp = opts.dtstamp || IcalEvent.makeDtstamp(new Date());
422
- const title = opts.title ? IcalEvent.escape(opts.title) : 'Untitled';
423
- const caldesc = opts.caldesc ? IcalEvent.escape(opts.caldesc) :
424
- opts.yahrzeit ?
425
- 'Yahrzeits + Anniversaries from www.hebcal.com' :
426
- 'Jewish Holidays from www.hebcal.com';
427
- const prodid = opts.prodid || `-//hebcal.com/NONSGML Hebcal Calendar v1${version}//${uclang}`;
428
- const preamble = [
429
- 'BEGIN:VCALENDAR',
430
- 'VERSION:2.0',
431
- `PRODID:${prodid}`,
432
- 'CALSCALE:GREGORIAN',
433
- 'METHOD:PUBLISH',
434
- 'X-LOTUS-CHARSET:UTF-8',
435
- ];
436
- if (opts.publishedTTL !== false) {
437
- const publishedTTL = opts.publishedTTL || 'PT7D';
438
- preamble.push(`X-PUBLISHED-TTL:${publishedTTL}`);
439
- }
440
- preamble.push(`X-WR-CALNAME:${title}`);
441
- preamble.push(`X-WR-CALDESC:${caldesc}`);
442
- for (const line of preamble.map(IcalEvent.fold)) {
443
- stream.push(line);
444
- stream.push('\r\n');
445
- }
446
- if (opts.relcalid) {
447
- stream.push(IcalEvent.fold(`X-WR-RELCALID:${opts.relcalid}`));
448
- stream.push('\r\n');
449
- }
450
- if (opts.calendarColor) {
451
- stream.push(`X-APPLE-CALENDAR-COLOR:${opts.calendarColor}\r\n`);
452
- }
453
- const location = opts.location;
454
- const tzid = location === null || location === void 0 ? void 0 : location.getTzid();
455
- if (tzid) {
456
- stream.push(`X-WR-TIMEZONE;VALUE=TEXT:${tzid}\r\n`);
457
- const vtz = vtimezoneCache.get(tzid);
458
- if (typeof vtz === 'string') {
459
- stream.push(vtz);
460
- stream.push('\r\n');
461
- }
462
- else {
463
- const vtimezoneFilename = `./zoneinfo/${tzid}.ics`;
464
- try {
465
- const vtimezoneIcs = await fs.promises.readFile(vtimezoneFilename, 'utf-8');
466
- const lines = vtimezoneIcs.split('\r\n');
467
- // ignore first 3 and last 1 lines
468
- const str = lines.slice(3, lines.length - 2).join('\r\n');
469
- stream.push(str);
470
- stream.push('\r\n');
471
- vtimezoneCache.set(tzid, str);
472
- }
473
- catch (error) {
474
- // ignore failure when no timezone definition to read
475
- }
476
- }
477
- }
478
- for (const ical of icals) {
479
- const lines = ical.getLines();
480
- for (const line of lines) {
481
- stream.push(line);
482
- stream.push('\r\n');
483
- }
484
- }
485
- stream.push('END:VCALENDAR\r\n');
486
- return stream.join('');
487
- }
488
-
489
- exports.IcalEvent = IcalEvent;
490
- exports.eventToIcal = eventToIcal;
491
- exports.eventsToIcalendar = eventsToIcalendar;
492
- exports.icalEventsToString = icalEventsToString;