diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..6c5765d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,21 @@ +module.exports = { + 'env': { + 'commonjs': true, + 'es6': true, + 'node': true + }, + 'globals': { + 'Atomics': 'readonly', + 'SharedArrayBuffer': 'readonly', + 'Promise': 'readonly' + }, + 'parserOptions': { + 'ecmaVersion': 2018 + }, + 'extends': ['eslint:recommended', 'prettier'], + 'plugins': ['prettier'], + 'rules': { + 'prettier/prettier': ['error'], + 'no-var': 'error', 'prefer-const': 'warn' + } +}; diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..00620b8 --- /dev/null +++ b/.npmignore @@ -0,0 +1,13 @@ +Gruntfile.js +tasks +test +test.js +example.js +*.ics +*.tgz +.travis.yml +.idea +.git +.gitignore +node_modules +appveyor.yml diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..c9dccb0 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,10 @@ +module.exports = { + printWidth: 120, + tabWidth: 4, + semi: true, + singleQuote: true, + trailingComma: 'es5', + quoteProps: 'consistent', + bracketSpacing: true, + endOfLine: 'lf' +}; diff --git a/.travis.yml b/.travis.yml index d5d4c1e..60d33a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ language: node_js node_js: - - "8.9" -install: npm install + - "6" + - "8" + - "10" + - "12" +install: npm install diff --git a/LICENSE b/LICENSE index e454a52..8dada3e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,3 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -176,3 +175,27 @@ END OF TERMS AND CONDITIONS + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE deleted file mode 100644 index d5f926d..0000000 --- a/NOTICE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2012 Peter Braden - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d046d0b --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +# node-ical +[![Build Status](https://travis-ci.org/jens-maus/node-ical.png)](https://travis-ci.org/jens-maus/node-ical) +[![NPM version](http://img.shields.io/npm/v/node-ical.svg)](https://www.npmjs.com/package/node-ical) +[![Downloads](https://img.shields.io/npm/dm/node-ical.svg)](https://www.npmjs.com/package/node-ical) +[![Known Vulnerabilities](https://snyk.io/test/github/jens-maus/node-ical/badge.svg?targetFile=package.json)](https://snyk.io/test/github/jens-maus/node-ical?targetFile=package.json) + +[![NPM](https://nodei.co/npm/node-ical.png?downloads=true)](https://nodei.co/npm/node-ical/) + +A minimal iCalendar/ICS (http://tools.ietf.org/html/rfc5545) parser for Node.js. This module is a direct fork +of the ical.js module by Peter Braden (https://github.com/peterbraden/ical.js) which is primarily targeted +for parsing iCalender/ICS files in a pure JavaScript environment. (ex. within the browser itself) This node-ical +module however, primarily targets Node.js use and allows for more flexible APIs and interactions within a Node environment. (like filesystem access!) + +## Install +node-ical is availble on npm: +```sh +npm install node-ical +``` + +## API +The API has now been broken into three sections: + - [sync](#sync) + - [async](#async) + - [autodetect](#autodetect) + +`sync` provides synchronous API functions. +These are easy to use but can block the event loop and are not recommended for applications that need to serve content or handle events. + +`async` provides proper asynchronous support for iCal parsing. +All functions will either return a promise for `async/await` or use a callback if one is provided. + +`autodetect` provides a mix of both for backwards compatibility with older node-ical applications. + +All API functions are documented using JSDoc in the [index.js](index.js) file. +This allows for IDE hinting! + +### sync +```javascript +// import ical +const ical = require('node-ical'); + +// use the sync function parseFile() to parse this ics file +const events = ical.sync.parseFile('example-calendar.ics'); +// loop through events and log them +for (const event of Object.values(events)) { + console.log( + 'Summary: ' + event.summary + + '\nDescription: ' + event.description + + '\nStart Date: ' + event.start.toISOString() + + '\n' + ); +}; + +// or just parse some iCalendar data directly +const directEvents = ical.sync.parseICS(` +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VEVENT +SUMMARY:Hey look! An example event! +DTSTART;TZID=America/New_York:20130802T103400 +DTEND;TZID=America/New_York:20130802T110400 +LOCATION:1000 Broadway Ave.\, Brooklyn +DESCRIPTION: Do something in NY. +STATUS:CONFIRMED +UID:7014-1567468800-1567555199@peterbraden@peterbraden.co.uk +END:VEVENT +END:VCALENDAR +`); +// log the ids of these events +console.log(Object.keys(directEvents)); +``` + +### async +```javascript +// import ical +const ical = require('node-ical'); + +// do stuff in an async function +;(async () => { + // load and parse this file without blocking the event loop + const events = await ical.async.parseFile('example-calendar.ics'); + + // you can also use the async lib to download and parse iCal from the web + const webEvents = await ical.async.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics'); + // also you can pass options to request() (optional though!) + const headerWebEvents = await ical.async.fromURL( + 'http://lanyrd.com/topics/nodejs/nodejs.ics', + { headers: { 'User-Agent': 'API-Example / 1.0' } } + ); + + // parse iCal data without blocking the main loop for extra-large events + const directEvents = await ical.async.parseICS(` +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VEVENT +SUMMARY:Hey look! An example event! +DTSTART;TZID=America/New_York:20130802T103400 +DTEND;TZID=America/New_York:20130802T110400 +DESCRIPTION: Do something in NY. +UID:7014-1567468800-1567555199@peterbraden@peterbraden.co.uk +END:VEVENT +END:VCALENDAR + `); +})() + .catch(console.error.bind()); + +// old fashioned callbacks cause why not + +// parse a file with a callback +ical.async.parseFile('example-calendar.ics', function(err, data) { + if (err) { + console.error(err); + process.exit(1); + } + console.log(data); +}); + +// or a URL +ical.async.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', function(err, data) { console.log(data); }); + +// or directly +ical.async.parseICS(` +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VEVENT +SUMMARY:Hey look! An example event! +DTSTART;TZID=America/New_York:20130802T103400 +DTEND;TZID=America/New_York:20130802T110400 +DESCRIPTION: Do something in NY. +UID:7014-1567468800-1567555199@peterbraden@peterbraden.co.uk +END:VEVENT +END:VCALENDAR +`, function(err, data) { console.log(data); }); +``` + +### autodetect +These are the old API examples, which still work and will be converted to the new API automatically. +Functions with callbacks provided will also have better performance over the older versions even if they use the old API. + +Parses a string with ICS content in sync. This can block the event loop on big files. +```javascript +const ical = require('node-ical'); +ical.parseICS(str); +``` + +Parses a string with ICS content in async to prevent the event loop from being blocked. +```javascript +const ical = require('node-ical'); +ical.parseICS(str, function(err, data) { + if (err) console.log(err); + console.log(data); +}); +``` + +Parses a string with an ICS file in sync. This can block the event loop on big files. +```javascript +const ical = require('node-ical'); +const data = ical.parseFile(filename); +``` + +Parses a string with an ICS file in async to prevent event loop from being blocked. +```javascript +const ical = require('node-ical'); +const data = ical.parseFile(filename, function(err, data) { + if (err) console.log(err); + console.log(data); +}); +``` + +Reads in the specified iCal file from the URL, parses it and returns the parsed data. +```javascript +const ical = require('node-ical'); +ical.fromURL(url, options, function(err, data) { + if (err) console.log(err); + console.log(data); +}); +``` + +Use the request library to fetch the specified URL (```opts``` gets passed on to the ```request()``` call), and call the function with the result. (either an error or the data) + +#### Example 1 - Print list of upcoming node conferences (see example.js) (parses the file synchronous) +```javascript +const ical = require('node-ical'); +const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) { + for (let k in data) { + if (data.hasOwnProperty(k)) { + const ev = data[k]; + if (data[k].type == 'VEVENT') { + console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`); + } + } + } +}); +``` diff --git a/example.js b/example.js index 7b41f1a..edc4b04 100644 --- a/example.js +++ b/example.js @@ -1,16 +1,17 @@ -'use strict'; +const ical = require('./index'); -const ical = require('ical'); const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; -ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) { - for (let k in data) { - if (data.hasOwnProperty(k)) { - var ev = data[k]; - if (data[k].type == 'VEVENT') { - console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`); - - } - } - } +ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function(err, data) { + for (const k in data) { + if (!{}.hasOwnProperty.call(data, k)) continue; + const ev = data[k]; + if (data[k].type == 'VEVENT') { + console.log( + `${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${ + months[ev.start.getMonth()] + } at ${ev.start.toLocaleTimeString('en-GB')}` + ); + } + } }); diff --git a/example_rrule.js b/example_rrule.js index 0eb13e8..94db051 100644 --- a/example_rrule.js +++ b/example_rrule.js @@ -1,118 +1,100 @@ -var ical = require('./node-ical') -var moment = require('moment') - -var data = ical.parseFile('./examples/example_rrule.ics'); - -// Complicated example demonstrating how to handle recurrence rules and exceptions. - -for (var k in data) { - - // When dealing with calendar recurrences, you need a range of dates to query against, - // because otherwise you can get an infinite number of calendar events. - var rangeStart = moment("2017-01-01"); - var rangeEnd = moment("2017-12-31"); - - - var event = data[k] - if (event.type === 'VEVENT') { - - var title = event.summary; - var startDate = moment(event.start); - var endDate = moment(event.end); - - // Calculate the duration of the event for use with recurring events. - var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x")); - - // Simple case - no recurrences, just print out the calendar event. - if (typeof event.rrule === 'undefined') - { - console.log('title:' + title); - console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a')); - console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a')); - console.log('duration:' + moment.duration(duration).humanize()); - console.log(); - } - - // Complicated case - if an RRULE exists, handle multiple recurrences of the event. - else if (typeof event.rrule !== 'undefined') - { - // For recurring events, get the set of event start dates that fall within the range - // of dates we're looking for. - var dates = event.rrule.between( - rangeStart.toDate(), - rangeEnd.toDate(), - true, - function(date, i) {return true;} - ) - - // The "dates" array contains the set of dates within our desired date range range that are valid - // for the recurrence rule. *However*, it's possible for us to have a specific recurrence that - // had its date changed from outside the range to inside the range. One way to handle this is - // to add *all* recurrence override entries into the set of dates that we check, and then later - // filter out any recurrences that don't actually belong within our range. - if (event.recurrences != undefined) - { - for (var r in event.recurrences) - { - // Only add dates that weren't already in the range we added from the rrule so that - // we don't double-add those events. - if (moment(new Date(r)).isBetween(rangeStart, rangeEnd) != true) - { - dates.push(new Date(r)); - } - } - } - - // Loop through the set of date entries to see which recurrences should be printed. - for(var i in dates) { - - var date = dates[i]; - var curEvent = event; - var showRecurrence = true; - var curDuration = duration; - - startDate = moment(date); - - // Use just the date of the recurrence to look up overrides and exceptions (i.e. chop off time information) - var dateLookupKey = date.toISOString().substring(0, 10); - - // For each date that we're checking, it's possible that there is a recurrence override for that one day. - if ((curEvent.recurrences != undefined) && (curEvent.recurrences[dateLookupKey] != undefined)) - { - // We found an override, so for this recurrence, use a potentially different title, start date, and duration. - curEvent = curEvent.recurrences[dateLookupKey]; - startDate = moment(curEvent.start); - curDuration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x")); - } - // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule. - else if ((curEvent.exdate != undefined) && (curEvent.exdate[dateLookupKey] != undefined)) - { - // This date is an exception date, which means we should skip it in the recurrence pattern. - showRecurrence = false; - } - - // Set the the title and the end date from either the regular event or the recurrence override. - var recurrenceTitle = curEvent.summary; - endDate = moment(parseInt(startDate.format("x")) + curDuration, 'x'); - - // If this recurrence ends before the start of the date range, or starts after the end of the date range, - // don't process it. - if (endDate.isBefore(rangeStart) || startDate.isAfter(rangeEnd)) { - showRecurrence = false; - } - - if (showRecurrence === true) { - - console.log('title:' + recurrenceTitle); - console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a')); - console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a')); - console.log('duration:' + moment.duration(curDuration).humanize()); - console.log(); - } - - } - } - } -} - - +const moment = require('moment-timezone'); +const ical = require('./index'); // require('node-ical'); + +const data = ical.parseFile('./examples/example_rrule.ics'); + +// Complicated example demonstrating how to handle recurrence rules and exceptions. + +for (const k in data) { + // When dealing with calendar recurrences, you need a range of dates to query against, + // because otherwise you can get an infinite number of calendar events. + const rangeStart = moment('2017-01-01'); + const rangeEnd = moment('2017-12-31'); + + const event = data[k]; + if (event.type === 'VEVENT') { + const title = event.summary; + let startDate = moment(event.start); + let endDate = moment(event.end); + + // Calculate the duration of the event for use with recurring events. + const duration = parseInt(endDate.format('x')) - parseInt(startDate.format('x')); + + // Simple case - no recurrences, just print out the calendar event. + if (typeof event.rrule === 'undefined') { + console.log(`title:${title}`); + console.log(`startDate:${startDate.format('MMMM Do YYYY, h:mm:ss a')}`); + console.log(`endDate:${endDate.format('MMMM Do YYYY, h:mm:ss a')}`); + console.log(`duration:${moment.duration(duration).humanize()}`); + console.log(); + } + + // Complicated case - if an RRULE exists, handle multiple recurrences of the event. + else if (typeof event.rrule !== 'undefined') { + // For recurring events, get the set of event start dates that fall within the range + // of dates we're looking for. + const dates = event.rrule.between(rangeStart.toDate(), rangeEnd.toDate(), true, function(date, i) { + return true; + }); + + // The "dates" array contains the set of dates within our desired date range range that are valid + // for the recurrence rule. *However*, it's possible for us to have a specific recurrence that + // had its date changed from outside the range to inside the range. One way to handle this is + // to add *all* recurrence override entries into the set of dates that we check, and then later + // filter out any recurrences that don't actually belong within our range. + if (event.recurrences != undefined) { + for (const r in event.recurrences) { + // Only add dates that weren't already in the range we added from the rrule so that + // we don't double-add those events. + if (moment(new Date(r)).isBetween(rangeStart, rangeEnd) != true) { + dates.push(new Date(r)); + } + } + } + + // Loop through the set of date entries to see which recurrences should be printed. + for (const i in dates) { + const date = dates[i]; + let curEvent = event; + let showRecurrence = true; + let curDuration = duration; + + startDate = moment(date); + + // Use just the date of the recurrence to look up overrides and exceptions (i.e. chop off time information) + const dateLookupKey = date.toISOString().substring(0, 10); + + // For each date that we're checking, it's possible that there is a recurrence override for that one day. + if (curEvent.recurrences != undefined && curEvent.recurrences[dateLookupKey] != undefined) { + // We found an override, so for this recurrence, use a potentially different title, start date, and duration. + curEvent = curEvent.recurrences[dateLookupKey]; + startDate = moment(curEvent.start); + curDuration = parseInt(moment(curEvent.end).format('x')) - parseInt(startDate.format('x')); + } + // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule. + else if (curEvent.exdate != undefined && curEvent.exdate[dateLookupKey] != undefined) { + // This date is an exception date, which means we should skip it in the recurrence pattern. + showRecurrence = false; + } + + // Set the the title and the end date from either the regular event or the recurrence override. + const recurrenceTitle = curEvent.summary; + endDate = moment(parseInt(startDate.format('x')) + curDuration, 'x'); + + // If this recurrence ends before the start of the date range, or starts after the end of the date range, + // don't process it. + if (endDate.isBefore(rangeStart) || startDate.isAfter(rangeEnd)) { + showRecurrence = false; + } + + if (showRecurrence === true) { + console.log(`title:${recurrenceTitle}`); + console.log(`startDate:${startDate.format('MMMM Do YYYY, h:mm:ss a')}`); + console.log(`endDate:${endDate.format('MMMM Do YYYY, h:mm:ss a')}`); + console.log(`duration:${moment.duration(curDuration).humanize()}`); + console.log(); + } + } + } + } +} diff --git a/ical.js b/ical.js index 09308ea..1424d28 100755 --- a/ical.js +++ b/ical.js @@ -1,473 +1,535 @@ -(function(name, definition) { +const UUID = require('uuid/v4'); +const moment = require('moment-timezone'); +const rrule = require('rrule').RRule; -/**************** +/** ************** * A tolerant, minimal icalendar parser * (http://tools.ietf.org/html/rfc5545) * * - * **************/ - - if (typeof module !== 'undefined') { - module.exports = definition(); - } else if (typeof define === 'function' && typeof define.amd === 'object'){ - define(definition); - } else { - this[name] = definition(); - } - -}('ical', function(){ - - // Unescape Text re RFC 4.3.11 - var text = function(t){ - t = t || ""; - return (t - .replace(/\\\,/g, ',') - .replace(/\\\;/g, ';') - .replace(/\\[nN]/g, '\n') - .replace(/\\\\/g, '\\') - ) - } - - var parseParams = function(p){ - var out = {} - for (var i = 0; i -1 && params.indexOf('VALUE=DATE-TIME') == -1) { + ret = 'date'; + } - ['start', 'end'].forEach(function (name, index) { - dateParam(name)(parts[index], params, fb); - }); + return storeValParam(name)(ret, curr); + }; +}; - return curr; - } - } +const dateParam = function(name) { + return function(val, params, curr) { + let newDate = text(val); - return { + if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { + // Just Date + const comps = /^(\d{4})(\d{2})(\d{2}).*$/.exec(val); + if (comps !== null) { + // No TZ info - assume same timezone as this computer + newDate = new Date(comps[1], parseInt(comps[2], 10) - 1, comps[3]); - objectHandlers : { - 'BEGIN' : function(component, params, curr, stack){ - stack.push(curr) + newDate = addTZ(newDate, params); + newDate.dateOnly = true; - return {type:component, params:params} + // Store as string - worst case scenario + return storeValParam(name)(newDate, curr); + } } - , 'END' : function(component, params, curr, stack){ - // prevents the need to search the root of the tree for the VCALENDAR object - if (component === "VCALENDAR") { - //scan all high level object in curr and drop all strings - var key, - obj; - - for (key in curr) { - if(curr.hasOwnProperty(key)) { - obj = curr[key]; - if (typeof obj === 'string') { - delete curr[key]; - } + // typical RFC date-time format + const comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val); + if (comps !== null) { + if (comps[7] == 'Z') { + // GMT + newDate = new Date( + Date.UTC( + parseInt(comps[1], 10), + parseInt(comps[2], 10) - 1, + parseInt(comps[3], 10), + parseInt(comps[4], 10), + parseInt(comps[5], 10), + parseInt(comps[6], 10) + ) + ); + // TODO add tz + } else if (params && params[0] && params[0].indexOf('TZID=') > -1 && params[0].split('=')[1]) { + const tz = params[0].split('=')[1]; + // lookup tz + const found = moment.tz.names().filter(function(zone) { + return zone === tz; + })[0]; + if (found) { + const zoneDate = moment.tz(val, 'YYYYMMDDTHHmmss', tz); + newDate = zoneDate.toDate(); + } else { + // fallback if tz not found + newDate = new Date( + parseInt(comps[1], 10), + parseInt(comps[2], 10) - 1, + parseInt(comps[3], 10), + parseInt(comps[4], 10), + parseInt(comps[5], 10), + parseInt(comps[6], 10) + ); } + } else { + newDate = new Date( + parseInt(comps[1], 10), + parseInt(comps[2], 10) - 1, + parseInt(comps[3], 10), + parseInt(comps[4], 10), + parseInt(comps[5], 10), + parseInt(comps[6], 10) + ); } - return curr + newDate = addTZ(newDate, params); } - var par = stack.pop() - - if (curr.uid) - { - // If this is the first time we run into this UID, just save it. - if (par[curr.uid] === undefined) - { - par[curr.uid] = curr; - } - else - { - // If we have multiple ical entries with the same UID, it's either going to be a - // modification to a recurrence (RECURRENCE-ID), and/or a significant modification - // to the entry (SEQUENCE). - - // TODO: Look into proper sequence logic. - - if (curr.recurrenceid === undefined) - { - // If we have the same UID as an existing record, and it *isn't* a specific recurrence ID, - // not quite sure what the correct behaviour should be. For now, just take the new information - // and merge it with the old record by overwriting only the fields that appear in the new record. - var key; - for (key in curr) { - par[curr.uid][key] = curr[key]; - } - - } - } - - // If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id. - // To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences - // array. If it exists, then use the data from the calendar object in the recurrence instead of the parent - // for that day. - - // NOTE: Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry. In that - // case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry - // in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate - // fields in the parent record. - - if (curr.recurrenceid != null) - { - - // TODO: Is there ever a case where we have to worry about overwriting an existing entry here? - - // Create a copy of the current object to save in our recurrences array. (We *could* just do par = curr, - // except for the case that we get the RECURRENCE-ID record before the RRULE record. In that case, we - // would end up with a shared reference that would cause us to overwrite *both* records at the point - // that we try and fix up the parent record.) - var recurrenceObj = new Object(); - var key; - for (key in curr) { - recurrenceObj[key] = curr[key]; - } - - if (recurrenceObj.recurrences != undefined) { - delete recurrenceObj.recurrences; - } - - - // If we don't have an array to store recurrences in yet, create it. - if (par[curr.uid].recurrences === undefined) { - par[curr.uid].recurrences = new Array(); - } - - // Save off our cloned recurrence object into the array, keyed by date but not time. - // We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone). - // TODO: See if this causes a problem with events that have multiple recurrences per day. - if (typeof curr.recurrenceid.toISOString === 'function') { - par[curr.uid].recurrences[curr.recurrenceid.toISOString().substring(0,10)] = recurrenceObj; + // Store as string - worst case scenario + return storeValParam(name)(newDate, curr); + }; +}; + +const geoParam = function(name) { + return function(val, params, curr) { + storeParam(val, params, curr); + const parts = val.split(';'); + curr[name] = { lat: Number(parts[0]), lon: Number(parts[1]) }; + return curr; + }; +}; + +const categoriesParam = function(name) { + const separatorPattern = /\s*,\s*/g; + return function(val, params, curr) { + storeParam(val, params, curr); + if (curr[name] === undefined) curr[name] = val ? val.split(separatorPattern) : []; + else if (val) curr[name] = curr[name].concat(val.split(separatorPattern)); + return curr; + }; +}; + +// EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4"). +// The EXDATE entry itself can also contain a comma-separated list, so we make sure to parse each date out separately. +// There can also be more than one EXDATE entries in a calendar record. +// Since there can be multiple dates, we create an array of them. The index into the array is the ISO string of the date itself, for ease of use. +// i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception. +// NOTE: This specifically uses date only, and not time. This is to avoid a few problems: +// 1. The ISO string with time wouldn't work for "floating dates" (dates without timezones). +// ex: "20171225T060000" - this is supposed to mean 6 AM in whatever timezone you're currently in +// 2. Daylight savings time potentially affects the time you would need to look up +// 3. Some EXDATE entries in the wild seem to have times different from the recurrence rule, but are still excluded by calendar programs. Not sure how or why. +// These would fail any sort of sane time lookup, because the time literally doesn't match the event. So we'll ignore time and just use date. +// ex: DTSTART:20170814T140000Z +// RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU +// EXDATE:20171219T060000 +// Even though "T060000" doesn't match or overlap "T1400000Z", it's still supposed to be excluded? Odd. :( +// TODO: See if this causes any problems with events that recur multiple times a day. +const exdateParam = function(name) { + return function(val, params, curr) { + const separatorPattern = /\s*,\s*/g; + curr[name] = curr[name] || []; + const dates = val ? val.split(separatorPattern) : []; + dates.forEach(function(entry) { + const exdate = new Array(); + dateParam(name)(entry, params, exdate); + + if (exdate[name]) { + if (typeof exdate[name].toISOString === 'function') { + curr[name][exdate[name].toISOString().substring(0, 10)] = exdate[name]; } else { - console.error("No toISOString function in curr.recurrenceid", curr.recurrenceid); + console.error('No toISOString function in exdate[name]', exdate[name]); } } + }); + return curr; + }; +}; + +// RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule. +// TODO: It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE". This isn't currently handled. +const recurrenceParam = function(name) { + return dateParam(name); +}; + +const addFBType = function(fb, params) { + const p = parseParams(params); + + if (params && p) { + fb.type = p.FBTYPE || 'BUSY'; + } - // One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry, - // let's make sure to clear the recurrenceid off the parent field. - if ((par[curr.uid].rrule != undefined) && (par[curr.uid].recurrenceid != undefined)) - { - delete par[curr.uid].recurrenceid; - } + return fb; +}; + +const freebusyParam = function(name) { + return function(val, params, curr) { + const fb = addFBType({}, params); + curr[name] = curr[name] || []; + curr[name].push(fb); + + storeParam(val, params, fb); + + const parts = val.split('/'); + + ['start', 'end'].forEach(function(name, index) { + dateParam(name)(parts[index], params, fb); + }); + + return curr; + }; +}; + +module.exports = { + objectHandlers: { + 'BEGIN': function(component, params, curr, stack) { + stack.push(curr); + + return { type: component, params }; + }, + 'END': function(val, params, curr, stack) { + // original end function + const originalEnd = function(component, params, curr, stack) { + // prevents the need to search the root of the tree for the VCALENDAR object + if (component === 'VCALENDAR') { + // scan all high level object in curr and drop all strings + let key; + let obj; - } - else - par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID + for (key in curr) { + if (!{}.hasOwnProperty.call(curr, key)) continue; + obj = curr[key]; + if (typeof obj === 'string') { + delete curr[key]; + } + } - return par - } + return curr; + } - , 'SUMMARY' : storeParam('summary') - , 'DESCRIPTION' : storeParam('description') - , 'URL' : storeParam('url') - , 'UID' : storeParam('uid') - , 'LOCATION' : storeParam('location') - , 'DTSTART' : dateParam('start') - , 'DTEND' : dateParam('end') - , 'EXDATE' : exdateParam('exdate') - ,' CLASS' : storeParam('class') - , 'TRANSP' : storeParam('transparency') - , 'GEO' : geoParam('geo') - , 'PERCENT-COMPLETE': storeParam('completion') - , 'COMPLETED': dateParam('completed') - , 'CATEGORIES': categoriesParam('categories') - , 'FREEBUSY': freebusyParam('freebusy') - , 'DTSTAMP': dateParam('dtstamp') - , 'CREATED': dateParam('created') - , 'LAST-MODIFIED': dateParam('lastmodified') - , 'RECURRENCE-ID': recurrenceParam('recurrenceid') + const par = stack.pop(); + + if (curr.uid) { + // If this is the first time we run into this UID, just save it. + if (par[curr.uid] === undefined) { + par[curr.uid] = curr; + } else { + // If we have multiple ical entries with the same UID, it's either going to be a + // modification to a recurrence (RECURRENCE-ID), and/or a significant modification + // to the entry (SEQUENCE). + + // TODO: Look into proper sequence logic. + + if (curr.recurrenceid === undefined) { + // If we have the same UID as an existing record, and it *isn't* a specific recurrence ID, + // not quite sure what the correct behaviour should be. For now, just take the new information + // and merge it with the old record by overwriting only the fields that appear in the new record. + let key; + for (key in curr) { + par[curr.uid][key] = curr[key]; + } + } + } - }, + // If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id. + // To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences + // array. If it exists, then use the data from the calendar object in the recurrence instead of the parent + // for that day. + + // NOTE: Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry. In that + // case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry + // in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate + // fields in the parent record. + + if (curr.recurrenceid != null) { + // TODO: Is there ever a case where we have to worry about overwriting an existing entry here? + + // Create a copy of the current object to save in our recurrences array. (We *could* just do par = curr, + // except for the case that we get the RECURRENCE-ID record before the RRULE record. In that case, we + // would end up with a shared reference that would cause us to overwrite *both* records at the point + // that we try and fix up the parent record.) + const recurrenceObj = new Object(); + let key; + for (key in curr) { + recurrenceObj[key] = curr[key]; + } + + if (recurrenceObj.recurrences != undefined) { + delete recurrenceObj.recurrences; + } + + // If we don't have an array to store recurrences in yet, create it. + if (par[curr.uid].recurrences === undefined) { + par[curr.uid].recurrences = {}; + } + + // Save off our cloned recurrence object into the array, keyed by date but not time. + // We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone). + // TODO: See if this causes a problem with events that have multiple recurrences per day. + if (typeof curr.recurrenceid.toISOString === 'function') { + par[curr.uid].recurrences[curr.recurrenceid.toISOString().substring(0, 10)] = recurrenceObj; + } else { + console.error('No toISOString function in curr.recurrenceid', curr.recurrenceid); + } + } + // One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry, + // let's make sure to clear the recurrenceid off the parent field. + if (par[curr.uid].rrule != undefined && par[curr.uid].recurrenceid != undefined) { + delete par[curr.uid].recurrenceid; + } + } else par[UUID()] = curr; + + return par; + }; + // Recurrence rules are only valid for VEVENT, VTODO, and VJOURNAL. + // More specifically, we need to filter the VCALENDAR type because we might end up with a defined rrule + // due to the subtypes. + if (val === 'VEVENT' || val === 'VTODO' || val === 'VJOURNAL') { + if (curr.rrule) { + let rule = curr.rrule.replace('RRULE:', ''); + if (rule.indexOf('DTSTART') === -1) { + if (curr.start.length === 8) { + const comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start); + if (comps) { + curr.start = new Date(comps[1], comps[2] - 1, comps[3]); + } + } + + if (typeof curr.start.toISOString === 'function') { + try { + rule += `;DTSTART=${curr.start.toISOString().replace(/[-:]/g, '')}`; + rule = rule.replace(/\.[0-9]{3}/, ''); + } catch (error) { + console.error('ERROR when trying to convert to ISOString', error); + } + } else { + console.error('No toISOString function in curr.start', curr.start); + } + } + curr.rrule = rrule.fromString(rule); + } + } + return originalEnd.call(this, val, params, curr, stack); + }, + 'SUMMARY': storeParam('summary'), + 'DESCRIPTION': storeParam('description'), + 'URL': storeParam('url'), + 'UID': storeParam('uid'), + 'LOCATION': storeParam('location'), + 'DTSTART': function(val, params, curr) { + curr = dateParam('start')(val, params, curr); + return typeParam('datetype')(val, params, curr); + }, + 'DTEND': dateParam('end'), + 'EXDATE': exdateParam('exdate'), + ' CLASS': storeParam('class'), // should there be a space in this property? + 'TRANSP': storeParam('transparency'), + 'GEO': geoParam('geo'), + 'PERCENT-COMPLETE': storeParam('completion'), + 'COMPLETED': dateParam('completed'), + 'CATEGORIES': categoriesParam('categories'), + 'FREEBUSY': freebusyParam('freebusy'), + 'DTSTAMP': dateParam('dtstamp'), + 'CREATED': dateParam('created'), + 'LAST-MODIFIED': dateParam('lastmodified'), + 'RECURRENCE-ID': recurrenceParam('recurrenceid'), + 'RRULE': function(val, params, curr, stack, line) { + curr.rrule = line; + return curr; + }, + }, - handleObject : function(name, val, params, ctx, stack, line){ - var self = this + handleObject(name, val, params, ctx, stack, line) { + const self = this; - if(self.objectHandlers[name]) - return self.objectHandlers[name](val, params, ctx, stack, line) + if (self.objectHandlers[name]) return self.objectHandlers[name](val, params, ctx, stack, line); - //handling custom properties - if(name.match(/X\-[\w\-]+/) && stack.length > 0) { - //trimming the leading and perform storeParam - name = name.substring(2); - return (storeParam(name))(val, params, ctx, stack, line); - } + // handling custom properties + if (name.match(/X-[\w-]+/) && stack.length > 0) { + // trimming the leading and perform storeParam + name = name.substring(2); + return storeParam(name)(val, params, ctx, stack, line); + } - return storeParam(name.toLowerCase())(val, params, ctx); + return storeParam(name.toLowerCase())(val, params, ctx); }, - getLineBreakChar: function (string) { - const indexOfLF = string.indexOf('\n', 1); // No need to check first-character + parseLines(lines, limit, ctx, stack, lastIndex, cb) { + const self = this; - if (indexOfLF === -1) { - if (string.indexOf('\r') !== -1) return '\r'; - - return '\n'; - } + if (!cb && typeof ctx === 'function') { + cb = ctx; + ctx = undefined; + } + ctx = ctx || {}; + stack = stack || []; + + let limitCounter = 0; + + let i = lastIndex || 0; + for (let ii = lines.length; i < ii; i++) { + let l = lines[i]; + // Unfold : RFC#3.1 + while (lines[i + 1] && /[ \t]/.test(lines[i + 1][0])) { + l += lines[i + 1].slice(1); + i++; + } - if (string[indexOfLF - 1] === '\r') return '\r?\n'; + const exp = /([^":;]+)((?:;(?:[^":;]+)(?:=(?:(?:"[^"]*")|(?:[^":;]+))))*):(.*)/; + let kv = l.match(exp); - return '\n'; - }, + if (kv === null) { + // Invalid line - must have k&v + continue; + } + kv = kv.slice(1); - parseICS : function(str){ - var self = this - var line_end_type = self.getLineBreakChar(str) - var lines = str.split(line_end_type=='\n'?/\n/:/\r?\n/) - var ctx = {} - var stack = [] + const value = kv[kv.length - 1]; + const name = kv[0]; + const params = kv[1] ? kv[1].split(';').slice(1) : []; - for (var i = 0, ii = lines.length, l = lines[0]; i limit) { + break; + } } - // Split on semicolons except if the semicolon is surrounded by quotes - var kv = l.split(/:(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/g) - - if (kv.length < 2){ - // Invalid line - must have k&v - continue; + if (i >= lines.length) { + // type and params are added to the list of items, get rid of them. + delete ctx.type; + delete ctx.params; } - // Although the spec says that vals with colons should be quote wrapped - // in practise nobody does, so we assume further colons are part of the - // val - var value = kv.slice(1).join(":") - , kp = kv[0].split(";") - , name = kp[0] - , params = kp.slice(1) + if (cb) { + if (i < lines.length) { + setImmediate(function() { + self.parseLines(lines, limit, ctx, stack, i + 1, cb); + }); + } else { + setImmediate(function() { + cb(null, ctx); + }); + } + } else { + return ctx; + } + }, - ctx = self.handleObject(name, value, params, ctx, stack, l) || {} + getLineBreakChar: function (string) { + const indexOfLF = string.indexOf('\n', 1); // No need to check first-character + + if (indexOfLF === -1) { + if (string.indexOf('\r') !== -1) return '\r'; + + return '\n'; } + + if (string[indexOfLF - 1] === '\r') return '\r?\n'; + + return '\n'; + }, - // type and params are added to the list of items, get rid of them. - delete ctx.type - delete ctx.params - - return ctx - } + parseICS(str, cb) { + const self = this; + const line_end_type = self.getLineBreakChar(str) + const lines = str.split(line_end_type==='\n'?/\n/:/\r?\n/); + let ctx; - } -})) + if (cb) { + // asynchronous execution + self.parseLines(lines, 2000, cb); + } else { + // synchronous execution + ctx = self.parseLines(lines, lines.length); + return ctx; + } + }, +}; diff --git a/index.js b/index.js index 401dbcd..3c2c839 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,237 @@ -module.exports = require('./ical') +const request = require('request'); +const fs = require('fs'); -var node = require('./node-ical') +const ical = require('./ical.js'); -// Copy node functions across to exports -for (var i in node){ - module.exports[i] = node[i] -} \ No newline at end of file +/** + * iCal event object. + * + * These two fields are always present: + * - type + * - params + * + * The rest of the fields may or may not be present depending on the input. + * Do not assume any of these fields are valid and check them before using. + * Most types are simply there as a general guide for IDEs and users. + * + * @typedef iCalEvent + * @type {object} + * + * @property {string} type - Type of event. + * @property {Array} params - Extra event parameters. + * + * @property {?object} start - When this event starts. + * @property {?object} end - When this event ends. + * + * @property {?string} summary - Event summary string. + * @property {?string} description - Event description. + * + * @property {?object} dtstamp - DTSTAMP field of this event. + * + * @property {?object} created - When this event was created. + * @property {?object} lastmodified - When this event was last modified. + * + * @property {?string} uid - Unique event identifier. + * + * @property {?string} status - Event status. + * + * @property {?string} sequence - Event sequence. + * + * @property {?string} url - URL of this event. + * + * @property {?string} location - Where this event occurs. + * @property {?{ + * lat: number, lon: number + * }} geo - Lat/lon location of this event. + * + * @property {?Array.} - Array of event catagories. + */ +/** + * Object containing iCal events. + * @typedef {Object.} iCalData + */ +/** + * Callback for iCal parsing functions with error and iCal data as a JavaScript object. + * @callback icsCallback + * @param {Error} err + * @param {iCalData} ics + */ +/** + * A Promise that is undefined if a compatible callback is passed. + * @typedef {(Promise.|undefined)} optionalPromise + */ + +// utility to allow callbacks to be used for promises +function promiseCallback(fn, cb) { + const promise = new Promise(fn); + if (!cb) { + return promise; + } + promise + .then(function(ret) { + cb(null, ret); + }) + .catch(function(err) { + cb(err, null); + }); +} + +// sync functions +const sync = {}; +// async functions +const async = {}; +// auto-detect functions for backwards compatibility. +const autodetect = {}; + +/** + * Download an iCal file from the web and parse it. + * + * @param {string} url - URL of file to request. + * @param {Object|icsCallback} [opts] - Options to pass to request() from npm:request. + * Alternatively you can pass the callback function directly. + * If no callback is provided a promise will be returned. + * @param {icsCallback} [cb] - Callback function. + * If no callback is provided a promise will be returned. + * + * @returns {optionalPromise} Promise is returned if no callback is passed. + */ +async.fromURL = function(url, opts, cb) { + return promiseCallback(function(resolve, reject) { + request(url, opts, function(err, res, data) { + if (err) { + reject(err); + return; + } + // if (r.statusCode !== 200) { + // all ok status codes should be accepted (any 2XX code) + if (Math.floor(res.statusCode / 100) !== 2) { + reject(new Error(`${res.statusCode} ${res.statusMessage}`)); + return; + } + ical.parseICS(data, function(err, ics) { + if (err) { + reject(err); + return; + } + resolve(ics); + }); + }); + }, cb); +}; + +/** + * Load iCal data from a file and parse it. + * + * @param {string} filename - File path to load. + * @param {icsCallback} [cb] - Callback function. + * If no callback is provided a promise will be returned. + * + * @returns {optionalPromise} Promise is returned if no callback is passed. + */ +async.parseFile = function(filename, cb) { + return promiseCallback(function(resolve, reject) { + fs.readFile(filename, 'utf8', function(err, data) { + if (err) { + reject(err); + return; + } + ical.parseICS(data, function(err, ics) { + if (err) { + reject(err); + return; + } + resolve(ics); + }); + }); + }, cb); +}; + +/** + * Parse iCal data from a string. + * + * @param {string} data - String containing iCal data. + * @param {icsCallback} [cb] - Callback function. + * If no callback is provided a promise will be returned. + * + * @returns {optionalPromise} Promise is returned if no callback is passed. + */ +async.parseICS = function(data, cb) { + return promiseCallback(function(resolve, reject) { + ical.parseICS(data, function(err, ics) { + if (err) { + reject(err); + return; + } + resolve(ics); + }); + }, cb); +}; + +/** + * Load iCal data from a file and parse it. + * + * @param {string} filename - File path to load. + * + * @returns {iCalData} Parsed iCal data. + */ +sync.parseFile = function(filename) { + const data = fs.readFileSync(filename, 'utf8'); + return ical.parseICS(data); +}; + +/** + * Parse iCal data from a string. + * + * @param {string} data - String containing iCal data. + * + * @returns {iCalData} Parsed iCal data. + */ +sync.parseICS = function(data) { + return ical.parseICS(data); +}; + +/** + * Load iCal data from a file and parse it. + * + * @param {string} filename - File path to load. + * @param {icsCallback} [cb] - Callback function. + * If no callback is provided this function runs synchronously. + * + * @returns {iCalData|undefined} Parsed iCal data or undefined if a callback is being used. + */ +autodetect.parseFile = function(filename, cb) { + if (!cb) return sync.parseFile(filename); + + async.parseFile(filename, cb); +}; + +/** + * Parse iCal data from a string. + * + * @param {string} data - String containing iCal data. + * @param {icsCallback} [cb] - Callback function. + * If no callback is provided this function runs synchronously. + * + * @returns {iCalData|undefined} Parsed iCal data or undefined if a callback is being used. + */ +autodetect.parseICS = function(data, cb) { + if (!cb) return sync.parseICS(data); + + async.parseICS(data, cb); +}; + +// export api functions +module.exports = { + // autodetect + fromURL: async.fromURL, + parseFile: autodetect.parseFile, + parseICS: autodetect.parseICS, + // sync + sync, + // async + async, + // other backwards compat things + objectHandlers: ical.objectHandlers, + handleObject: ical.handleObject, + parseLines: ical.parseLines, +}; diff --git a/node-ical.d.ts b/node-ical.d.ts new file mode 100644 index 0000000..2aab340 --- /dev/null +++ b/node-ical.d.ts @@ -0,0 +1,121 @@ +declare module 'node-ical' { + import { CoreOptions } from 'request'; + + /** + * Methods (Sync) + */ + export interface NodeICalSync { + parseICS(body: string): CalendarResponse; + + fromFile(file: string): CalendarResponse; + } + + export const sync: NodeICalSync; + + /** + * Methods (Async) + */ + export interface NodeICalAsync { + fromURL(url: string, callback: NodeIcalCallback): void; + + fromURL(url: string, options: CoreOptions | NodeIcalCallback, callback?: NodeIcalCallback): void; + + fromURL(url: string): Promise; + + parseICS(body: string, callback: NodeIcalCallback): void; + + parseICS(body: string): Promise; + + fromFile(file: string, callback: NodeIcalCallback): void; + + fromFile(file: string): Promise; + } + + export const async: NodeICalAsync; + + /** + * Methods (Autodetect) + */ + export function fromURL(url: string, callback: NodeIcalCallback): void; + + export function fromURL(url: string, options: CoreOptions | NodeIcalCallback, callback?: NodeIcalCallback): void; + + export function fromURL(url: string): Promise; + + export function parseICS(body: string, callback: NodeIcalCallback): void; + + export function parseICS(body: string): CalendarResponse; + + export function fromFile(file: string, callback: NodeIcalCallback): void; + + export function fromFile(file: string): CalendarResponse; + + /** + * Response objects + */ + export type NodeIcalCallback = (err: any, data: CalendarResponse) => void; + + export interface CalendarResponse { + [key: string]: CalendarComponent; + } + + export type CalendarComponent = VTimeZone | VEvent; + + export type VTimeZone = TimeZoneProps & TimeZoneDictionary; + + interface TimeZoneProps extends BaseComponent { + type: 'VTIMEZONE'; + tzid: string; + tzurl: string; + } + + interface TimeZoneDictionary { + [key: string]: TimeZoneDef | undefined; + } + + export interface VEvent extends BaseComponent { + type: 'VEVENT'; + dtstamp: DateWithTimeZone; + uid: string; + sequence: string; + transparency: Transparency; + class: Class; + summary: string; + start: DateWithTimeZone; + datetype: DateType; + end: DateWithTimeZone; + location: string; + description: string; + url: string; + completion: string; + created: DateWithTimeZone; + lastmodified: DateWithTimeZone; + + // I am not entirely sure about these, leave them as any for now.. + organizer: any; + exdate: any; + geo: any; + recurrenceid: any; + } + + export interface BaseComponent { + params: any[]; + } + + export interface TimeZoneDef { + type: 'DAYLIGHT' | 'STANDARD'; + params: any[]; + tzoffsetfrom: string; + tzoffsetto: string; + tzname: string; + start: DateWithTimeZone; + dateType: DateType; + rrule: string; + rdate: string | string[]; + } + + export type DateWithTimeZone = Date & { tz: string }; + export type DateType = 'date-time' | 'date'; + export type Transparency = 'TRANSPARENT' | 'OPAQUE'; + export type Class = 'PUBLIC' | 'PRIVATE' | 'CONFIDENTIAL'; +} diff --git a/node-ical.js b/node-ical.js deleted file mode 100644 index a62a4f9..0000000 --- a/node-ical.js +++ /dev/null @@ -1,47 +0,0 @@ -var ical = require('./ical') - , fs = require('fs') - -exports.parseFile = function(filename){ - return ical.parseICS(fs.readFileSync(filename, 'utf8')) -} - -var rrule = require('rrule').RRule - -ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){ - curr.rrule = line; - return curr -} -var originalEnd = ical.objectHandlers['END']; -ical.objectHandlers['END'] = function (val, params, curr, stack) { - // Recurrence rules are only valid for VEVENT, VTODO, and VJOURNAL. - // More specifically, we need to filter the VCALENDAR type because we might end up with a defined rrule - // due to the subtypes. - if ((val === "VEVENT") || (val === "VTODO") || (val === "VJOURNAL")) { - if (curr.rrule) { - var rule = curr.rrule.replace('RRULE:', ''); - if (rule.indexOf('DTSTART') === -1) { - - if (curr.start.length === 8) { - var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start); - if (comps) { - curr.start = new Date(comps[1], comps[2] - 1, comps[3]); - } - } - - - if (typeof curr.start.toISOString === 'function') { - try { - rule += ';DTSTART=' + curr.start.toISOString().replace(/[-:]/g, ''); - rule = rule.replace(/\.[0-9]{3}/, ''); - } catch (error) { - console.error("ERROR when trying to convert to ISOString", error); - } - } else { - console.error("No toISOString function in curr.start", curr.start); - } - } - curr.rrule = rrule.fromString(rule); - } - } - return originalEnd.call(this, val, params, curr, stack); -} diff --git a/package-lock.json b/package-lock.json index 0a9e14d..ee9378a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,183 @@ { - "name": "ical", - "version": "0.8.0", + "name": "node-ical", + "version": "0.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", + "dev": true + }, + "@types/node": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", + "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==", + "dev": true + }, + "@types/request": { + "version": "2.48.3", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.3.tgz", + "integrity": "sha512-3Wo2jNYwqgXcIz/rrq18AdOZUQB8cQ34CXZo+LUwPJNpvRAL86+Kc2wwI8mqpz9Cr1V+enIox5v+WZhy/p3h8w==", + "dev": true, + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/tough-cookie": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", + "dev": true + }, + "acorn": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", + "dev": true + }, + "acorn-jsx": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", + "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", + "dev": true + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -20,34 +188,600 @@ "concat-map": "0.0.1" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "diff": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.8.tgz", "integrity": "sha1-NDJ2MI7Jkbe8giZ+1VvBQR+XFmY=", "dev": true }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", + "integrity": "sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.0", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-inspect": "^1.6.0", + "object-keys": "^1.1.1", + "string.prototype.trimleft": "^2.1.0", + "string.prototype.trimright": "^2.1.0" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.5.1.tgz", + "integrity": "sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.2", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.4.1", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-config-prettier": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.3.0.tgz", + "integrity": "sha512-EWaGjlDAZRzVFveh2Jsglcere2KK5CJBhkNSa1xs3KfMUGdRiT7lG089eqPdvlzWHpAqaekubOsOMu8W8Yk71A==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", + "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", + "dev": true, + "requires": { + "debug": "^2.6.8", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.18.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", + "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.11.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz", + "integrity": "sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", + "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.0.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.1.tgz", + "integrity": "sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==", + "dev": true, + "requires": { + "acorn": "^7.0.0", + "acorn-jsx": "^5.0.2", + "eslint-visitor-keys": "^1.1.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, "eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", "dev": true }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -58,58 +792,1015 @@ "path-is-absolute": "^1.0.0" } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", "dev": true, "requires": { - "once": "^1.3.0", - "wrappy": "1" + "is-glob": "^4.0.1" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, - "luxon": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.3.3.tgz", - "integrity": "sha512-3CM0jpS3mbHwWoPYprX1/Zsd5esni0LkhMfSiSY6xQ3/M3pnct3OPWbWkQdEEl9MO9593k6PvDn1DhxCkpuZEw==", - "optional": true + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "dev": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { - "brace-expansion": "^1.1.7" + "ajv": "^6.5.5", + "har-schema": "^2.0.0" } }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "wrappy": "1" + "function-bind": "^1.1.1" } }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "rrule": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.4.1.tgz", - "integrity": "sha512-+NcvhETefswZq13T8nkuEnnQ6YgUeZaqMqVbp+ZiFDPCbp3AVgQIwUvNVDdMNrP05bKZG9ddDULFp0qZZYDrxg==", + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", + "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", + "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "luxon": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.19.3.tgz", + "integrity": "sha512-YwTDjGRQC0QC9Iya2g2eKZfgEFqRId4ZoLHORQcfTMB/5xrTx427V7ZPjQJ1vzvhA2vJfG2bh1Kv8V8IFMWCUA==", + "optional": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "moment-timezone": { + "version": "0.5.26", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.26.tgz", + "integrity": "sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g==", + "requires": { + "moment": ">= 2.9.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-inspect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", + "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prettier": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", + "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "psl": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", + "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "resolve": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, "requires": { - "luxon": "^1.3.3" + "glob": "^7.1.3" + } + }, + "rrule": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.2.tgz", + "integrity": "sha512-xL38CM1zOYOIp4OO8hdD6zHH5UdR9siHMvPiv+CCSh7o0LYJ0owg87QcFW7GXJ0PfpLBHjanEMvvBjJxbRhAcQ==", + "requires": { + "luxon": "^1.3.3", + "tslib": "^1.9.0" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", + "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", + "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", + "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" } }, "underscore": { @@ -118,6 +1809,45 @@ "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", "dev": true }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "vows": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/vows/-/vows-0.8.2.tgz", @@ -129,11 +1859,35 @@ "glob": "^7.1.2" } }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } } } } diff --git a/package.json b/package.json index 9ccf4bc..136ec05 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,44 @@ { - "name": "ical", - "version": "0.8.0", + "name": "node-ical", + "version": "0.11.0", "main": "index.js", - "description": "A tolerant, minimal icalendar parser", + "types": "node-ical.d.ts", + "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ "ical", "ics", - "calendar" + "calendar", + "nodejs" ], - "homepage": "https://github.com/peterbraden/ical.js", - "author": "Peter Braden (peterbraden.co.uk)", + "homepage": "https://github.com/jens-maus/node-ical", + "author": "Jens Maus ", "license": "Apache-2.0", "repository": { "type": "git", - "url": "git://github.com/peterbraden/ical.js.git" + "url": "git://github.com/jens-maus/node-ical.git" }, "dependencies": { - "rrule": "2.4.1" + "moment-timezone": "^0.5.23", + "request": "^2.88.0", + "rrule": "^2.5.6", + "uuid": "^3.3.2" }, "devDependencies": { - "vows": "0.8.2", - "underscore": "1.9.1" + "@types/request": "^2.48.3", + "eslint": "^6.5.1", + "eslint-config-prettier": "^6.3.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-prettier": "^3.1.1", + "prettier": "^1.18.2", + "underscore": "1.9.1", + "vows": "^0.8.2" }, "scripts": { - "test": "./node_modules/vows/bin/vows ./test/test.js" + "test": "vows test/test.js && vows test/testAsync.js && printf \"\\n\"", + "lint": "eslint *.js test/*.js", + "fix": "eslint --fix *.js test/*.js", + "format-check": "prettier --config .prettierrc.js --check *.js test/*.js", + "format": "prettier --config .prettierrc.js --write *.js test/*.js", + "precommit": "npm run format && npm run fix" } } diff --git a/readme.md b/readme.md deleted file mode 100644 index c95b6d3..0000000 --- a/readme.md +++ /dev/null @@ -1,50 +0,0 @@ -# ical.js # -(Formerly node-ical) - -[![Build Status](https://travis-ci.org/peterbraden/ical.js.png)](https://travis-ci.org/peterbraden/ical.js) - -A tolerant, minimal icalendar parser for javascript/node -(http://tools.ietf.org/html/rfc5545) - -## Install - Node.js ## - -ical.js is availble on npm: - - npm install ical - -## API ## - - ical.parseICS(str) - -Parses a string with an ICS File - - var data = ical.parseFile(filename) - -## Example 1 - Print list of upcoming node conferences (see example.js) -```javascript -'use strict'; - -const ical = require('ical'); -const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - -const data = ical.parseICS('content from ical file'); - -for (let k in data) { - if (data.hasOwnProperty(k)) { - var ev = data[k]; - if (data[k].type == 'VEVENT') { - console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`); - - } - } -} -``` - -## Recurrences and Exceptions ## -Calendar events with recurrence rules can be significantly more complicated to handle correctly. There are three parts to handling them: - - 1. rrule - the recurrence rule specifying the pattern of recurring dates and times for the event. - 2. recurrences - an optional array of event data that can override specific occurrences of the event. - 3. exdate - an optional array of dates that should be excluded from the recurrence pattern. - -See example_rrule.js for an example of handling recurring calendar events. diff --git a/test/test.js b/test/test.js index c675ae8..1a0bad6 100755 --- a/test/test.js +++ b/test/test.js @@ -1,520 +1,616 @@ -/**** - * Tests - * - * - ***/ -process.env.TZ = 'America/San_Francisco'; -var ical = require('../index') - -var vows = require('vows') - , assert = require('assert') - , _ = require('underscore') - -vows.describe('node-ical').addBatch({ - 'when parsing test1.ics (node conferences schedule from lanyrd.com, modified)': { - topic: function () { - return ical.parseFile('./test/test1.ics') - } - - ,'we get 9 events': function (topic) { - var events = _.select(_.values(topic), function(x){ return x.type==='VEVENT'}) - assert.equal (events.length, 9); - } - - ,'event 47f6e' : { - topic: function(events){ - return _.select(_.values(events), - function(x){ - return x.uid ==='47f6ea3f28af2986a2192fa39a91fa7d60d26b76'})[0] - } - ,'is in fort lauderdale' : function(topic){ - assert.equal(topic.location, "Fort Lauderdale, United States") - } - ,'starts Tue, 29 Nov 2011' : function(topic){ - assert.equal(topic.start.toDateString(), new Date(2011,10,29).toDateString()) - } - } - , 'event 480a' : { - topic: function(events){ - return _.select(_.values(events), - function(x){ - return x.uid ==='480a3ad48af5ed8965241f14920f90524f533c18'})[0] - } - , 'has a summary (invalid colon handling tolerance)' : function(topic){ - assert.equal(topic.summary, '[Async]: Everything Express') - } - , 'has a date only start datetime' : function(topic){ - assert.equal(topic.start.dateOnly, true) - } - , 'has a date only end datetime' : function(topic){ - assert.equal(topic.end.dateOnly, true) - } - } - , 'event d4c8' :{ - topic : function(events){ - return _.select(_.values(events), - function(x){ - return x.uid === 'd4c826dfb701f611416d69b4df81caf9ff80b03a'})[0] - } - , 'has a start datetime' : function(topic){ - assert.equal(topic.start.toDateString(), new Date(Date.UTC(2011, 2, 12, 20, 0, 0)).toDateString()) - } - } - - , 'event sdfkf09fsd0 (Invalid Date)' :{ - topic : function(events){ - return _.select(_.values(events), - function(x){ - return x.uid === 'sdfkf09fsd0'})[0] - } - , 'has a start datetime' : function(topic){ - assert.equal(topic.start, "Next Year") - } - } - } - , 'with test2.ics (testing ical features)' : { - topic: function () { - return ical.parseFile('./test/test2.ics') - } - , 'todo item uid4@host1.com' : { - topic : function(items){ - return items['uid4@host1.com'] - } - , 'is a VTODO' : function(topic){ - assert.equal(topic.type, 'VTODO') - } - } - , 'vfreebusy' : { - topic: function(events) { - return _.select(_.values(events), function(x) { - return x.type === 'VFREEBUSY'; - })[0]; - } - , 'has a URL' : function(topic) { - assert.equal(topic.url, 'http://www.host.com/calendar/busytime/jsmith.ifb'); - } - } - , 'vfreebusy first freebusy' : { - topic: function(events) { - return _.select(_.values(events), function(x) { - return x.type === 'VFREEBUSY'; - })[0].freebusy[0]; - } - , 'has undefined type defaulting to busy' : function(topic) { - assert.equal(topic.type, "BUSY"); - } - , 'has an start datetime' : function(topic) { - assert.equal(topic.start.getFullYear(), 1998); - assert.equal(topic.start.getUTCMonth(), 2); - assert.equal(topic.start.getUTCDate(), 14); - assert.equal(topic.start.getUTCHours(), 23); - assert.equal(topic.start.getUTCMinutes(), 30); - } - , 'has an end datetime' : function(topic) { - assert.equal(topic.end.getFullYear(), 1998); - assert.equal(topic.end.getUTCMonth(), 2); - assert.equal(topic.end.getUTCDate(), 15); - assert.equal(topic.end.getUTCHours(), 00); - assert.equal(topic.end.getUTCMinutes(), 30); - } - } - } - , 'with test3.ics (testing tvcountdown.com)' : { - topic: function() { - return ical.parseFile('./test/test3.ics'); - } - , 'event -83' : { - topic: function(events) { - return _.select(_.values(events), function(x) { - return x.uid === '20110505T220000Z-83@tvcountdown.com'; - })[0]; - } - , 'has a start datetime' : function(topic) { - assert.equal(topic.start.getFullYear(), 2011); - assert.equal(topic.start.getMonth(), 4); - } - , 'has an end datetime' : function(topic) { - assert.equal(topic.end.getFullYear(), 2011); - assert.equal(topic.end.getMonth(), 4); - } - } - } - - , 'with test4.ics (testing tripit.com)' : { - topic: function() { - return ical.parseFile('./test/test4.ics'); - } - , 'event c32a5...' : { - topic: function(events) { - return _.select(_.values(events), function(x) { - return x.uid === 'c32a5eaba2354bb29e012ec18da827db90550a3b@tripit.com'; - })[0]; - } - , 'has a start datetime' : function(topic) { - assert.equal(topic.start.getFullYear(), 2011); - assert.equal(topic.start.getMonth(), 09); - assert.equal(topic.start.getDate(), 11); - } - - , 'has a summary' : function(topic){ - // escaped commas and semicolons should be replaced - assert.equal(topic.summary, 'South San Francisco, CA, October 2011;') - - } - - , 'has a description' : function(topic){ - var desired = 'John Doe is in South San Francisco, CA from Oct 11 ' + - 'to Oct 13, 2011\nView and/or edit details in TripIt : http://www.tripit.c' + - 'om/trip/show/id/23710889\nTripIt - organize your travel at http://www.trip' + - 'it.com\n' - assert.equal(topic.description, desired) - - } - - , 'has a geolocation' : function(topic){ - assert.ok(topic.geo, 'no geo param') - assert.equal(topic.geo.lat, 37.654656) - assert.equal(topic.geo.lon, -122.40775) - } - - , 'has transparency' : function(topic){ - assert.equal(topic.transparency, 'TRANSPARENT') - } - - } - } - - - - , 'with test5.ics (testing meetup.com)' : { - topic: function () { - return ical.parseFile('./test/test5.ics') - } - , 'event nsmxnyppbfc@meetup.com' : { - topic: function(events) { - return _.select(_.values(events), function(x) { - return x.uid === 'event_nsmxnyppbfc@meetup.com'; - })[0]; - } - , 'has a start' : function(topic){ - assert.equal(topic.start.tz, 'America/Phoenix') - assert.equal(topic.start.toISOString(), new Date(2011, 10, 09, 19, 0,0).toISOString()) - } - } - } - - , 'with test6.ics (testing assembly.org)': { - topic: function () { - return ical.parseFile('./test/test6.ics') - } - , 'event with no ID' : { - topic: function(events) { - return _.select(_.values(events), function(x) { - return x.summary === 'foobar Summer 2011 starts!'; - })[0]; - } - , 'has a start' : function(topic){ - assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 12, 0,0).toISOString()) - } - } - , 'event with rrule' :{ - topic: function(events){ - return _.select(_.values(events), function(x){ - return x.summary == "foobarTV broadcast starts" - })[0]; - } - , "Has an RRULE": function(topic){ - assert.notEqual(topic.rrule, undefined); - } - , "RRule text": function(topic){ - assert.equal(topic.rrule.toText(), "every 5 weeks on Monday, Friday until January 30, 2013") - } - } - } - , 'with test7.ics (testing dtstart of rrule)' :{ - topic: function() { - return ical.parseFile('./test/test7.ics'); - }, - 'recurring yearly event (14 july)': { - topic: function(events){ - var ev = _.values(events)[0]; - return ev.rrule.between(new Date(2013, 0, 1), new Date(2014, 0, 1)); - }, - 'dt start well set': function(topic) { - assert.equal(topic[0].toDateString(), new Date(2013, 6, 14).toDateString()); - } - } - } - , "with test 8.ics (VTODO completion)": { - topic: function() { - return ical.parseFile('./test/test8.ics'); - }, - 'grabbing VTODO task': { - topic: function(topic) { - return _.values(topic)[0]; - }, - 'task completed': function(task){ - assert.equal(task.completion, 100); - assert.equal(task.completed.toISOString(), new Date(2013, 06, 16, 10, 57, 45).toISOString()); - } - } - } - , "with test 9.ics (VEVENT with VALARM)": { - topic: function() { - return ical.parseFile('./test/test9.ics'); - }, - 'grabbing VEVENT task': { - topic: function(topic) { - return _.values(topic)[0]; - }, - 'task completed': function(task){ - assert.equal(task.summary, "Event with an alarm"); - } - } - } - , 'with test 11.ics (VEVENT with custom properties)': { - topic: function() { - return ical.parseFile('./test10.ics'); - }, - 'grabbing custom properties': { - topic: function(topic) { - - } - } - }, - - 'with test10.ics': { - topic: function () { - return ical.parseFile('./test/test10.ics'); - }, - - 'when categories present': { - topic: function (t) {return _.values(t)[0]}, - - 'should be a list': function (e) { - assert(e.categories instanceof [].constructor); - }, - - 'should contain individual category values': function (e) { - assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']); - } - }, - - 'when categories present with trailing whitespace': { - topic: function (t) {return _.values(t)[1]}, - - 'should contain individual category values without whitespace': function (e) { - assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']); - } - }, - - 'when categories present but empty': { - topic: function (t) {return _.values(t)[2]}, - - 'should be an empty list': function (e) { - assert.deepEqual(e.categories, []); - } - }, - - 'when categories present but singular': { - topic: function (t) {return _.values(t)[3]}, - - 'should be a list of single item': function (e) { - assert.deepEqual(e.categories, ['lonely-cat']); - } - }, - - 'when categories present on multiple lines': { - topic: function (t) {return _.values(t)[4]}, - - 'should contain the category values in an array': function (e) { - assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']); - } - } - }, - - 'with test11.ics (testing zimbra freebusy)': { - topic: function () { - return ical.parseFile('./test/test11.ics'); - }, - - 'freebusy params' : { - topic: function(events) { - return _.values(events)[0]; - } - , 'has a URL' : function(topic) { - assert.equal(topic.url, 'http://mail.example.com/yvr-2a@example.com/20140416'); - } - , 'has an ORGANIZER' : function(topic) { - assert.equal(topic.organizer, 'mailto:yvr-2a@example.com'); - } - , 'has an start datetime' : function(topic) { - assert.equal(topic.start.getFullYear(), 2014); - assert.equal(topic.start.getMonth(), 3); - } - , 'has an end datetime' : function(topic) { - assert.equal(topic.end.getFullYear(), 2014); - assert.equal(topic.end.getMonth(), 6); - } - } - , 'freebusy busy events' : { - topic: function(events) { - return _.select(_.values(events)[0].freebusy, function(x) { - return x.type === 'BUSY'; - })[0]; - } - , 'has an start datetime' : function(topic) { - assert.equal(topic.start.getFullYear(), 2014); - assert.equal(topic.start.getMonth(), 3); - assert.equal(topic.start.getUTCHours(), 15); - assert.equal(topic.start.getUTCMinutes(), 15); - } - , 'has an end datetime' : function(topic) { - assert.equal(topic.end.getFullYear(), 2014); - assert.equal(topic.end.getMonth(), 3); - assert.equal(topic.end.getUTCHours(), 19); - assert.equal(topic.end.getUTCMinutes(), 00); - } - } - } - - , 'with test12.ics (testing recurrences and exdates)': { - topic: function () { - return ical.parseFile('./test/test12.ics') - } - , 'event with rrule': { - topic: function (events) { - return _.select(_.values(events), function (x) { - return x.uid === '0000001'; - })[0]; - } - , "Has an RRULE": function (topic) { - assert.notEqual(topic.rrule, undefined); - } - , "Has summary Treasure Hunting": function (topic) { - assert.equal(topic.summary, 'Treasure Hunting'); - } - , "Has two EXDATES": function (topic) { - assert.notEqual(topic.exdate, undefined); - assert.notEqual(topic.exdate[new Date(2015, 06, 08, 12, 0, 0).toISOString().substring(0, 10)], undefined); - assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString().substring(0, 10)], undefined); - } - , "Has a RECURRENCE-ID override": function (topic) { - assert.notEqual(topic.recurrences, undefined); - assert.notEqual(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)], undefined); - assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)].summary, 'More Treasure Hunting'); - } - } - } - - , 'with test13.ics (testing recurrence-id before rrule)': { - topic: function () { - return ical.parseFile('./test/test13.ics') - } - , 'event with rrule': { - topic: function (events) { - return _.select(_.values(events), function (x) { - return x.uid === '6m2q7kb2l02798oagemrcgm6pk@google.com'; - })[0]; - } - , "Has an RRULE": function (topic) { - assert.notEqual(topic.rrule, undefined); - } - , "Has summary 'repeated'": function (topic) { - assert.equal(topic.summary, 'repeated'); - } - , "Has a RECURRENCE-ID override": function (topic) { - assert.notEqual(topic.recurrences, undefined); - assert.notEqual(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)], undefined); - assert.equal(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)].summary, 'bla bla'); - } - } - } - - , 'with test14.ics (testing comma-separated exdates)': { - topic: function () { - return ical.parseFile('./test/test14.ics') - } - , 'event with comma-separated exdate': { - topic: function (events) { - return _.select(_.values(events), function (x) { - return x.uid === '98765432-ABCD-DCBB-999A-987765432123'; - })[0]; - } - , "Has summary 'Example of comma-separated exdates'": function (topic) { - assert.equal(topic.summary, 'Example of comma-separated exdates'); - } - , "Has four comma-separated EXDATES": function (topic) { - assert.notEqual(topic.exdate, undefined); - // Verify the four comma-separated EXDATES are there - assert.notEqual(topic.exdate[new Date(2017, 6, 6, 12, 0, 0).toISOString().substring(0, 10)], undefined); - assert.notEqual(topic.exdate[new Date(2017, 6, 17, 12, 0, 0).toISOString().substring(0, 10)], undefined); - assert.notEqual(topic.exdate[new Date(2017, 6, 20, 12, 0, 0).toISOString().substring(0, 10)], undefined); - assert.notEqual(topic.exdate[new Date(2017, 7, 3, 12, 0, 0).toISOString().substring(0, 10)], undefined); - // Verify an arbitrary date isn't there - assert.equal(topic.exdate[new Date(2017, 4, 5, 12, 0, 0).toISOString().substring(0, 10)], undefined); - } - } - } - - , 'with test14.ics (testing exdates with bad times)': { - topic: function () { - return ical.parseFile('./test/test14.ics') - } - , 'event with exdates with bad times': { - topic: function (events) { - return _.select(_.values(events), function (x) { - return x.uid === '1234567-ABCD-ABCD-ABCD-123456789012'; - })[0]; - } - , "Has summary 'Example of exdate with bad times'": function (topic) { - assert.equal(topic.summary, 'Example of exdate with bad times'); - } - , "Has two EXDATES even though they have bad times": function (topic) { - assert.notEqual(topic.exdate, undefined); - // Verify the two EXDATES are there, even though they have bad times - assert.notEqual(topic.exdate[new Date(2017, 11, 18, 12, 0, 0).toISOString().substring(0, 10)], undefined); - assert.notEqual(topic.exdate[new Date(2017, 11, 19, 12, 0, 0).toISOString().substring(0, 10)], undefined); - } - } - } - - - , 'with test15.ics (testing Microsoft Exchange Server 2010 with timezones)' : { - topic: function () { - return ical.parseFile('./test/test15.ics') - } - , 'event with start and end including timezones' : { - topic: function(events) { - return _.select(_.values(events), function(x) { - return x.uid === '040000008200E00074C5B7101A82E00800000000C9AB6E5A6AFED401000000000000000010000000C55132227F0F0948A7D58F6190A3AEF9'; - })[0]; - } - , 'has a start' : function(topic){ - assert.equal(topic.start.tz, "(UTC+07:00) Bangkok, Hanoi, Jakarta") - assert.equal(topic.start.toISOString(), new Date(2019, 3, 30, 9, 0, 0).toISOString()) - assert.equal(topic.end.tz, "(UTC+07:00) Bangkok, Hanoi, Jakarta") - assert.equal(topic.end.toISOString(), new Date(2019, 3, 30, 12, 0, 0).toISOString()) - } - } - } - - , 'url request errors': { - topic : function () { - ical.fromURL('http://255.255.255.255/', {}, this.callback); - } - , 'are passed back to the callback' : function (err, result) { - assert.instanceOf(err, Error); - if (!err){ - console.log(">E:", err, result) - } - } - } -}).export(module) - - -//ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', -// {}, -// function(err, data){ -// console.log("OUT:", data) -// }) +/** ** + * Tests + * + * + ** */ +process.env.TZ = 'America/San_Francisco'; + +const vows = require('vows'); +const assert = require('assert'); +const _ = require('underscore'); +const ical = require('../index'); + +vows.describe('node-ical') + .addBatch({ + 'when parsing test1.ics (node conferences schedule from lanyrd.com, modified)': { + 'topic': function() { + return ical.parseFile('./test/test1.ics'); + }, + + 'we get 9 events': function(topic) { + const events = _.select(_.values(topic), function(x) { + return x.type === 'VEVENT'; + }); + assert.equal(events.length, 9); + }, + + 'event 47f6e': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '47f6ea3f28af2986a2192fa39a91fa7d60d26b76'; + })[0]; + }, + 'is in fort lauderdale': function(topic) { + assert.equal(topic.location, 'Fort Lauderdale, United States'); + }, + 'starts Tue, 29 Nov 2011': function(topic) { + assert.equal(topic.start.toDateString(), new Date(2011, 10, 29).toDateString()); + }, + 'datetype is date': function(topic) { + assert.equal(topic.datetype, 'date'); + }, + }, + 'event 480a': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '480a3ad48af5ed8965241f14920f90524f533c18'; + })[0]; + }, + 'has a summary (invalid colon handling tolerance)': function(topic) { + assert.equal(topic.summary, '[Async]: Everything Express'); + }, + 'has a date only start datetime' : function(topic) { + assert.equal(topic.start.dateOnly, true); + }, + 'has a date only end datetime' : function(topic) { + assert.equal(topic.end.dateOnly, true); + }, + }, + 'event d4c8': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === 'd4c826dfb701f611416d69b4df81caf9ff80b03a'; + })[0]; + }, + 'has a start datetime': function(topic) { + assert.equal(topic.start.toDateString(), new Date(Date.UTC(2011, 2, 12, 20, 0, 0)).toDateString()); + }, + 'datetype is date-time': function(topic) { + assert.equal(topic.datetype, 'date-time'); + }, + }, + + 'event sdfkf09fsd0 (Invalid Date)': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === 'sdfkf09fsd0'; + })[0]; + }, + 'has a start datetime': function(topic) { + assert.equal(topic.start, 'Next Year'); + }, + }, + }, + 'with test2.ics (testing ical features)': { + 'topic': function() { + return ical.parseFile('./test/test2.ics'); + }, + 'todo item uid4@host1.com': { + 'topic': function(items) { + return _.filter(items, function(obj) { + { + return obj.uid == 'uid4@host1.com'; + } + })[0]; + }, + 'is a VTODO': function(topic) { + assert.equal(topic.type, 'VTODO'); + }, + }, + 'vfreebusy': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.type === 'VFREEBUSY'; + })[0]; + }, + 'has a URL': function(topic) { + assert.equal(topic.url, 'http://www.host.com/calendar/busytime/jsmith.ifb'); + }, + }, + 'vfreebusy first freebusy': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.type === 'VFREEBUSY'; + })[0].freebusy[0]; + }, + 'has undefined type defaulting to busy': function(topic) { + assert.equal(topic.type, 'BUSY'); + }, + 'has an start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 1998); + assert.equal(topic.start.getUTCMonth(), 2); + assert.equal(topic.start.getUTCDate(), 14); + assert.equal(topic.start.getUTCHours(), 23); + assert.equal(topic.start.getUTCMinutes(), 30); + }, + 'has an end datetime': function(topic) { + assert.equal(topic.end.getFullYear(), 1998); + assert.equal(topic.end.getUTCMonth(), 2); + assert.equal(topic.end.getUTCDate(), 15); + assert.equal(topic.end.getUTCHours(), 0); + assert.equal(topic.end.getUTCMinutes(), 30); + }, + }, + 'tzid parsing': { + 'topic': function(events) { + return _.filter(events, function(obj) { + { + return obj.uid == 'EC9439B1-FF65-11D6-9973-003065F99D04'; + } + })[0]; + }, + 'tzid offset correctly applied': function(event) { + const start = new Date('2002-10-28T22:00:00.000Z'); + assert.equal(event.start.valueOf(), start.valueOf()); + }, + }, + }, + 'with test3.ics (testing tvcountdown.com)': { + 'topic': function() { + return ical.parseFile('./test/test3.ics'); + }, + 'event -83': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '20110505T220000Z-83@tvcountdown.com'; + })[0]; + }, + 'has a start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 2011); + assert.equal(topic.start.getMonth(), 4); + }, + 'has an end datetime': function(topic) { + assert.equal(topic.end.getFullYear(), 2011); + assert.equal(topic.end.getMonth(), 4); + }, + 'datetype is date-time': function(topic) { + assert.equal(topic.datetype, 'date-time'); + }, + }, + }, + + 'with test4.ics (testing tripit.com)': { + 'topic': function() { + return ical.parseFile('./test/test4.ics'); + }, + 'event c32a5...': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === 'c32a5eaba2354bb29e012ec18da827db90550a3b@tripit.com'; + })[0]; + }, + 'has a start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 2011); + assert.equal(topic.start.getMonth(), 9); + assert.equal(topic.start.getDate(), 11); + }, + + 'has a summary': function(topic) { + // escaped commas and semicolons should be replaced + assert.equal(topic.summary, 'South San Francisco, CA, October 2011;'); + }, + + 'has a description': function(topic) { + const desired = + 'John Doe is in South San Francisco, CA from Oct 11 ' + + 'to Oct 13, 2011\nView and/or edit details in TripIt : http://www.tripit.c' + + 'om/trip/show/id/23710889\nTripIt - organize your travel at http://www.trip' + + 'it.com\n'; + assert.equal(topic.description, desired); + }, + + 'has a geolocation': function(topic) { + assert.ok(topic.geo, 'no geo param'); + assert.equal(topic.geo.lat, 37.654656); + assert.equal(topic.geo.lon, -122.40775); + }, + + 'has transparency': function(topic) { + assert.equal(topic.transparency, 'TRANSPARENT'); + }, + }, + }, + + 'with test5.ics (testing meetup.com)': { + 'topic': function() { + return ical.parseFile('./test/test5.ics'); + }, + 'event nsmxnyppbfc@meetup.com': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === 'event_nsmxnyppbfc@meetup.com'; + })[0]; + }, + 'has a start': function(topic) { + assert.equal(topic.start.tz, 'America/Phoenix'); + assert.equal(topic.start.toISOString(), new Date(Date.UTC(2011, 10, 10, 2, 0, 0)).toISOString()); + }, + }, + }, + + 'with test6.ics (testing assembly.org)': { + 'topic': function() { + return ical.parseFile('./test/test6.ics'); + }, + 'event with no ID': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.summary === 'foobar Summer 2011 starts!'; + })[0]; + }, + 'has a start': function(topic) { + assert.equal(topic.start.toISOString(), new Date(2011, 7, 4, 0, 0, 0).toISOString()); + }, + }, + 'event with rrule': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.summary == 'foobarTV broadcast starts'; + })[0]; + }, + 'Has an RRULE': function(topic) { + assert.notEqual(topic.rrule, undefined); + }, + 'RRule text': function(topic) { + assert.equal(topic.rrule.toText(), 'every 5 weeks on Monday, Friday until January 30, 2013'); + }, + }, + }, + 'with test7.ics (testing dtstart of rrule)': { + 'topic': function() { + return ical.parseFile('./test/test7.ics'); + }, + 'recurring yearly event (14 july)': { + 'topic': function(events) { + const ev = _.values(events)[0]; + return ev.rrule.between(new Date(2013, 0, 1), new Date(2014, 0, 1)); + }, + 'dt start well set': function(topic) { + assert.equal(topic[0].toDateString(), new Date(2013, 6, 14).toDateString()); + }, + }, + }, + 'with test 8.ics (VTODO completion)': { + 'topic': function() { + return ical.parseFile('./test/test8.ics'); + }, + 'grabbing VTODO task': { + 'topic': function(topic) { + return _.values(topic)[0]; + }, + 'task completed': function(task) { + assert.equal(task.completion, 100); + assert.equal(task.completed.toISOString(), new Date(2013, 6, 16, 10, 57, 45).toISOString()); + }, + }, + }, + 'with test 9.ics (VEVENT with VALARM)': { + 'topic': function() { + return ical.parseFile('./test/test9.ics'); + }, + 'grabbing VEVENT task': { + 'topic': function(topic) { + return _.values(topic)[0]; + }, + 'task completed': function(task) { + assert.equal(task.summary, 'Event with an alarm'); + }, + }, + }, + 'with test 11.ics (VEVENT with custom properties)': { + 'topic': function() { + return ical.parseFile('./test10.ics'); + }, + 'grabbing custom properties': { + topic(topic) {}, + }, + }, + + 'with test10.ics': { + 'topic': function() { + return ical.parseFile('./test/test10.ics'); + }, + + 'when categories present': { + 'topic': function(t) { + return _.values(t)[0]; + }, + + 'should be a list': function(e) { + assert(e.categories instanceof [].constructor); + }, + + 'should contain individual category values': function(e) { + assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']); + }, + }, + + 'when categories present with trailing whitespace': { + 'topic': function(t) { + return _.values(t)[1]; + }, + + 'should contain individual category values without whitespace': function(e) { + assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']); + }, + }, + + 'when categories present but empty': { + 'topic': function(t) { + return _.values(t)[2]; + }, + + 'should be an empty list': function(e) { + assert.deepEqual(e.categories, []); + }, + }, + + 'when categories present but singular': { + 'topic': function(t) { + return _.values(t)[3]; + }, + + 'should be a list of single item': function(e) { + assert.deepEqual(e.categories, ['lonely-cat']); + }, + }, + + 'when categories present on multiple lines': { + 'topic': function(t) { + return _.values(t)[4]; + }, + + 'should contain the category values in an array': function(e) { + assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']); + }, + }, + }, + + 'with test11.ics (testing zimbra freebusy)': { + 'topic': function() { + return ical.parseFile('./test/test11.ics'); + }, + + 'freebusy params': { + 'topic': function(events) { + return _.values(events)[0]; + }, + 'has a URL': function(topic) { + assert.equal(topic.url, 'http://mail.example.com/yvr-2a@example.com/20140416'); + }, + 'has an ORGANIZER': function(topic) { + assert.equal(topic.organizer, 'mailto:yvr-2a@example.com'); + }, + 'has an start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 2014); + assert.equal(topic.start.getMonth(), 3); + }, + 'has an end datetime': function(topic) { + assert.equal(topic.end.getFullYear(), 2014); + assert.equal(topic.end.getMonth(), 6); + }, + }, + 'freebusy busy events': { + 'topic': function(events) { + return _.select(_.values(events)[0].freebusy, function(x) { + return x.type === 'BUSY'; + })[0]; + }, + 'has an start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 2014); + assert.equal(topic.start.getMonth(), 3); + assert.equal(topic.start.getUTCHours(), 15); + assert.equal(topic.start.getUTCMinutes(), 15); + }, + 'has an end datetime': function(topic) { + assert.equal(topic.end.getFullYear(), 2014); + assert.equal(topic.end.getMonth(), 3); + assert.equal(topic.end.getUTCHours(), 19); + assert.equal(topic.end.getUTCMinutes(), 0); + }, + }, + }, + + 'with test12.ics (testing recurrences and exdates)': { + 'topic': function() { + return ical.parseFile('./test/test12.ics'); + }, + 'event with rrule': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '0000001'; + })[0]; + }, + 'Has an RRULE': function(topic) { + assert.notEqual(topic.rrule, undefined); + }, + 'Has summary Treasure Hunting': function(topic) { + assert.equal(topic.summary, 'Treasure Hunting'); + }, + 'Has two EXDATES': function(topic) { + assert.notEqual(topic.exdate, undefined); + assert.notEqual( + topic.exdate[new Date(Date.UTC(2015, 6, 8, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.notEqual( + topic.exdate[new Date(Date.UTC(2015, 6, 10, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + }, + 'Has a RECURRENCE-ID override': function(topic) { + assert.notEqual(topic.recurrences, undefined); + assert.notEqual( + topic.recurrences[new Date(Date.UTC(2015, 6, 7, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.equal( + topic.recurrences[new Date(Date.UTC(2015, 6, 7, 19, 0, 0)).toISOString().substring(0, 10)] + .summary, + 'More Treasure Hunting' + ); + }, + }, + }, + + 'with test13.ics (testing recurrence-id before rrule)': { + 'topic': function() { + return ical.parseFile('./test/test13.ics'); + }, + 'event with rrule': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '6m2q7kb2l02798oagemrcgm6pk@google.com'; + })[0]; + }, + 'Has an RRULE': function(topic) { + assert.notEqual(topic.rrule, undefined); + }, + "Has summary 'repeated'": function(topic) { + assert.equal(topic.summary, 'repeated'); + }, + 'Has a RECURRENCE-ID override': function(topic) { + assert.notEqual(topic.recurrences, undefined); + assert.notEqual( + topic.recurrences[new Date(Date.UTC(2016, 7, 26, 11, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.equal( + topic.recurrences[new Date(Date.UTC(2016, 7, 26, 11, 0, 0)).toISOString().substring(0, 10)] + .summary, + 'bla bla' + ); + }, + }, + }, + + 'with test14.ics (testing comma-separated exdates)': { + 'topic': function() { + return ical.parseFile('./test/test14.ics'); + }, + 'event with comma-separated exdate': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '98765432-ABCD-DCBB-999A-987765432123'; + })[0]; + }, + "Has summary 'Example of comma-separated exdates'": function(topic) { + assert.equal(topic.summary, 'Example of comma-separated exdates'); + }, + 'Has four comma-separated EXDATES': function(topic) { + assert.notEqual(topic.exdate, undefined); + // Verify the four comma-separated EXDATES are there + assert.notEqual( + topic.exdate[new Date(2017, 6, 6, 12, 0, 0).toISOString().substring(0, 10)], + undefined + ); + assert.notEqual( + topic.exdate[new Date(2017, 6, 17, 12, 0, 0).toISOString().substring(0, 10)], + undefined + ); + assert.notEqual( + topic.exdate[new Date(2017, 6, 20, 12, 0, 0).toISOString().substring(0, 10)], + undefined + ); + assert.notEqual( + topic.exdate[new Date(2017, 7, 3, 12, 0, 0).toISOString().substring(0, 10)], + undefined + ); + // Verify an arbitrary date isn't there + assert.equal( + topic.exdate[new Date(2017, 4, 5, 12, 0, 0).toISOString().substring(0, 10)], + undefined + ); + }, + }, + }, + + 'with test14.ics (testing exdates with bad times)': { + 'topic': function() { + return ical.parseFile('./test/test14.ics'); + }, + 'event with exdates with bad times': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '1234567-ABCD-ABCD-ABCD-123456789012'; + })[0]; + }, + "Has summary 'Example of exdate with bad times'": function(topic) { + assert.equal(topic.summary, 'Example of exdate with bad times'); + }, + 'Has two EXDATES even though they have bad times': function(topic) { + assert.notEqual(topic.exdate, undefined); + // Verify the two EXDATES are there, even though they have bad times + assert.notEqual( + topic.exdate[new Date(2017, 11, 18, 12, 0, 0).toISOString().substring(0, 10)], + undefined + ); + assert.notEqual( + topic.exdate[new Date(2017, 11, 19, 12, 0, 0).toISOString().substring(0, 10)], + undefined + ); + }, + }, + }, + + 'with test15.ics (testing Microsoft Exchange Server 2010 with timezones)' : { + topic: function () { + return ical.parseFile('./test/test15.ics') + }, + 'event with start and end including timezones' : { + topic: function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '040000008200E00074C5B7101A82E00800000000C9AB6E5A6AFED401000000000000000010000000C55132227F0F0948A7D58F6190A3AEF9'; + })[0]; + }, + 'has a start' : function(topic){ + assert.equal(topic.start.tz, "(UTC+07:00) Bangkok, Hanoi, Jakarta") + assert.equal(topic.start.toISOString(), new Date(2019, 3, 30, 9, 0, 0).toISOString()) + assert.equal(topic.end.tz, "(UTC+07:00) Bangkok, Hanoi, Jakarta") + assert.equal(topic.end.toISOString(), new Date(2019, 3, 30, 12, 0, 0).toISOString()) + } + } + }, + + 'with test16.ics (testing quoted parameter values)': { + 'topic': function() { + return ical.parseFile('./test/test16.ics'); + }, + 'quoted params': { + 'topic': function(events) { + return _.values(events)[0]; + }, + 'is quoted': function(topic) { + assert.notEqual(topic.start.tz, undefined); + }, + }, + }, + + 'with test17.ics (testing for non-stringified start/end time)': { + 'topic': function() { + return ical.parseFile('./test/test17.ics'); + }, + 'stringified params': { + 'topic': function(events) { + return _.values(events)[0]; + }, + 'is not string': function(topic) { + assert.notEqual(typeof topic.start, 'string'); + assert.notEqual(typeof topic.end, 'string'); + }, + }, + }, + + 'url request errors': { + 'topic': function() { + ical.fromURL('http://255.255.255.255/', {}, this.callback); + }, + 'are passed back to the callback': function(err, result) { + assert.instanceOf(err, Error); + if (!err) { + console.log('>E:', err, result); + } + }, + }, + }) + .export(module); diff --git a/test/test15.ics b/test/test15.ics index b416999..c30e3d7 100644 --- a/test/test15.ics +++ b/test/test15.ics @@ -38,4 +38,4 @@ X-MICROSOFT-CDO-IMPORTANCE:1 X-MICROSOFT-CDO-INSTTYPE:0 X-MICROSOFT-DISALLOW-COUNTER:FALSE END:VEVENT -END:VCALENDAR \ No newline at end of file +END:VCALENDAR diff --git a/test/test16.ics b/test/test16.ics new file mode 100644 index 0000000..a9efbf2 --- /dev/null +++ b/test/test16.ics @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:0000001 +SUMMARY:Treasure Hunting +DTSTART;TZID="(UTC-05:00) America/New_York":20150706T120000 +DTEND;TZID=America/Los_Angeles:20150706T130000 +RRULE:FREQ=DAILY;COUNT=10 +EXDATE;TZID=America/Los_Angeles:20150708T120000 +EXDATE;TZID=America/Los_Angeles:20150710T120000 +END:VEVENT +END:VCALENDAR diff --git a/test/test17.ics b/test/test17.ics new file mode 100644 index 0000000..817a2f1 --- /dev/null +++ b/test/test17.ics @@ -0,0 +1,40 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:Schulferien +X-APPLE-CALENDAR-COLOR:#31CC7C +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19810329T030000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19961027T030000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +UID:56df0d2b37ded@67.calovo +DTSTART;TZID=Europe/Berlin;VALUE=DATE:20180702 +SEQUENCE:1 +TRANSP:TRANSPARENT +STATUS:CONFIRMED +DTEND;TZID=Europe/Berlin;VALUE=DATE:20180811 +URL:http://i.cal.to/r/1AHk +SUMMARY:Sommerferien Sachsen +DESCRIPTION:Deine Ferien. In deinem Kalender. Immer aktuell. +X-MICROSOFT-CDO-ALLDAYEVENT:TRUE +DTSTAMP:20161129T131929Z +CREATED:20160308T173435Z +LAST-MODIFIED:20161129T131929Z +END:VEVENT +END:VCALENDAR diff --git a/test/test7.ics b/test/test7.ics index 4379cd0..f883fbf 100755 --- a/test/test7.ics +++ b/test/test7.ics @@ -8,7 +8,7 @@ DTSTAMP:20111106T124709Z UID:FA9831E7-C238-4FEC-95E5-CD46BD466421 SUMMARY:Fête Nationale - Férié RRULE:FREQ=YEARLY -DTSTART;VALUE=DATE:20120714 +DTSTART;TZID=Europe/Berlin;VALUE=DATE:20120714 DTEND;VALUE=DATE:20120715 TRANSP:OPAQUE SEQUENCE:5 diff --git a/test/testAsync.js b/test/testAsync.js new file mode 100644 index 0000000..0278f47 --- /dev/null +++ b/test/testAsync.js @@ -0,0 +1,590 @@ +/** ** + * Tests + * + * + ** */ +process.env.TZ = 'America/San_Francisco'; + +const vows = require('vows'); +const assert = require('assert'); +const _ = require('underscore'); +const ical = require('../index'); + +console.log('START Async Tests'); +vows.describe('node-ical') + .addBatch({ + 'when parsing test1.ics (node conferences schedule from lanyrd.com, modified)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test1.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + + 'we get 9 events': function(topic) { + const events = _.select(_.values(topic), function(x) { + return x.type === 'VEVENT'; + }); + assert.equal(events.length, 9); + }, + + 'event 47f6e': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '47f6ea3f28af2986a2192fa39a91fa7d60d26b76'; + })[0]; + }, + 'is in fort lauderdale': function(topic) { + assert.equal(topic.location, 'Fort Lauderdale, United States'); + }, + 'starts Tue, 29 Nov 2011': function(topic) { + assert.equal(topic.start.toDateString(), new Date(2011, 10, 29).toDateString()); + }, + 'datetype is date': function(topic) { + assert.equal(topic.datetype, 'date'); + }, + }, + 'event 480a': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '480a3ad48af5ed8965241f14920f90524f533c18'; + })[0]; + }, + 'has a summary (invalid colon handling tolerance)': function(topic) { + assert.equal(topic.summary, '[Async]: Everything Express'); + }, + }, + 'event d4c8': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === 'd4c826dfb701f611416d69b4df81caf9ff80b03a'; + })[0]; + }, + 'has a start datetime': function(topic) { + assert.equal(topic.start.toDateString(), new Date(Date.UTC(2011, 2, 12, 20, 0, 0)).toDateString()); + }, + 'datetype is date-time': function(topic) { + assert.equal(topic.datetype, 'date-time'); + }, + }, + + 'event sdfkf09fsd0 (Invalid Date)': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === 'sdfkf09fsd0'; + })[0]; + }, + 'has a start datetime': function(topic) { + assert.equal(topic.start, 'Next Year'); + }, + }, + }, + 'with test2.ics (testing ical features)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test2.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'todo item uid4@host1.com': { + 'topic': function(items) { + return _.filter(items, function(obj) { + { + return obj.uid == 'uid4@host1.com'; + } + })[0]; + }, + 'is a VTODO': function(topic) { + assert.equal(topic.type, 'VTODO'); + }, + }, + 'vfreebusy': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.type === 'VFREEBUSY'; + })[0]; + }, + 'has a URL': function(topic) { + assert.equal(topic.url, 'http://www.host.com/calendar/busytime/jsmith.ifb'); + }, + }, + 'vfreebusy first freebusy': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.type === 'VFREEBUSY'; + })[0].freebusy[0]; + }, + 'has undefined type defaulting to busy': function(topic) { + assert.equal(topic.type, 'BUSY'); + }, + 'has an start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 1998); + assert.equal(topic.start.getUTCMonth(), 2); + assert.equal(topic.start.getUTCDate(), 14); + assert.equal(topic.start.getUTCHours(), 23); + assert.equal(topic.start.getUTCMinutes(), 30); + }, + 'has an end datetime': function(topic) { + assert.equal(topic.end.getFullYear(), 1998); + assert.equal(topic.end.getUTCMonth(), 2); + assert.equal(topic.end.getUTCDate(), 15); + assert.equal(topic.end.getUTCHours(), 0); + assert.equal(topic.end.getUTCMinutes(), 30); + }, + }, + 'tzid parsing': { + 'topic': function(events) { + return _.filter(events, function(obj) { + { + return obj.uid == 'EC9439B1-FF65-11D6-9973-003065F99D04'; + } + })[0]; + }, + 'tzid offset correctly applied': function(event) { + const start = new Date('2002-10-28T22:00:00.000Z'); + assert.equal(event.start.valueOf(), start.valueOf()); + }, + }, + }, + 'with test3.ics (testing tvcountdown.com)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test3.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'event -83': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '20110505T220000Z-83@tvcountdown.com'; + })[0]; + }, + 'has a start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 2011); + assert.equal(topic.start.getMonth(), 4); + }, + 'has an end datetime': function(topic) { + assert.equal(topic.end.getFullYear(), 2011); + assert.equal(topic.end.getMonth(), 4); + }, + 'datetype is date-time': function(topic) { + assert.equal(topic.datetype, 'date-time'); + }, + }, + }, + + 'with test4.ics (testing tripit.com)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test4.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'event c32a5...': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === 'c32a5eaba2354bb29e012ec18da827db90550a3b@tripit.com'; + })[0]; + }, + 'has a start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 2011); + assert.equal(topic.start.getMonth(), 9); + assert.equal(topic.start.getDate(), 11); + }, + + 'has a summary': function(topic) { + // escaped commas and semicolons should be replaced + assert.equal(topic.summary, 'South San Francisco, CA, October 2011;'); + }, + + 'has a description': function(topic) { + const desired = + 'John Doe is in South San Francisco, CA from Oct 11 ' + + 'to Oct 13, 2011\nView and/or edit details in TripIt : http://www.tripit.c' + + 'om/trip/show/id/23710889\nTripIt - organize your travel at http://www.trip' + + 'it.com\n'; + assert.equal(topic.description, desired); + }, + + 'has a geolocation': function(topic) { + assert.ok(topic.geo, 'no geo param'); + assert.equal(topic.geo.lat, 37.654656); + assert.equal(topic.geo.lon, -122.40775); + }, + + 'has transparency': function(topic) { + assert.equal(topic.transparency, 'TRANSPARENT'); + }, + }, + }, + + 'with test5.ics (testing meetup.com)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test5.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'event nsmxnyppbfc@meetup.com': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === 'event_nsmxnyppbfc@meetup.com'; + })[0]; + }, + 'has a start': function(topic) { + assert.equal(topic.start.tz, 'America/Phoenix'); + assert.equal(topic.start.toISOString(), new Date(Date.UTC(2011, 10, 10, 2, 0, 0)).toISOString()); + }, + }, + }, + + 'with test6.ics (testing assembly.org)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test6.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'event with no ID': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.summary === 'foobar Summer 2011 starts!'; + })[0]; + }, + 'has a start': function(topic) { + assert.equal(topic.start.toISOString(), new Date(2011, 7, 4, 0, 0, 0).toISOString()); + }, + }, + 'event with rrule': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.summary == 'foobarTV broadcast starts'; + })[0]; + }, + 'Has an RRULE': function(topic) { + assert.notEqual(topic.rrule, undefined); + }, + 'RRule text': function(topic) { + assert.equal(topic.rrule.toText(), 'every 5 weeks on Monday, Friday until January 30, 2013'); + }, + }, + }, + 'with test7.ics (testing dtstart of rrule)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test7.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'recurring yearly event (14 july)': { + 'topic': function(events) { + const ev = _.values(events)[0]; + return ev.rrule.between(new Date(2013, 0, 1), new Date(2014, 0, 1)); + }, + 'dt start well set': function(topic) { + assert.equal(topic[0].toDateString(), new Date(2013, 6, 14).toDateString()); + }, + }, + }, + 'with test 8.ics (VTODO completion)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test8.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'grabbing VTODO task': { + 'topic': function(topic) { + return _.values(topic)[0]; + }, + 'task completed': function(task) { + assert.equal(task.completion, 100); + assert.equal(task.completed.toISOString(), new Date(2013, 6, 16, 10, 57, 45).toISOString()); + }, + }, + }, + 'with test 9.ics (VEVENT with VALARM)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test9.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'grabbing VEVENT task': { + 'topic': function(topic) { + return _.values(topic)[0]; + }, + 'task completed': function(task) { + assert.equal(task.summary, 'Event with an alarm'); + }, + }, + }, + 'with test 11.ics (VEVENT with custom properties)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test10.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'grabbing custom properties': { + topic(topic) {}, + }, + }, + + 'with test10.ics': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test10.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + + 'when categories present': { + 'topic': function(t) { + return _.values(t)[0]; + }, + + 'should be a list': function(e) { + assert(e.categories instanceof [].constructor); + }, + + 'should contain individual category values': function(e) { + assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']); + }, + }, + + 'when categories present with trailing whitespace': { + 'topic': function(t) { + return _.values(t)[1]; + }, + + 'should contain individual category values without whitespace': function(e) { + assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']); + }, + }, + + 'when categories present but empty': { + 'topic': function(t) { + return _.values(t)[2]; + }, + + 'should be an empty list': function(e) { + assert.deepEqual(e.categories, []); + }, + }, + + 'when categories present but singular': { + 'topic': function(t) { + return _.values(t)[3]; + }, + + 'should be a list of single item': function(e) { + assert.deepEqual(e.categories, ['lonely-cat']); + }, + }, + + 'when categories present on multiple lines': { + 'topic': function(t) { + return _.values(t)[4]; + }, + + 'should contain the category values in an array': function(e) { + assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']); + }, + }, + }, + + 'with test11.ics (testing zimbra freebusy)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test11.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + + 'freebusy params': { + 'topic': function(events) { + return _.values(events)[0]; + }, + 'has a URL': function(topic) { + assert.equal(topic.url, 'http://mail.example.com/yvr-2a@example.com/20140416'); + }, + 'has an ORGANIZER': function(topic) { + assert.equal(topic.organizer, 'mailto:yvr-2a@example.com'); + }, + 'has an start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 2014); + assert.equal(topic.start.getMonth(), 3); + }, + 'has an end datetime': function(topic) { + assert.equal(topic.end.getFullYear(), 2014); + assert.equal(topic.end.getMonth(), 6); + }, + }, + 'freebusy busy events': { + 'topic': function(events) { + return _.select(_.values(events)[0].freebusy, function(x) { + return x.type === 'BUSY'; + })[0]; + }, + 'has an start datetime': function(topic) { + assert.equal(topic.start.getFullYear(), 2014); + assert.equal(topic.start.getMonth(), 3); + assert.equal(topic.start.getUTCHours(), 15); + assert.equal(topic.start.getUTCMinutes(), 15); + }, + 'has an end datetime': function(topic) { + assert.equal(topic.end.getFullYear(), 2014); + assert.equal(topic.end.getMonth(), 3); + assert.equal(topic.end.getUTCHours(), 19); + assert.equal(topic.end.getUTCMinutes(), 0); + }, + }, + }, + + 'with test12.ics (testing recurrences and exdates)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test12.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'event with rrule': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '0000001'; + })[0]; + }, + 'Has an RRULE': function(topic) { + assert.notEqual(topic.rrule, undefined); + }, + 'Has summary Treasure Hunting': function(topic) { + assert.equal(topic.summary, 'Treasure Hunting'); + }, + 'Has two EXDATES': function(topic) { + assert.notEqual(topic.exdate, undefined); + assert.notEqual( + topic.exdate[new Date(Date.UTC(2015, 6, 8, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.notEqual( + topic.exdate[new Date(Date.UTC(2015, 6, 10, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + }, + 'Has a RECURRENCE-ID override': function(topic) { + assert.notEqual(topic.recurrences, undefined); + assert.notEqual( + topic.recurrences[new Date(Date.UTC(2015, 6, 7, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.equal( + topic.recurrences[new Date(Date.UTC(2015, 6, 7, 19, 0, 0)).toISOString().substring(0, 10)] + .summary, + 'More Treasure Hunting' + ); + }, + }, + }, + + 'with test13.ics (testing recurrence-id before rrule)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test13.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'event with rrule': { + 'topic': function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '6m2q7kb2l02798oagemrcgm6pk@google.com'; + })[0]; + }, + 'Has an RRULE': function(topic) { + assert.notEqual(topic.rrule, undefined); + }, + "Has summary 'repeated'": function(topic) { + assert.equal(topic.summary, 'repeated'); + }, + 'Has a RECURRENCE-ID override': function(topic) { + assert.notEqual(topic.recurrences, undefined); + assert.notEqual( + topic.recurrences[new Date(Date.UTC(2016, 7, 26, 11, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.equal( + topic.recurrences[new Date(Date.UTC(2016, 7, 26, 11, 0, 0)).toISOString().substring(0, 10)] + .summary, + 'bla bla' + ); + }, + }, + }, + + 'with test15.ics (testing Microsoft Exchange Server 2010 with timezones)' : { + topic: function () { + return ical.parseFile('./test/test15.ics') + }, + 'event with start and end including timezones' : { + topic: function(events) { + return _.select(_.values(events), function(x) { + return x.uid === '040000008200E00074C5B7101A82E00800000000C9AB6E5A6AFED401000000000000000010000000C55132227F0F0948A7D58F6190A3AEF9'; + })[0]; + }, + 'has a start' : function(topic){ + assert.equal(topic.start.tz, "(UTC+07:00) Bangkok, Hanoi, Jakarta") + assert.equal(topic.start.toISOString(), new Date(2019, 3, 30, 9, 0, 0).toISOString()) + assert.equal(topic.end.tz, "(UTC+07:00) Bangkok, Hanoi, Jakarta") + assert.equal(topic.end.toISOString(), new Date(2019, 3, 30, 12, 0, 0).toISOString()) + } + } + }, + + 'with test16.ics (testing quoted parameter values)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test16.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'quoted params': { + 'topic': function(events) { + return _.values(events)[0]; + }, + 'is quoted': function(topic) { + assert.notEqual(topic.start.tz, undefined); + }, + }, + }, + + 'with test17.ics (testing for non-stringified start/end time)': { + 'topic': function() { + const self = this; + ical.parseFile('./test/test17.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'stringified params': { + 'topic': function(events) { + return _.values(events)[0]; + }, + 'is not string': function(topic) { + assert.notEqual(typeof topic.start, 'string'); + assert.notEqual(typeof topic.end, 'string'); + }, + }, + }, + + 'url request errors': { + 'topic': function() { + ical.fromURL('http://255.255.255.255/', {}, this.callback); + }, + 'are passed back to the callback': function(err, result) { + assert.instanceOf(err, Error); + if (!err) { + console.log('>E:', err, result); + } + }, + }, + }) + .export(module);