@hebcal/icalendar 4.18.2 → 4.18.4

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.
Files changed (3) hide show
  1. package/dist/index.js +34 -107
  2. package/dist/index.mjs +34 -105
  3. package/package.json +12 -12
package/dist/index.js CHANGED
@@ -1,20 +1,19 @@
1
- /*! @hebcal/icalendar v4.18.2 */
1
+ /*! @hebcal/icalendar v4.18.4 */
2
2
  'use strict';
3
3
 
4
- Object.defineProperty(exports, '__esModule', { value: true });
5
-
6
4
  var core = require('@hebcal/core');
7
5
  var murmurhash3 = require('murmurhash3');
8
6
  var restApi = require('@hebcal/rest-api');
9
7
  var fs = require('fs');
10
8
 
11
- const version="4.18.2";
9
+ const version="4.18.4";
12
10
 
13
11
  const VTIMEZONE = {};
14
12
  const CATEGORY = {
15
13
  candles: 'Holiday',
16
14
  dafyomi: 'Daf Yomi',
17
15
  mishnayomi: 'Mishna Yomi',
16
+ yerushalmi: 'Yerushalmi Yomi',
18
17
  havdalah: 'Holiday',
19
18
  hebdate: null,
20
19
  holiday: 'Holiday',
@@ -26,44 +25,41 @@ const CATEGORY = {
26
25
  user: 'Personal',
27
26
  zmanim: null
28
27
  };
28
+
29
29
  /**
30
30
  * @private
31
31
  * @param {string[]} arr
32
32
  * @param {string} key
33
33
  * @param {string} val
34
34
  */
35
-
36
35
  function addOptional(arr, key, val) {
37
36
  if (val) {
38
37
  const str = IcalEvent.escape(val);
39
38
  arr.push(key + ':' + str);
40
39
  }
41
40
  }
41
+
42
42
  /**
43
43
  * @private
44
44
  * @param {string} url
45
45
  * @param {HebrewCalendar.Options} options
46
46
  * @return {string}
47
47
  */
48
-
49
-
50
48
  function appendTrackingToUrl(url, options) {
51
49
  if (!url) {
52
50
  return null;
53
51
  }
54
-
55
52
  const utmSource = options.utmSource || 'js';
56
53
  const utmMedium = options.utmMedium || 'icalendar';
57
54
  const utmCampaign = options.utmCampaign;
58
55
  return restApi.appendIsraelAndTracking(url, options.il, utmSource, utmMedium, utmCampaign);
59
56
  }
60
-
61
57
  const encoder = new TextEncoder();
62
58
  const char74re = /(.{1,74})/g;
59
+
63
60
  /**
64
61
  * Represents an RFC 2445 iCalendar VEVENT
65
62
  */
66
-
67
63
  class IcalEvent {
68
64
  /**
69
65
  * Builds an IcalEvent object from a Hebcal Event
@@ -78,7 +74,6 @@ class IcalEvent {
78
74
  const locale = options.locale;
79
75
  let subj = restApi.shouldRenderBrief(ev) ? ev.renderBrief(locale) : ev.render(locale);
80
76
  const mask = ev.getFlags();
81
-
82
77
  if (ev.locationName) {
83
78
  this.locationName = ev.locationName;
84
79
  } else if (mask & core.flags.DAF_YOMI) {
@@ -89,39 +84,33 @@ class IcalEvent {
89
84
  const comma = options.location.name.indexOf(',');
90
85
  this.locationName = comma == -1 ? options.location.name : options.location.name.substring(0, comma);
91
86
  }
92
-
93
87
  const date = IcalEvent.formatYYYYMMDD(ev.getDate().greg());
94
88
  this.startDate = date;
95
89
  this.dtargs = '';
96
90
  this.transp = 'TRANSPARENT';
97
91
  this.busyStatus = 'FREE';
98
-
99
92
  if (timed) {
100
93
  let [hour, minute] = ev.eventTimeStr.split(':');
101
94
  hour = +hour;
102
95
  minute = +minute;
103
96
  this.startDate += 'T' + restApi.pad2(hour) + restApi.pad2(minute) + '00';
104
97
  this.endDate = this.startDate;
105
-
106
98
  if (options.location && options.location.tzid) {
107
99
  this.dtargs = `;TZID=${options.location.tzid}`;
108
100
  }
109
101
  } else {
110
- this.endDate = IcalEvent.formatYYYYMMDD(ev.getDate().next().greg()); // for all-day untimed, use DTEND;VALUE=DATE intsead of DURATION:P1D.
102
+ this.endDate = IcalEvent.formatYYYYMMDD(ev.getDate().next().greg());
103
+ // for all-day untimed, use DTEND;VALUE=DATE intsead of DURATION:P1D.
111
104
  // It's more compatible with everthing except ancient versions of
112
105
  // Lotus Notes circa 2004
113
-
114
106
  this.dtargs = ';VALUE=DATE';
115
-
116
107
  if (mask & core.flags.CHAG) {
117
108
  this.transp = 'OPAQUE';
118
109
  this.busyStatus = 'OOF';
119
110
  }
120
111
  }
121
-
122
112
  if (options.emoji) {
123
113
  const prefix = ev.getEmoji();
124
-
125
114
  if (prefix) {
126
115
  if (mask & core.flags.OMER_COUNT) {
127
116
  subj = subj + ' ' + prefix;
@@ -129,32 +118,27 @@ class IcalEvent {
129
118
  subj = prefix + ' ' + subj;
130
119
  }
131
120
  }
132
- } // make subject safe for iCalendar
133
-
121
+ }
134
122
 
123
+ // make subject safe for iCalendar
135
124
  subj = IcalEvent.escape(subj);
136
-
137
125
  if (options.appendHebrewToSubject) {
138
126
  const hebrew = ev.renderBrief('he');
139
-
140
127
  if (hebrew) {
141
128
  subj += ` / ${hebrew}`;
142
129
  }
143
130
  }
144
-
145
131
  this.subj = subj;
146
132
  this.category = ev.category || CATEGORY[restApi.getEventCategories(ev)[0]];
147
133
  }
134
+
148
135
  /**
149
136
  * @return {string}
150
137
  */
151
-
152
-
153
138
  getAlarm() {
154
139
  const ev = this.ev;
155
140
  const mask = ev.getFlags();
156
141
  const evAlarm = ev.alarm;
157
-
158
142
  if (typeof evAlarm === 'string') {
159
143
  return 'TRIGGER:' + evAlarm;
160
144
  } else if (core.greg.isDate(evAlarm)) {
@@ -167,19 +151,16 @@ class IcalEvent {
167
151
  } else if (this.timed && ev.getDesc().startsWith('Candle lighting')) {
168
152
  return 'TRIGGER:-P0DT0H10M0S';
169
153
  }
170
-
171
154
  return null;
172
155
  }
156
+
173
157
  /**
174
158
  * @return {string}
175
159
  */
176
-
177
-
178
160
  getUid() {
179
161
  const options = this.options;
180
162
  const digest = murmurhash3.murmur32HexSync(this.ev.getDesc());
181
163
  let uid = `hebcal-${this.startDate}-${digest}`;
182
-
183
164
  if (this.timed && options.location) {
184
165
  if (options.location.geoid) {
185
166
  uid += `-${options.location.geoid}`;
@@ -187,104 +168,85 @@ class IcalEvent {
187
168
  uid += '-' + restApi.makeAnchor(options.location.name);
188
169
  }
189
170
  }
190
-
191
171
  return uid;
192
172
  }
173
+
193
174
  /**
194
175
  * @return {string[]}
195
176
  */
196
-
197
-
198
177
  getLongLines() {
199
178
  if (this.lines) return this.lines;
200
179
  const categoryLine = this.category ? `CATEGORIES:${this.category}` : [];
201
180
  const uid = this.ev.uid || this.getUid();
202
181
  const arr = this.lines = ['BEGIN:VEVENT', `DTSTAMP:${this.dtstamp}`].concat(categoryLine).concat([`SUMMARY:${this.subj}`, `DTSTART${this.dtargs}:${this.startDate}`, `DTEND${this.dtargs}:${this.endDate}`, `UID:${uid}`, `TRANSP:${this.transp}`, `X-MICROSOFT-CDO-BUSYSTATUS:${this.busyStatus}`]);
203
-
204
182
  if (!this.timed) {
205
183
  arr.push('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE');
206
184
  }
207
-
208
185
  const ev = this.ev;
209
186
  const mask = ev.getFlags();
210
187
  const isUserEvent = Boolean(mask & core.flags.USER_EVENT);
211
-
212
188
  if (!isUserEvent) {
213
189
  arr.push('CLASS:PUBLIC');
214
190
  }
215
-
216
- const options = this.options; // create memo (holiday descr, Torah, etc)
217
-
191
+ const options = this.options;
192
+ // create memo (holiday descr, Torah, etc)
218
193
  const memo = createMemo(ev, options);
219
194
  addOptional(arr, 'DESCRIPTION', memo);
220
195
  addOptional(arr, 'LOCATION', this.locationName);
221
-
222
196
  if (this.timed && options.location) {
223
197
  arr.push('GEO:' + options.location.latitude + ';' + options.location.longitude);
224
198
  }
225
-
226
199
  const trigger = this.getAlarm();
227
-
228
200
  if (trigger) {
229
201
  arr.push('BEGIN:VALARM', 'ACTION:DISPLAY', 'DESCRIPTION:Event reminder', `${trigger}`, 'END:VALARM');
230
202
  }
231
-
232
203
  arr.push('END:VEVENT');
233
204
  return arr;
234
205
  }
206
+
235
207
  /**
236
208
  * @return {string}
237
209
  */
238
-
239
-
240
210
  toString() {
241
211
  return this.getLines().join('\r\n');
242
212
  }
213
+
243
214
  /**
244
215
  * fold lines to 75 characters
245
216
  * @return {string[]}
246
217
  */
247
-
248
-
249
218
  getLines() {
250
219
  return this.getLongLines().map(IcalEvent.fold);
251
220
  }
221
+
252
222
  /**
253
223
  * fold line to 75 characters
254
224
  * @param {string} line
255
225
  * @return {string}
256
226
  */
257
-
258
-
259
227
  static fold(line) {
260
228
  let isASCII = true;
261
-
262
229
  for (let i = 0; i < line.length; i++) {
263
230
  if (line.charCodeAt(i) > 255) {
264
231
  isASCII = false;
265
232
  break;
266
233
  }
267
234
  }
268
-
269
235
  if (isASCII) {
270
236
  return line.length <= 74 ? line : line.match(char74re).join('\r\n ');
271
237
  }
272
-
273
238
  if (encoder.encode(line).length <= 74) {
274
239
  return line;
275
- } // iterate unicode character by character, making sure
240
+ }
241
+ // iterate unicode character by character, making sure
276
242
  // that adding a new character would keep the line <= 75 octets
277
-
278
-
279
243
  let result = '';
280
244
  let current = '';
281
245
  let len = 0;
282
-
283
246
  for (let i = 0; i < line.length; i++) {
284
247
  const char = line[i];
285
248
  const octets = char.charCodeAt(0) < 256 ? 1 : encoder.encode(char).length;
286
249
  const newlen = len + octets;
287
-
288
250
  if (newlen < 75) {
289
251
  current += char;
290
252
  len = newlen;
@@ -296,176 +258,150 @@ class IcalEvent {
296
258
  i = -1;
297
259
  }
298
260
  }
299
-
300
261
  return result + current;
301
262
  }
263
+
302
264
  /**
303
265
  * @param {string} str
304
266
  * @return {string}
305
267
  */
306
-
307
-
308
268
  static escape(str) {
309
269
  if (str.indexOf(',') !== -1) {
310
270
  str = str.replace(/,/g, '\\,');
311
271
  }
312
-
313
272
  if (str.indexOf(';') !== -1) {
314
273
  str = str.replace(/;/g, '\\;');
315
274
  }
316
-
317
275
  return str;
318
276
  }
277
+
319
278
  /**
320
279
  * @param {Date} dt
321
280
  * @return {string}
322
281
  */
323
-
324
-
325
282
  static formatYYYYMMDD(dt) {
326
283
  return restApi.pad4(dt.getFullYear()) + restApi.pad2(dt.getMonth() + 1) + restApi.pad2(dt.getDate());
327
284
  }
285
+
328
286
  /**
329
287
  * Returns UTC string for iCalendar
330
288
  * @param {Date} dt
331
289
  * @return {string}
332
290
  */
333
-
334
-
335
291
  static makeDtstamp(dt) {
336
292
  const s = dt.toISOString();
337
293
  return s.slice(0, 4) + s.slice(5, 7) + s.slice(8, 13) + s.slice(14, 16) + s.slice(17, 19) + 'Z';
338
294
  }
339
- /** @return {string} */
340
-
341
295
 
296
+ /** @return {string} */
342
297
  static version() {
343
298
  return version;
344
299
  }
345
-
346
300
  }
301
+
347
302
  /**
348
303
  * Transforms a single Event into a VEVENT string
349
304
  * @param {Event} ev
350
305
  * @param {HebrewCalendar.Options} options
351
306
  * @return {string} multi-line result, delimited by \r\n
352
307
  */
353
-
354
308
  function eventToIcal(ev, options) {
355
309
  const ical = new IcalEvent(ev, options);
356
310
  return ical.toString();
357
311
  }
358
312
  const torahMemoCache = new Map();
359
- const HOLIDAY_IGNORE_MASK = core.flags.DAF_YOMI | core.flags.OMER_COUNT | core.flags.SHABBAT_MEVARCHIM | core.flags.MOLAD | core.flags.USER_EVENT | core.flags.MISHNA_YOMI | core.flags.HEBREW_DATE;
313
+ const HOLIDAY_IGNORE_MASK = core.flags.DAF_YOMI | core.flags.OMER_COUNT | core.flags.SHABBAT_MEVARCHIM | core.flags.MOLAD | core.flags.USER_EVENT | core.flags.MISHNA_YOMI | core.flags.YERUSHALMI_YOMI | core.flags.HEBREW_DATE;
314
+
360
315
  /**
361
316
  * @private
362
317
  * @param {Event} ev
363
318
  * @param {boolean} il
364
319
  * @return {string}
365
320
  */
366
-
367
321
  function makeTorahMemo(ev, il) {
368
322
  if (ev.getFlags() & HOLIDAY_IGNORE_MASK || ev.eventTime) {
369
323
  return '';
370
324
  }
371
-
372
325
  const hd = ev.getDate();
373
326
  const yy = hd.getFullYear();
374
327
  const mm = hd.getMonth();
375
328
  const dd = hd.getDate();
376
329
  const key = [yy, mm, dd, il ? '1' : '0', ev.getDesc()].join('-');
377
330
  let memo = torahMemoCache.get(key);
378
-
379
331
  if (typeof memo === 'string') {
380
332
  return memo;
381
333
  }
382
-
383
334
  memo = restApi.makeTorahMemoText(ev, il).replace(/\n/g, '\\n');
384
335
  torahMemoCache.set(key, memo);
385
336
  return memo;
386
337
  }
338
+
387
339
  /**
388
340
  * @private
389
341
  * @param {Event} e
390
342
  * @param {HebrewCalendar.Options} options
391
343
  * @return {string}
392
344
  */
393
-
394
-
395
345
  function createMemo(e, options) {
396
346
  const desc = e.getDesc();
397
347
  const candles = desc === 'Havdalah' || desc === 'Candle lighting';
398
-
399
348
  if (typeof e.memo === 'string' && e.memo.length && e.memo.indexOf('\n') !== -1) {
400
349
  e.memo = e.memo.replace(/\n/g, '\\n');
401
350
  }
402
-
403
351
  if (candles) {
404
352
  return e.memo || '';
405
353
  }
406
-
407
354
  const mask = e.getFlags();
408
-
409
355
  if (mask & core.flags.OMER_COUNT) {
410
356
  const sefira = [e.sefira('en'), e.sefira('he'), e.sefira('translit')].join('\\n');
411
357
  return e.getTodayIs('en') + '\\n\\n' + e.getTodayIs('he') + '\\n\\n' + sefira;
412
358
  }
413
-
414
359
  const url = appendTrackingToUrl(e.url(), options);
415
360
  const torahMemo = makeTorahMemo(e, options.il);
416
-
417
361
  if (mask & core.flags.PARSHA_HASHAVUA) {
418
362
  return torahMemo + '\\n\\n' + url;
419
363
  } else {
420
364
  let memo = e.memo || restApi.getHolidayDescription(e);
421
-
422
365
  if (!memo && typeof e.linkedEvent !== 'undefined') {
423
366
  memo = e.linkedEvent.render(options.locale);
424
367
  }
425
-
426
368
  if (torahMemo) {
427
369
  memo += '\\n\\n' + torahMemo;
428
370
  }
429
-
430
371
  if (url) {
431
372
  if (memo.length) {
432
373
  memo += '\\n\\n';
433
374
  }
434
-
435
375
  memo += url;
436
376
  }
437
-
438
377
  return memo;
439
378
  }
440
379
  }
380
+
441
381
  /**
442
382
  * Generates an RFC 2445 iCalendar string from an array of events
443
383
  * @param {Event[]} events
444
384
  * @param {HebrewCalendar.Options} options
445
385
  * @return {string}
446
386
  */
447
-
448
-
449
387
  async function eventsToIcalendar(events, options) {
450
388
  if (!events.length) throw new RangeError('Events can not be empty');
451
389
  if (!options) throw new TypeError('Invalid options object');
452
390
  const opts = Object.assign({}, options);
453
391
  opts.dtstamp = opts.dtstamp || IcalEvent.makeDtstamp(new Date());
454
-
455
392
  if (!opts.title) {
456
393
  opts.title = restApi.getCalendarTitle(events, opts);
457
394
  }
458
-
459
395
  const icals = events.map(ev => new IcalEvent(ev, opts));
460
396
  return icalEventsToString(icals, opts);
461
397
  }
398
+
462
399
  /**
463
400
  * Generates an RFC 2445 iCalendar string from an array of IcalEvents
464
401
  * @param {IcalEvent[]} icals
465
402
  * @param {HebrewCalendar.Options} options
466
403
  * @return {string}
467
404
  */
468
-
469
405
  async function icalEventsToString(icals, options) {
470
406
  const stream = [];
471
407
  const uclang = core.Locale.getLocaleName().toUpperCase();
@@ -476,55 +412,46 @@ async function icalEventsToString(icals, options) {
476
412
  const publishedTTL = opts.publishedTTL || 'PT7D';
477
413
  const prodid = opts.prodid || `-//hebcal.com/NONSGML Hebcal Calendar v1${version}//${uclang}`;
478
414
  const preamble = ['BEGIN:VCALENDAR', 'VERSION:2.0', `PRODID:${prodid}`, 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', 'X-LOTUS-CHARSET:UTF-8', `X-PUBLISHED-TTL:${publishedTTL}`, `X-WR-CALNAME:${title}`, `X-WR-CALDESC:${caldesc}`];
479
-
480
415
  for (const line of preamble.map(IcalEvent.fold)) {
481
416
  stream.push(line);
482
417
  stream.push('\r\n');
483
418
  }
484
-
485
419
  if (opts.relcalid) {
486
420
  stream.push(IcalEvent.fold(`X-WR-RELCALID:${opts.relcalid}`));
487
421
  stream.push('\r\n');
488
422
  }
489
-
490
423
  if (opts.calendarColor) {
491
424
  stream.push(`X-APPLE-CALENDAR-COLOR:${opts.calendarColor}\r\n`);
492
425
  }
493
-
494
426
  const location = opts.location;
495
-
496
427
  if (location && location.tzid) {
497
428
  const tzid = location.tzid;
498
429
  stream.push(`X-WR-TIMEZONE;VALUE=TEXT:${tzid}\r\n`);
499
-
500
430
  if (VTIMEZONE[tzid]) {
501
431
  stream.push(VTIMEZONE[tzid]);
502
432
  stream.push('\r\n');
503
433
  } else {
504
434
  const vtimezoneFilename = `./zoneinfo/${tzid}.ics`;
505
-
506
435
  try {
507
436
  const vtimezoneIcs = await fs.promises.readFile(vtimezoneFilename, 'utf-8');
508
- const lines = vtimezoneIcs.split('\r\n'); // ignore first 3 and last 1 lines
509
-
437
+ const lines = vtimezoneIcs.split('\r\n');
438
+ // ignore first 3 and last 1 lines
510
439
  const str = lines.slice(3, lines.length - 2).join('\r\n');
511
440
  stream.push(str);
512
441
  stream.push('\r\n');
513
442
  VTIMEZONE[tzid] = str; // cache for later
514
- } catch (error) {// ignore failure when no timezone definition to read
443
+ } catch (error) {
444
+ // ignore failure when no timezone definition to read
515
445
  }
516
446
  }
517
447
  }
518
-
519
448
  for (const ical of icals) {
520
449
  const lines = ical.getLines();
521
-
522
450
  for (const line of lines) {
523
451
  stream.push(line);
524
452
  stream.push('\r\n');
525
453
  }
526
454
  }
527
-
528
455
  stream.push('END:VCALENDAR\r\n');
529
456
  return stream.join('');
530
457
  }
package/dist/index.mjs CHANGED
@@ -1,16 +1,17 @@
1
- /*! @hebcal/icalendar v4.18.2 */
1
+ /*! @hebcal/icalendar v4.18.4 */
2
2
  import { flags, Locale, greg } from '@hebcal/core';
3
3
  import { murmur32HexSync } from 'murmurhash3';
4
4
  import { shouldRenderBrief, pad2, getEventCategories, makeAnchor, pad4, getHolidayDescription, getCalendarTitle, appendIsraelAndTracking, makeTorahMemoText } from '@hebcal/rest-api';
5
5
  import { promises } from 'fs';
6
6
 
7
- const version="4.18.2";
7
+ const version="4.18.4";
8
8
 
9
9
  const VTIMEZONE = {};
10
10
  const CATEGORY = {
11
11
  candles: 'Holiday',
12
12
  dafyomi: 'Daf Yomi',
13
13
  mishnayomi: 'Mishna Yomi',
14
+ yerushalmi: 'Yerushalmi Yomi',
14
15
  havdalah: 'Holiday',
15
16
  hebdate: null,
16
17
  holiday: 'Holiday',
@@ -22,44 +23,41 @@ const CATEGORY = {
22
23
  user: 'Personal',
23
24
  zmanim: null
24
25
  };
26
+
25
27
  /**
26
28
  * @private
27
29
  * @param {string[]} arr
28
30
  * @param {string} key
29
31
  * @param {string} val
30
32
  */
31
-
32
33
  function addOptional(arr, key, val) {
33
34
  if (val) {
34
35
  const str = IcalEvent.escape(val);
35
36
  arr.push(key + ':' + str);
36
37
  }
37
38
  }
39
+
38
40
  /**
39
41
  * @private
40
42
  * @param {string} url
41
43
  * @param {HebrewCalendar.Options} options
42
44
  * @return {string}
43
45
  */
44
-
45
-
46
46
  function appendTrackingToUrl(url, options) {
47
47
  if (!url) {
48
48
  return null;
49
49
  }
50
-
51
50
  const utmSource = options.utmSource || 'js';
52
51
  const utmMedium = options.utmMedium || 'icalendar';
53
52
  const utmCampaign = options.utmCampaign;
54
53
  return appendIsraelAndTracking(url, options.il, utmSource, utmMedium, utmCampaign);
55
54
  }
56
-
57
55
  const encoder = new TextEncoder();
58
56
  const char74re = /(.{1,74})/g;
57
+
59
58
  /**
60
59
  * Represents an RFC 2445 iCalendar VEVENT
61
60
  */
62
-
63
61
  class IcalEvent {
64
62
  /**
65
63
  * Builds an IcalEvent object from a Hebcal Event
@@ -74,7 +72,6 @@ class IcalEvent {
74
72
  const locale = options.locale;
75
73
  let subj = shouldRenderBrief(ev) ? ev.renderBrief(locale) : ev.render(locale);
76
74
  const mask = ev.getFlags();
77
-
78
75
  if (ev.locationName) {
79
76
  this.locationName = ev.locationName;
80
77
  } else if (mask & flags.DAF_YOMI) {
@@ -85,39 +82,33 @@ class IcalEvent {
85
82
  const comma = options.location.name.indexOf(',');
86
83
  this.locationName = comma == -1 ? options.location.name : options.location.name.substring(0, comma);
87
84
  }
88
-
89
85
  const date = IcalEvent.formatYYYYMMDD(ev.getDate().greg());
90
86
  this.startDate = date;
91
87
  this.dtargs = '';
92
88
  this.transp = 'TRANSPARENT';
93
89
  this.busyStatus = 'FREE';
94
-
95
90
  if (timed) {
96
91
  let [hour, minute] = ev.eventTimeStr.split(':');
97
92
  hour = +hour;
98
93
  minute = +minute;
99
94
  this.startDate += 'T' + pad2(hour) + pad2(minute) + '00';
100
95
  this.endDate = this.startDate;
101
-
102
96
  if (options.location && options.location.tzid) {
103
97
  this.dtargs = `;TZID=${options.location.tzid}`;
104
98
  }
105
99
  } else {
106
- this.endDate = IcalEvent.formatYYYYMMDD(ev.getDate().next().greg()); // for all-day untimed, use DTEND;VALUE=DATE intsead of DURATION:P1D.
100
+ this.endDate = IcalEvent.formatYYYYMMDD(ev.getDate().next().greg());
101
+ // for all-day untimed, use DTEND;VALUE=DATE intsead of DURATION:P1D.
107
102
  // It's more compatible with everthing except ancient versions of
108
103
  // Lotus Notes circa 2004
109
-
110
104
  this.dtargs = ';VALUE=DATE';
111
-
112
105
  if (mask & flags.CHAG) {
113
106
  this.transp = 'OPAQUE';
114
107
  this.busyStatus = 'OOF';
115
108
  }
116
109
  }
117
-
118
110
  if (options.emoji) {
119
111
  const prefix = ev.getEmoji();
120
-
121
112
  if (prefix) {
122
113
  if (mask & flags.OMER_COUNT) {
123
114
  subj = subj + ' ' + prefix;
@@ -125,32 +116,27 @@ class IcalEvent {
125
116
  subj = prefix + ' ' + subj;
126
117
  }
127
118
  }
128
- } // make subject safe for iCalendar
129
-
119
+ }
130
120
 
121
+ // make subject safe for iCalendar
131
122
  subj = IcalEvent.escape(subj);
132
-
133
123
  if (options.appendHebrewToSubject) {
134
124
  const hebrew = ev.renderBrief('he');
135
-
136
125
  if (hebrew) {
137
126
  subj += ` / ${hebrew}`;
138
127
  }
139
128
  }
140
-
141
129
  this.subj = subj;
142
130
  this.category = ev.category || CATEGORY[getEventCategories(ev)[0]];
143
131
  }
132
+
144
133
  /**
145
134
  * @return {string}
146
135
  */
147
-
148
-
149
136
  getAlarm() {
150
137
  const ev = this.ev;
151
138
  const mask = ev.getFlags();
152
139
  const evAlarm = ev.alarm;
153
-
154
140
  if (typeof evAlarm === 'string') {
155
141
  return 'TRIGGER:' + evAlarm;
156
142
  } else if (greg.isDate(evAlarm)) {
@@ -163,19 +149,16 @@ class IcalEvent {
163
149
  } else if (this.timed && ev.getDesc().startsWith('Candle lighting')) {
164
150
  return 'TRIGGER:-P0DT0H10M0S';
165
151
  }
166
-
167
152
  return null;
168
153
  }
154
+
169
155
  /**
170
156
  * @return {string}
171
157
  */
172
-
173
-
174
158
  getUid() {
175
159
  const options = this.options;
176
160
  const digest = murmur32HexSync(this.ev.getDesc());
177
161
  let uid = `hebcal-${this.startDate}-${digest}`;
178
-
179
162
  if (this.timed && options.location) {
180
163
  if (options.location.geoid) {
181
164
  uid += `-${options.location.geoid}`;
@@ -183,104 +166,85 @@ class IcalEvent {
183
166
  uid += '-' + makeAnchor(options.location.name);
184
167
  }
185
168
  }
186
-
187
169
  return uid;
188
170
  }
171
+
189
172
  /**
190
173
  * @return {string[]}
191
174
  */
192
-
193
-
194
175
  getLongLines() {
195
176
  if (this.lines) return this.lines;
196
177
  const categoryLine = this.category ? `CATEGORIES:${this.category}` : [];
197
178
  const uid = this.ev.uid || this.getUid();
198
179
  const arr = this.lines = ['BEGIN:VEVENT', `DTSTAMP:${this.dtstamp}`].concat(categoryLine).concat([`SUMMARY:${this.subj}`, `DTSTART${this.dtargs}:${this.startDate}`, `DTEND${this.dtargs}:${this.endDate}`, `UID:${uid}`, `TRANSP:${this.transp}`, `X-MICROSOFT-CDO-BUSYSTATUS:${this.busyStatus}`]);
199
-
200
180
  if (!this.timed) {
201
181
  arr.push('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE');
202
182
  }
203
-
204
183
  const ev = this.ev;
205
184
  const mask = ev.getFlags();
206
185
  const isUserEvent = Boolean(mask & flags.USER_EVENT);
207
-
208
186
  if (!isUserEvent) {
209
187
  arr.push('CLASS:PUBLIC');
210
188
  }
211
-
212
- const options = this.options; // create memo (holiday descr, Torah, etc)
213
-
189
+ const options = this.options;
190
+ // create memo (holiday descr, Torah, etc)
214
191
  const memo = createMemo(ev, options);
215
192
  addOptional(arr, 'DESCRIPTION', memo);
216
193
  addOptional(arr, 'LOCATION', this.locationName);
217
-
218
194
  if (this.timed && options.location) {
219
195
  arr.push('GEO:' + options.location.latitude + ';' + options.location.longitude);
220
196
  }
221
-
222
197
  const trigger = this.getAlarm();
223
-
224
198
  if (trigger) {
225
199
  arr.push('BEGIN:VALARM', 'ACTION:DISPLAY', 'DESCRIPTION:Event reminder', `${trigger}`, 'END:VALARM');
226
200
  }
227
-
228
201
  arr.push('END:VEVENT');
229
202
  return arr;
230
203
  }
204
+
231
205
  /**
232
206
  * @return {string}
233
207
  */
234
-
235
-
236
208
  toString() {
237
209
  return this.getLines().join('\r\n');
238
210
  }
211
+
239
212
  /**
240
213
  * fold lines to 75 characters
241
214
  * @return {string[]}
242
215
  */
243
-
244
-
245
216
  getLines() {
246
217
  return this.getLongLines().map(IcalEvent.fold);
247
218
  }
219
+
248
220
  /**
249
221
  * fold line to 75 characters
250
222
  * @param {string} line
251
223
  * @return {string}
252
224
  */
253
-
254
-
255
225
  static fold(line) {
256
226
  let isASCII = true;
257
-
258
227
  for (let i = 0; i < line.length; i++) {
259
228
  if (line.charCodeAt(i) > 255) {
260
229
  isASCII = false;
261
230
  break;
262
231
  }
263
232
  }
264
-
265
233
  if (isASCII) {
266
234
  return line.length <= 74 ? line : line.match(char74re).join('\r\n ');
267
235
  }
268
-
269
236
  if (encoder.encode(line).length <= 74) {
270
237
  return line;
271
- } // iterate unicode character by character, making sure
238
+ }
239
+ // iterate unicode character by character, making sure
272
240
  // that adding a new character would keep the line <= 75 octets
273
-
274
-
275
241
  let result = '';
276
242
  let current = '';
277
243
  let len = 0;
278
-
279
244
  for (let i = 0; i < line.length; i++) {
280
245
  const char = line[i];
281
246
  const octets = char.charCodeAt(0) < 256 ? 1 : encoder.encode(char).length;
282
247
  const newlen = len + octets;
283
-
284
248
  if (newlen < 75) {
285
249
  current += char;
286
250
  len = newlen;
@@ -292,176 +256,150 @@ class IcalEvent {
292
256
  i = -1;
293
257
  }
294
258
  }
295
-
296
259
  return result + current;
297
260
  }
261
+
298
262
  /**
299
263
  * @param {string} str
300
264
  * @return {string}
301
265
  */
302
-
303
-
304
266
  static escape(str) {
305
267
  if (str.indexOf(',') !== -1) {
306
268
  str = str.replace(/,/g, '\\,');
307
269
  }
308
-
309
270
  if (str.indexOf(';') !== -1) {
310
271
  str = str.replace(/;/g, '\\;');
311
272
  }
312
-
313
273
  return str;
314
274
  }
275
+
315
276
  /**
316
277
  * @param {Date} dt
317
278
  * @return {string}
318
279
  */
319
-
320
-
321
280
  static formatYYYYMMDD(dt) {
322
281
  return pad4(dt.getFullYear()) + pad2(dt.getMonth() + 1) + pad2(dt.getDate());
323
282
  }
283
+
324
284
  /**
325
285
  * Returns UTC string for iCalendar
326
286
  * @param {Date} dt
327
287
  * @return {string}
328
288
  */
329
-
330
-
331
289
  static makeDtstamp(dt) {
332
290
  const s = dt.toISOString();
333
291
  return s.slice(0, 4) + s.slice(5, 7) + s.slice(8, 13) + s.slice(14, 16) + s.slice(17, 19) + 'Z';
334
292
  }
335
- /** @return {string} */
336
-
337
293
 
294
+ /** @return {string} */
338
295
  static version() {
339
296
  return version;
340
297
  }
341
-
342
298
  }
299
+
343
300
  /**
344
301
  * Transforms a single Event into a VEVENT string
345
302
  * @param {Event} ev
346
303
  * @param {HebrewCalendar.Options} options
347
304
  * @return {string} multi-line result, delimited by \r\n
348
305
  */
349
-
350
306
  function eventToIcal(ev, options) {
351
307
  const ical = new IcalEvent(ev, options);
352
308
  return ical.toString();
353
309
  }
354
310
  const torahMemoCache = new Map();
355
- const HOLIDAY_IGNORE_MASK = flags.DAF_YOMI | flags.OMER_COUNT | flags.SHABBAT_MEVARCHIM | flags.MOLAD | flags.USER_EVENT | flags.MISHNA_YOMI | flags.HEBREW_DATE;
311
+ const HOLIDAY_IGNORE_MASK = flags.DAF_YOMI | flags.OMER_COUNT | flags.SHABBAT_MEVARCHIM | flags.MOLAD | flags.USER_EVENT | flags.MISHNA_YOMI | flags.YERUSHALMI_YOMI | flags.HEBREW_DATE;
312
+
356
313
  /**
357
314
  * @private
358
315
  * @param {Event} ev
359
316
  * @param {boolean} il
360
317
  * @return {string}
361
318
  */
362
-
363
319
  function makeTorahMemo(ev, il) {
364
320
  if (ev.getFlags() & HOLIDAY_IGNORE_MASK || ev.eventTime) {
365
321
  return '';
366
322
  }
367
-
368
323
  const hd = ev.getDate();
369
324
  const yy = hd.getFullYear();
370
325
  const mm = hd.getMonth();
371
326
  const dd = hd.getDate();
372
327
  const key = [yy, mm, dd, il ? '1' : '0', ev.getDesc()].join('-');
373
328
  let memo = torahMemoCache.get(key);
374
-
375
329
  if (typeof memo === 'string') {
376
330
  return memo;
377
331
  }
378
-
379
332
  memo = makeTorahMemoText(ev, il).replace(/\n/g, '\\n');
380
333
  torahMemoCache.set(key, memo);
381
334
  return memo;
382
335
  }
336
+
383
337
  /**
384
338
  * @private
385
339
  * @param {Event} e
386
340
  * @param {HebrewCalendar.Options} options
387
341
  * @return {string}
388
342
  */
389
-
390
-
391
343
  function createMemo(e, options) {
392
344
  const desc = e.getDesc();
393
345
  const candles = desc === 'Havdalah' || desc === 'Candle lighting';
394
-
395
346
  if (typeof e.memo === 'string' && e.memo.length && e.memo.indexOf('\n') !== -1) {
396
347
  e.memo = e.memo.replace(/\n/g, '\\n');
397
348
  }
398
-
399
349
  if (candles) {
400
350
  return e.memo || '';
401
351
  }
402
-
403
352
  const mask = e.getFlags();
404
-
405
353
  if (mask & flags.OMER_COUNT) {
406
354
  const sefira = [e.sefira('en'), e.sefira('he'), e.sefira('translit')].join('\\n');
407
355
  return e.getTodayIs('en') + '\\n\\n' + e.getTodayIs('he') + '\\n\\n' + sefira;
408
356
  }
409
-
410
357
  const url = appendTrackingToUrl(e.url(), options);
411
358
  const torahMemo = makeTorahMemo(e, options.il);
412
-
413
359
  if (mask & flags.PARSHA_HASHAVUA) {
414
360
  return torahMemo + '\\n\\n' + url;
415
361
  } else {
416
362
  let memo = e.memo || getHolidayDescription(e);
417
-
418
363
  if (!memo && typeof e.linkedEvent !== 'undefined') {
419
364
  memo = e.linkedEvent.render(options.locale);
420
365
  }
421
-
422
366
  if (torahMemo) {
423
367
  memo += '\\n\\n' + torahMemo;
424
368
  }
425
-
426
369
  if (url) {
427
370
  if (memo.length) {
428
371
  memo += '\\n\\n';
429
372
  }
430
-
431
373
  memo += url;
432
374
  }
433
-
434
375
  return memo;
435
376
  }
436
377
  }
378
+
437
379
  /**
438
380
  * Generates an RFC 2445 iCalendar string from an array of events
439
381
  * @param {Event[]} events
440
382
  * @param {HebrewCalendar.Options} options
441
383
  * @return {string}
442
384
  */
443
-
444
-
445
385
  async function eventsToIcalendar(events, options) {
446
386
  if (!events.length) throw new RangeError('Events can not be empty');
447
387
  if (!options) throw new TypeError('Invalid options object');
448
388
  const opts = Object.assign({}, options);
449
389
  opts.dtstamp = opts.dtstamp || IcalEvent.makeDtstamp(new Date());
450
-
451
390
  if (!opts.title) {
452
391
  opts.title = getCalendarTitle(events, opts);
453
392
  }
454
-
455
393
  const icals = events.map(ev => new IcalEvent(ev, opts));
456
394
  return icalEventsToString(icals, opts);
457
395
  }
396
+
458
397
  /**
459
398
  * Generates an RFC 2445 iCalendar string from an array of IcalEvents
460
399
  * @param {IcalEvent[]} icals
461
400
  * @param {HebrewCalendar.Options} options
462
401
  * @return {string}
463
402
  */
464
-
465
403
  async function icalEventsToString(icals, options) {
466
404
  const stream = [];
467
405
  const uclang = Locale.getLocaleName().toUpperCase();
@@ -472,55 +410,46 @@ async function icalEventsToString(icals, options) {
472
410
  const publishedTTL = opts.publishedTTL || 'PT7D';
473
411
  const prodid = opts.prodid || `-//hebcal.com/NONSGML Hebcal Calendar v1${version}//${uclang}`;
474
412
  const preamble = ['BEGIN:VCALENDAR', 'VERSION:2.0', `PRODID:${prodid}`, 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', 'X-LOTUS-CHARSET:UTF-8', `X-PUBLISHED-TTL:${publishedTTL}`, `X-WR-CALNAME:${title}`, `X-WR-CALDESC:${caldesc}`];
475
-
476
413
  for (const line of preamble.map(IcalEvent.fold)) {
477
414
  stream.push(line);
478
415
  stream.push('\r\n');
479
416
  }
480
-
481
417
  if (opts.relcalid) {
482
418
  stream.push(IcalEvent.fold(`X-WR-RELCALID:${opts.relcalid}`));
483
419
  stream.push('\r\n');
484
420
  }
485
-
486
421
  if (opts.calendarColor) {
487
422
  stream.push(`X-APPLE-CALENDAR-COLOR:${opts.calendarColor}\r\n`);
488
423
  }
489
-
490
424
  const location = opts.location;
491
-
492
425
  if (location && location.tzid) {
493
426
  const tzid = location.tzid;
494
427
  stream.push(`X-WR-TIMEZONE;VALUE=TEXT:${tzid}\r\n`);
495
-
496
428
  if (VTIMEZONE[tzid]) {
497
429
  stream.push(VTIMEZONE[tzid]);
498
430
  stream.push('\r\n');
499
431
  } else {
500
432
  const vtimezoneFilename = `./zoneinfo/${tzid}.ics`;
501
-
502
433
  try {
503
434
  const vtimezoneIcs = await promises.readFile(vtimezoneFilename, 'utf-8');
504
- const lines = vtimezoneIcs.split('\r\n'); // ignore first 3 and last 1 lines
505
-
435
+ const lines = vtimezoneIcs.split('\r\n');
436
+ // ignore first 3 and last 1 lines
506
437
  const str = lines.slice(3, lines.length - 2).join('\r\n');
507
438
  stream.push(str);
508
439
  stream.push('\r\n');
509
440
  VTIMEZONE[tzid] = str; // cache for later
510
- } catch (error) {// ignore failure when no timezone definition to read
441
+ } catch (error) {
442
+ // ignore failure when no timezone definition to read
511
443
  }
512
444
  }
513
445
  }
514
-
515
446
  for (const ical of icals) {
516
447
  const lines = ical.getLines();
517
-
518
448
  for (const line of lines) {
519
449
  stream.push(line);
520
450
  stream.push('\r\n');
521
451
  }
522
452
  }
523
-
524
453
  stream.push('END:VCALENDAR\r\n');
525
454
  return stream.join('');
526
455
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hebcal/icalendar",
3
- "version": "4.18.2",
3
+ "version": "4.18.4",
4
4
  "author": "Michael J. Radwin (https://github.com/mjradwin)",
5
5
  "keywords": [
6
6
  "ical",
@@ -24,8 +24,8 @@
24
24
  "url": "https://github.com/hebcal/hebcal-icalendar/issues"
25
25
  },
26
26
  "dependencies": {
27
- "@hebcal/core": "^3.42.3",
28
- "@hebcal/rest-api": "^4.1.0",
27
+ "@hebcal/core": "^3.46.1",
28
+ "@hebcal/rest-api": "^4.3.7",
29
29
  "murmurhash3": "^0.5.0"
30
30
  },
31
31
  "scripts": {
@@ -48,17 +48,17 @@
48
48
  "verbose": true
49
49
  },
50
50
  "devDependencies": {
51
- "@babel/core": "^7.18.13",
52
- "@babel/preset-env": "^7.18.10",
51
+ "@babel/core": "^7.20.2",
52
+ "@babel/preset-env": "^7.20.2",
53
53
  "@babel/register": "^7.18.9",
54
- "@rollup/plugin-babel": "^5.3.1",
55
- "@rollup/plugin-commonjs": "^22.0.2",
56
- "@rollup/plugin-json": "^4.1.0",
57
- "ava": "^4.3.1",
58
- "eslint": "^8.22.0",
54
+ "@rollup/plugin-babel": "^6.0.2",
55
+ "@rollup/plugin-commonjs": "^23.0.2",
56
+ "@rollup/plugin-json": "^5.0.1",
57
+ "ava": "^5.1.0",
58
+ "eslint": "^8.28.0",
59
59
  "eslint-config-google": "^0.14.0",
60
- "jsdoc": "^3.6.11",
60
+ "jsdoc": "^4.0.0",
61
61
  "jsdoc-to-markdown": "^7.1.1",
62
- "rollup": "^2.78.1"
62
+ "rollup": "^3.4.0"
63
63
  }
64
64
  }