@hebcal/icalendar
Advanced tools
Comparing version 3.1.1 to 3.2.0
@@ -350,3 +350,3 @@ 'use strict'; | ||
var version="3.1.1"; | ||
var version="3.2.0"; | ||
@@ -418,132 +418,168 @@ const VTIMEZONE = {}; | ||
} | ||
const char74re = /(.{1,74})/g; | ||
/** | ||
* @private | ||
* @param {Event} e | ||
* @param {HebrewCalendar.Options} options | ||
* @return {string[]} | ||
* Represents an RFC 2445 iCalendar VEVENT | ||
*/ | ||
class IcalEvent { | ||
/** | ||
* Builds an IcalEvent object from a Hebcal Event | ||
* @param {Event} ev | ||
* @param {HebrewCalendar.Options} options | ||
*/ | ||
constructor(ev, options) { | ||
const dtstamp = options.dtstamp || makeDtstamp(new Date()); | ||
const timed = Boolean(ev.eventTime); | ||
let subj = timed ? renderTitleWithoutTime_1(ev) : ev.render(); | ||
const desc = ev.getDesc(); // original untranslated | ||
function eventToIcal0(e, options) { | ||
const dtstamp = options.dtstamp || makeDtstamp(new Date()); | ||
const timed = Boolean(e.eventTime); | ||
let subj = timed ? renderTitleWithoutTime_1(e) : e.render(); | ||
const desc = e.getDesc(); // original untranslated | ||
const mask = ev.getFlags(); | ||
let location; | ||
const mask = e.getFlags(); | ||
const candles = desc === 'Havdalah' || desc === 'Candle lighting'; | ||
let location; | ||
if (timed && options.location.name) { | ||
const comma = options.location.name.indexOf(','); | ||
location = comma == -1 ? options.location.name : options.location.name.substring(0, comma); | ||
} | ||
if (timed && options.location.name) { | ||
const comma = options.location.name.indexOf(','); | ||
location = comma == -1 ? options.location.name : options.location.name.substring(0, comma); | ||
} | ||
if (mask & core.flags.DAF_YOMI) { | ||
const colon = subj.indexOf(': '); | ||
if (mask & core.flags.DAF_YOMI) { | ||
const colon = subj.indexOf(': '); | ||
if (colon != -1) { | ||
location = subj.substring(0, colon); | ||
subj = subj.substring(colon + 2); | ||
if (colon != -1) { | ||
location = subj.substring(0, colon); | ||
subj = subj.substring(colon + 2); | ||
} | ||
} | ||
} | ||
const date = formatYYYYMMDD(e.getDate().greg()); | ||
let startDate = date; | ||
let dtargs; | ||
let endDate; | ||
let transp = 'TRANSPARENT'; | ||
let busyStatus = 'FREE'; | ||
const date = formatYYYYMMDD(ev.getDate().greg()); | ||
let startDate = date; | ||
let dtargs; | ||
let endDate; | ||
let transp = 'TRANSPARENT'; | ||
let busyStatus = 'FREE'; | ||
if (timed) { | ||
let [hour, minute] = e.eventTimeStr.split(':'); | ||
hour = +hour; | ||
minute = +minute; | ||
startDate += 'T' + pad2_1(hour) + pad2_1(minute) + '00'; | ||
endDate = startDate; | ||
dtargs = `;TZID=${options.location.tzid}`; | ||
} else { | ||
endDate = formatYYYYMMDD(e.getDate().next().greg()); // for all-day untimed, use DTEND;VALUE=DATE intsead of DURATION:P1D. | ||
// It's more compatible with everthing except ancient versions of | ||
// Lotus Notes circa 2004 | ||
if (timed) { | ||
let [hour, minute] = ev.eventTimeStr.split(':'); | ||
hour = +hour; | ||
minute = +minute; | ||
startDate += 'T' + pad2_1(hour) + pad2_1(minute) + '00'; | ||
endDate = startDate; | ||
dtargs = `;TZID=${options.location.tzid}`; | ||
} else { | ||
endDate = formatYYYYMMDD(ev.getDate().next().greg()); // for all-day untimed, use DTEND;VALUE=DATE intsead of DURATION:P1D. | ||
// It's more compatible with everthing except ancient versions of | ||
// Lotus Notes circa 2004 | ||
dtargs = ';VALUE=DATE'; | ||
dtargs = ';VALUE=DATE'; | ||
if (mask & core.flags.CHAG) { | ||
transp = 'OPAQUE'; | ||
busyStatus = 'OOF'; | ||
if (mask & core.flags.CHAG) { | ||
transp = 'OPAQUE'; | ||
busyStatus = 'OOF'; | ||
} | ||
} | ||
} | ||
const digest = murmur3_1(desc).toString(16); | ||
let uid = `hebcal-${date}-${digest}`; | ||
const digest = murmur3_1(desc).toString(16); | ||
let uid = `hebcal-${date}-${digest}`; | ||
if (timed && options.location) { | ||
if (options.location.geoid) { | ||
uid += `-${options.location.geoid}`; | ||
} else if (options.location.name) { | ||
uid += '-' + makeAnchor_1(options.location.name); | ||
if (timed && options.location) { | ||
if (options.location.geoid) { | ||
uid += `-${options.location.geoid}`; | ||
} else if (options.location.name) { | ||
uid += '-' + makeAnchor_1(options.location.name); | ||
} | ||
} // make subject safe for iCalendar | ||
subj = subj.replace(/,/g, '\\,'); | ||
if (options.appendHebrewToSubject) { | ||
const hebrew = ev.renderBrief('he'); | ||
if (hebrew) { | ||
subj += ` / ${hebrew}`; | ||
} | ||
} | ||
} // make subject safe for iCalendar | ||
const isUserEvent = Boolean(mask & core.flags.USER_EVENT); | ||
const category = isUserEvent ? 'Personal' : 'Holiday'; | ||
const arr = ['BEGIN:VEVENT', `DTSTAMP:${dtstamp}`, `CATEGORIES:${category}`, `SUMMARY:${subj}`, `DTSTART${dtargs}:${startDate}`, `DTEND${dtargs}:${endDate}`, `TRANSP:${transp}`, `X-MICROSOFT-CDO-BUSYSTATUS:${busyStatus}`, `UID:${uid}`]; | ||
subj = subj.replace(/,/g, '\\,'); | ||
if (!isUserEvent) { | ||
arr.push('CLASS:PUBLIC'); | ||
} // create memo (holiday descr, Torah, etc) | ||
if (options.appendHebrewToSubject) { | ||
const hebrew = e.renderBrief('he'); | ||
if (hebrew) { | ||
subj += ` / ${hebrew}`; | ||
const candles = desc === 'Havdalah' || desc === 'Candle lighting'; | ||
const memo = candles ? '' : createMemo(ev, options.il); | ||
addOptional(arr, 'DESCRIPTION', memo); | ||
addOptional(arr, 'LOCATION', location); | ||
if (timed && options.location) { | ||
arr.push('GEO:' + options.location.latitude + ';' + options.location.longitude); | ||
} | ||
} | ||
const isUserEvent = Boolean(mask & core.flags.USER_EVENT); | ||
const category = isUserEvent ? 'Personal' : 'Holiday'; | ||
const arr = ['BEGIN:VEVENT', `DTSTAMP:${dtstamp}`, `CATEGORIES:${category}`, `SUMMARY:${subj}`, `DTSTART${dtargs}:${startDate}`, `DTEND${dtargs}:${endDate}`, `TRANSP:${transp}`, `X-MICROSOFT-CDO-BUSYSTATUS:${busyStatus}`, `UID:${uid}`]; | ||
let alarm; | ||
if (!isUserEvent) { | ||
arr.push('CLASS:PUBLIC'); | ||
} // create memo (holiday descr, Torah, etc) | ||
if (mask & core.flags.OMER_COUNT) { | ||
alarm = '3H'; // 9pm Omer alarm evening before | ||
} else if (isUserEvent) { | ||
alarm = '12H'; // noon the day before | ||
} else if (timed && desc.startsWith('Candle lighting')) { | ||
alarm = '10M'; // ten minutes | ||
} | ||
if (alarm) { | ||
arr.push('BEGIN:VALARM', 'ACTION:DISPLAY', 'DESCRIPTION:REMINDER', `TRIGGER;RELATED=START:-PT${alarm}`, 'END:VALARM'); | ||
} | ||
const memo = candles ? '' : createMemo(e, options.il); | ||
addOptional(arr, 'DESCRIPTION', memo); | ||
addOptional(arr, 'LOCATION', location); | ||
arr.push('END:VEVENT'); | ||
this.lines = arr; | ||
this.options = options; | ||
this.ev = ev; | ||
} | ||
/** | ||
* @return {string} | ||
*/ | ||
if (timed && options.location) { | ||
arr.push('GEO:' + options.location.latitude + ';' + options.location.longitude); | ||
toString() { | ||
return this.getFoldedLines().join('\r\n'); | ||
} | ||
/** | ||
* @return {NodeJS.ReadableStream} | ||
*/ | ||
let alarm; | ||
if (mask & core.flags.OMER_COUNT) { | ||
alarm = '3H'; // 9pm Omer alarm evening before | ||
} else if (isUserEvent) { | ||
alarm = '12H'; // noon the day before | ||
} else if (timed && desc.startsWith('Candle lighting')) { | ||
alarm = '10M'; // ten minutes | ||
toStream() { | ||
const readable = new stream.Readable(); | ||
const lines = this.getFoldedLines(); | ||
for (const line of lines) { | ||
readable.push(line); | ||
readable.push('\r\n'); | ||
} | ||
readable.push(null); | ||
return readable; | ||
} | ||
/** | ||
* fold lines to 75 characters | ||
* @return {string[]} | ||
*/ | ||
if (alarm) { | ||
arr.push('BEGIN:VALARM', 'ACTION:DISPLAY', 'DESCRIPTION:REMINDER', `TRIGGER;RELATED=START:-PT${alarm}`, 'END:VALARM'); | ||
getFoldedLines() { | ||
return this.getLines().map(line => { | ||
return line.length <= 74 ? line : line.match(char74re).join('\r\n '); | ||
}); | ||
} | ||
/** | ||
* @return {string[]} | ||
*/ | ||
arr.push('END:VEVENT'); | ||
return arr; | ||
} | ||
const char74re = /(.{1,74})/g; | ||
/** | ||
* @private | ||
* @param {Event} ev | ||
* @param {HebrewCalendar.Options} options | ||
* @return {string[]} | ||
*/ | ||
getLines() { | ||
return this.lines; | ||
} | ||
function eventToIcal1(ev, options) { | ||
const lines = eventToIcal0(ev, options); // fold lines to 75 characters | ||
return lines.map(line => { | ||
return line.length <= 74 ? line : line.match(char74re).join('\r\n '); | ||
}); | ||
} | ||
@@ -557,6 +593,5 @@ /** | ||
function eventToIcal(ev, options) { | ||
const lines = eventToIcal1(ev, options); | ||
return lines.join('\r\n'); | ||
const ical = new IcalEvent(ev, options); | ||
return ical.toString(); | ||
} | ||
@@ -596,3 +631,3 @@ /** | ||
* Generates an RFC 2445 iCalendar stream from an array of events | ||
* @param {NodeJS.WritableStream} stream | ||
* @param {NodeJS.ReadableStream} stream | ||
* @param {Event[]} events | ||
@@ -610,8 +645,8 @@ * @param {HebrewCalendar.Options} options | ||
['BEGIN:VCALENDAR', 'VERSION:2.0', `PRODID:-//hebcal.com/NONSGML Hebcal Calendar v1${version}//${uclang}`, 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', 'X-LOTUS-CHARSET:UTF-8', 'X-PUBLISHED-TTL:PT7D', `X-WR-CALNAME:${title}`, `X-WR-CALDESC:${caldesc}`].forEach(line => { | ||
stream.write(line); | ||
stream.write('\r\n'); | ||
stream.push(line); | ||
stream.push('\r\n'); | ||
}); | ||
if (options.relcalid) { | ||
stream.write(`X-WR-RELCALID:${options.relcalid}\r\n`); | ||
stream.push(`X-WR-RELCALID:${options.relcalid}\r\n`); | ||
} | ||
@@ -623,7 +658,7 @@ | ||
const tzid = location.tzid; | ||
stream.write(`X-WR-TIMEZONE;VALUE=TEXT:${tzid}\r\n`); | ||
stream.push(`X-WR-TIMEZONE;VALUE=TEXT:${tzid}\r\n`); | ||
if (VTIMEZONE[tzid]) { | ||
stream.write(VTIMEZONE[tzid]); | ||
stream.write('\r\n'); | ||
stream.push(VTIMEZONE[tzid]); | ||
stream.push('\r\n'); | ||
} else { | ||
@@ -635,4 +670,4 @@ try { | ||
const str = lines.slice(3, lines.length - 2).join('\r\n'); | ||
stream.write(str); | ||
stream.write('\r\n'); | ||
stream.push(str); | ||
stream.push('\r\n'); | ||
VTIMEZONE[tzid] = str; // cache for later | ||
@@ -646,12 +681,28 @@ } catch (error) {// ignore failure when no timezone definition to read | ||
events.forEach(ev => { | ||
const lines = eventToIcal1(ev, options); | ||
const ical = new IcalEvent(ev, options); | ||
const lines = ical.getFoldedLines(); | ||
lines.forEach(line => { | ||
stream.write(line); | ||
stream.write('\r\n'); | ||
stream.push(line); | ||
stream.push('\r\n'); | ||
}); | ||
}); | ||
stream.write('END:VCALENDAR\r\n'); | ||
stream.end(); | ||
stream.push('END:VCALENDAR\r\n'); | ||
stream.push(null); | ||
} | ||
/** | ||
* @private | ||
* @param {stream.Readable} readable | ||
* @return {string} | ||
*/ | ||
async function readableToString(readable) { | ||
let result = ''; | ||
for await (const chunk of readable) { | ||
result += chunk; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Renders an array of events as a full RFC 2445 iCalendar string | ||
@@ -663,17 +714,15 @@ * @param {Event[]} events | ||
async function eventsToIcalendar(events, options) { | ||
const writable = new stream.Writable(); | ||
const chunks = []; | ||
writable._write = function (chunk, encoding, next) { | ||
chunks.push(chunk); | ||
next(); | ||
}; | ||
eventsToIcalendarStream(writable, events, options); | ||
return Buffer.concat(chunks).toString('utf8'); | ||
const readStream = new stream.Readable(); | ||
eventsToIcalendarStream(readStream, events, options); | ||
readStream.on('error', err => { | ||
throw err; | ||
}); | ||
return readableToString(readStream); | ||
} | ||
exports.IcalEvent = IcalEvent; | ||
exports.eventToIcal = eventToIcal; | ||
exports.eventsToIcalendar = eventsToIcalendar; | ||
exports.eventsToIcalendarStream = eventsToIcalendarStream; |
{ | ||
"name": "@hebcal/icalendar", | ||
"version": "3.1.1", | ||
"version": "3.2.0", | ||
"author": "Michael J. Radwin (https://github.com/mjradwin)", | ||
@@ -59,3 +59,3 @@ "keywords": [ | ||
"@babel/polyfill": "^7.12.1", | ||
"@babel/preset-env": "^7.12.10", | ||
"@babel/preset-env": "^7.12.11", | ||
"@babel/register": "^7.12.10", | ||
@@ -62,0 +62,0 @@ "@rollup/plugin-babel": "^5.2.2", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
31196
626