@hebcal/icalendar 4.18.1 → 4.18.3

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