@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.
- package/dist/index.js +33 -107
- package/dist/index.mjs +33 -105
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
/*! @hebcal/icalendar v4.18.
|
|
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.
|
|
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());
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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');
|
|
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) {
|
|
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
|
+
/*! @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.
|
|
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());
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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');
|
|
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) {
|
|
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.
|
|
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.
|
|
28
|
-
"@hebcal/rest-api": "^4.
|
|
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.
|
|
52
|
-
"@babel/preset-env": "^7.
|
|
51
|
+
"@babel/core": "^7.20.2",
|
|
52
|
+
"@babel/preset-env": "^7.20.2",
|
|
53
53
|
"@babel/register": "^7.18.9",
|
|
54
|
-
"@rollup/plugin-babel": "^
|
|
55
|
-
"@rollup/plugin-commonjs": "^
|
|
56
|
-
"@rollup/plugin-json": "^
|
|
57
|
-
"ava": "^
|
|
58
|
-
"eslint": "^8.
|
|
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": "^
|
|
60
|
+
"jsdoc": "^4.0.0",
|
|
61
61
|
"jsdoc-to-markdown": "^7.1.1",
|
|
62
|
-
"rollup": "^
|
|
62
|
+
"rollup": "^3.3.0"
|
|
63
63
|
}
|
|
64
64
|
}
|