From 03723bf2af169007b3f582f5c7a8e4908903f236 Mon Sep 17 00:00:00 2001 From: jjd314 Date: Thu, 22 May 2014 16:21:06 -0400 Subject: [PATCH 01/79] Update ical.js: handle quoted param values Although the original comment said nobody uses those, Google sent me one today (TZID="(UTC-05:00) America/New_York", whatever that might mean). Changed the split to a regexp. --- ical.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/ical.js b/ical.js index 54146de..7a062fc 100755 --- a/ical.js +++ b/ical.js @@ -249,20 +249,18 @@ i += 1 } - var kv = l.split(":") + var exp = /([^":;]+)((?:;(?:[^":;]+)(?:=(?:(?:"[^"]*")|(?:[^":;]+))))*):(.+)/; + var kv = l.match(exp); - if (kv.length < 2){ + if (kv === null) { // Invalid line - must have k&v continue; } + kv = kv.slice(1); - // 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) + var value = kv[kv.length - 1] + , name = kv[0] + , params = kv[1]?kv[1].split(';').slice(1):[] ctx = self.handleObject(name, value, params, ctx, stack, l) || {} } From f7c7dd2b46d957e9c52b11a87335d7e07c439f9c Mon Sep 17 00:00:00 2001 From: Julian Frumar Date: Mon, 4 Aug 2014 14:18:01 -0700 Subject: [PATCH 02/79] Support exdates by returning an array with the parsed object. --- .gitignore | 1 + ical.js | 113 ++++++++++++++++++++++++++++++++--------------------- 2 files changed, 69 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 3c3629e..eb79dd5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.idea diff --git a/ical.js b/ical.js index 54146de..e4226f3 100755 --- a/ical.js +++ b/ical.js @@ -51,71 +51,93 @@ return curr } - } + }; - var addTZ = function(dt, name, params){ + var addTZ = function(dateObj, params){ var p = parseParams(params); if (params && p){ - dt[name].tz = p.TZID + dateObj.tz = p.TZID } - return dt - } - + return dateObj + }; - var dateParam = function(name){ - return function(val, params, curr){ + /** + * Convert a date string from ICS format into a native Date object + * @param {string} val - The ICS string to be parsed + * @param {array} params + * @param {object} curr - The current Object that we're building + * @return {object} The Javascript date object + */ + function parseDate(val, params, curr) { + var objToReturn = {}; - // Store as string - worst case scenario - storeParam(name)(val, undefined, curr) + if (params && params[0] === "VALUE=DATE") { + // Just Date - if (params && params[0] === "VALUE=DATE") { - // Just Date - - var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); - if (comps !== null) { - // No TZ info - assume same timezone as this computer - curr[name] = new Date( - comps[1], + var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); + if (comps !== null) { + // No TZ info - assume same timezone as this computer + objToReturn = new Date( + comps[1], parseInt(comps[2], 10)-1, - comps[3] - ); + comps[3] + ); - return addTZ(curr, name, params); - } + return addTZ(objToReturn, params); } + } - //typical RFC date-time format - var 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 - curr[name] = new Date(Date.UTC( - parseInt(comps[1], 10), + //typical RFC date-time format + var 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 + return 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 { - curr[name] = new Date( - parseInt(comps[1], 10), + parseInt(comps[3], 10), + parseInt(comps[4], 10), + parseInt(comps[5], 10), + parseInt(comps[6], 10 ) + )); + // TODO add tz + } else { + return 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) - ); - } + parseInt(comps[3], 10), + parseInt(comps[4], 10), + parseInt(comps[5], 10), + parseInt(comps[6], 10) + ); } - - return addTZ(curr, name, params) } } + var dateParam = function(name){ + return function(val, params, curr){ + // Store as string - worst case scenario + storeParam(name)(val, undefined, curr); + var dateObj = parseDate(val, params, curr); + curr[name] = addTZ(dateObj, params); + return curr; + } + }; + + var dateParamArray = function(name) { + return function(date, params, curr) { + // initialize + curr[name] = curr[name] || []; + // load date + var dateObj = parseDate(date, params, curr); + dateObj = addTZ(dateObj, params); + curr[name].push(dateObj); + return curr; + } + + }; var geoParam = function(name){ return function(val, params, curr){ @@ -216,6 +238,7 @@ , 'COMPLETED': dateParam('completed') , 'CATEGORIES': categoriesParam('categories') , 'FREEBUSY': freebusyParam('freebusy') + , 'EXDATE': dateParamArray('exdate') }, From 277579b5061687facfa1aa6bfb9230cc2132b6c9 Mon Sep 17 00:00:00 2001 From: Julian Frumar Date: Mon, 4 Aug 2014 14:40:51 -0700 Subject: [PATCH 03/79] Only set timezone if we have a valid parsed date. --- ical.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ical.js b/ical.js index e4226f3..cbc1ba3 100755 --- a/ical.js +++ b/ical.js @@ -56,11 +56,11 @@ var addTZ = function(dateObj, params){ var p = parseParams(params); - if (params && p){ + if (params && p && dateObj){ dateObj.tz = p.TZID } - return dateObj + return dateObj; }; /** @@ -71,7 +71,7 @@ * @return {object} The Javascript date object */ function parseDate(val, params, curr) { - var objToReturn = {}; + var objToReturn = val; if (params && params[0] === "VALUE=DATE") { // Just Date @@ -118,10 +118,14 @@ var dateParam = function(name){ return function(val, params, curr){ - // Store as string - worst case scenario - storeParam(name)(val, undefined, curr); var dateObj = parseDate(val, params, curr); - curr[name] = addTZ(dateObj, params); + dateObj = addTZ(dateObj, params); + if (dateObj) { + curr[name] = dateObj; + } else { + // Store as string - worst case scenario + storeParam(name)(val, undefined, curr); + } return curr; } }; From 97cd72ecac514b82adee6c84fab5d5bb5efa9f8d Mon Sep 17 00:00:00 2001 From: Julian Frumar Date: Thu, 7 Aug 2014 13:17:30 -0700 Subject: [PATCH 04/79] Index VEVENTs by a guid instead of the external UID (which can isn't always unique). --- ical.js | 9 ++++----- package.json | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ical.js b/ical.js index cbc1ba3..edbdd91 100755 --- a/ical.js +++ b/ical.js @@ -1,3 +1,5 @@ +var UUID = require('node-uuid'); + (function(name, definition) { /**************** @@ -219,11 +221,7 @@ } var par = stack.pop() - - if (curr.uid) - par[curr.uid] = curr - else - par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID + par[UUID.v4()] = curr; return par } @@ -243,6 +241,7 @@ , 'CATEGORIES': categoriesParam('categories') , 'FREEBUSY': freebusyParam('freebusy') , 'EXDATE': dateParamArray('exdate') + , 'RECURRENCE-ID': storeParam('recurrenceId') }, diff --git a/package.json b/package.json index 8847145..428068e 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,9 @@ "url": "git://github.com/peterbraden/ical.js.git" }, "dependencies": { + "node-uuid": "^1.4.1", "request": "", - "rrule": "2.0.0" + "rrule": "2.1.0" }, "devDependencies": { "vows": "0.7.0", From 6470cca342e9fed358857bee19ac0690833e8aca Mon Sep 17 00:00:00 2001 From: Julian Frumar Date: Mon, 27 Oct 2014 12:31:54 -0700 Subject: [PATCH 05/79] Fix missing comma in package.json, and fix broken test. --- package.json | 2 +- test/test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fed6692..9b25838 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "node-uuid": "^1.4.1", - "rrule": "2.1.0" + "rrule": "2.1.0", "request": "2.40.0" }, "devDependencies": { diff --git a/test/test.js b/test/test.js index e7b5221..733c56d 100755 --- a/test/test.js +++ b/test/test.js @@ -72,7 +72,7 @@ vows.describe('node-ical').addBatch({ } , 'todo item uid4@host1.com' : { topic : function(items){ - return items['uid4@host1.com'] + return _.filter(items,function(obj) { { return obj.uid == 'uid4@host1.com'; } })[0]; } , 'is a VTODO' : function(topic){ assert.equal(topic.type, 'VTODO') From 2521065864f992cfe2ce15fc25d7d43d07f41260 Mon Sep 17 00:00:00 2001 From: Julian Frumar Date: Fri, 6 Feb 2015 10:28:47 -0800 Subject: [PATCH 06/79] Return a date object even when there's no time value --- ical.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ical.js b/ical.js index edbdd91..4cff50a 100755 --- a/ical.js +++ b/ical.js @@ -116,6 +116,16 @@ var UUID = require('node-uuid'); ); } } + + // date format (no time) + var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); + if (comps !== null) { + return new Date(Date.UTC( + parseInt(comps[1], 10), + parseInt(comps[2], 10)-1, + parseInt(comps[3], 10) + )); + } } var dateParam = function(name){ From 1aab4370725ee876d291efa4578f301d7d67fe20 Mon Sep 17 00:00:00 2001 From: Julian Frumar Date: Fri, 6 Feb 2015 12:01:11 -0800 Subject: [PATCH 07/79] Support importing dates that don't have a time component. --- ical.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ical.js b/ical.js index 4cff50a..51de6b1 100755 --- a/ical.js +++ b/ical.js @@ -118,13 +118,16 @@ var UUID = require('node-uuid'); } // date format (no time) - var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); + comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); if (comps !== null) { - return new Date(Date.UTC( - parseInt(comps[1], 10), + // No TZ info - assume same timezone as this computer + objToReturn = new Date( + comps[1], parseInt(comps[2], 10)-1, - parseInt(comps[3], 10) - )); + comps[3] + ); + + return addTZ(objToReturn, params); } } From c299840bc72da49237636e8c00ac4ee55f873263 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 14:44:57 +0100 Subject: [PATCH 08/79] =?UTF-8?q?change=D1=95=20to=20get=20stuff=20persona?= =?UTF-8?q?lized.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 25 ++++++++++++++++++++++++- NOTICE | 13 ------------- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 +++++++------ readme.md | 51 --------------------------------------------------- 5 files changed, 82 insertions(+), 71 deletions(-) delete mode 100644 NOTICE create mode 100644 README.md delete mode 100644 readme.md 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..d54afe9 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# 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) + +[![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 nodejs. This modul is a direct fork +of the ical.js module by Peter Braden (https://github.com/peterbraden/ical.js) which is primarily targeted +for allowing to parse icalender/ics files in pure javascript, thus within the browser itself. This node-ical +module, however, primarily targets nodejs use which should allow to parse icalendar/ics files in a more flexible +way. + +## Install +node-ical is availble on npm: + + npm install node-ical + +## API + + var ical = require('node-ical'); + ical.parseICS(str); + +Parses a string with an ICS File + + var data = ical.parseFile(filename); + +Reads in the specified iCal file, parses it and returns the parsed data + + ical.fromURL(url, options, function(err, 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) +```js + var ical = require('node-ical'); + var 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 (var k in data){ + if (data.hasOwnProperty(k)) { + var ev = data[k] + console.log("Conference", + ev.summary, + 'is in', + ev.location, + 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()]); + } + } + }); +``` diff --git a/package.json b/package.json index 3c893e2..3b11f4d 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { - "name": "ical", + "name": "node-ical", "version": "0.5.0", "main": "index.js", - "description": "A tolerant, minimal icalendar parser", + "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 ", "repository": { "type": "git", - "url": "git://github.com/peterbraden/ical.js.git" + "url": "git://github.com/jens-maus/node-ical.git" }, "dependencies": { "request": "2.75.0", diff --git a/readme.md b/readme.md deleted file mode 100644 index 2daba07..0000000 --- a/readme.md +++ /dev/null @@ -1,51 +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) - -Reads in the specified iCal file, parses it and returns the parsed data - - ical.fromURL(url, options, function(err, 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) -```javascript - var ical = require('ical') - , 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 (var k in data){ - if (data.hasOwnProperty(k)) { - var ev = data[k] - console.log("Conference", - ev.summary, - 'is in', - ev.location, - 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()]); - } - } - }); -``` From 8d9ed4284409d36d2f5396e8e01094ccf326c5ef Mon Sep 17 00:00:00 2001 From: Alex Kit Date: Thu, 6 Mar 2014 20:34:46 +0100 Subject: [PATCH 09/79] [++] parse same keys to an array / [fix] parseParams --- ical.js | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/ical.js b/ical.js index 7a062fc..478d50b 100755 --- a/ical.js +++ b/ical.js @@ -31,24 +31,50 @@ var out = {} for (var i = 0; i Date: Wed, 3 Sep 2014 08:57:52 +0800 Subject: [PATCH 10/79] See peterbraden#46 version to 0.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8847145..df9c31e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ical", - "version": "0.2.0", + "version": "0.3.0", "main": "index.js", "description": "A tolerant, minimal icalendar parser", "keywords": [ From caf6d6fab82e1bc0b7a174fcb7413dfe4286139e Mon Sep 17 00:00:00 2001 From: Peter Braden Date: Wed, 3 Sep 2014 21:14:28 +0200 Subject: [PATCH 11/79] fix tests --- package.json | 2 +- test/test.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index df9c31e..3ecdc28 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "url": "git://github.com/peterbraden/ical.js.git" }, "dependencies": { - "request": "", + "request": "2.40.0", "rrule": "2.0.0" }, "devDependencies": { diff --git a/test/test.js b/test/test.js index 7498e9d..e7b5221 100755 --- a/test/test.js +++ b/test/test.js @@ -3,6 +3,7 @@ * * ***/ +process.env.TZ = 'America/San_Francisco'; var ical = require('../index') var vows = require('vows') @@ -355,6 +356,9 @@ vows.describe('node-ical').addBatch({ } , 'are passed back to the callback' : function (err, result) { assert.instanceOf(err, Error); + if (!err){ + console.log(">E:", err, result) + } } } }).export(module) From 4e205f096a0c19e8e586a69b486942f64661d158 Mon Sep 17 00:00:00 2001 From: Peter Braden Date: Wed, 3 Sep 2014 21:15:38 +0200 Subject: [PATCH 12/79] bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ecdc28..285d215 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ical", - "version": "0.3.0", + "version": "0.3.1", "main": "index.js", "description": "A tolerant, minimal icalendar parser", "keywords": [ From 4e8769e4435259cc5a445c78f6c7df4bdb7f180b Mon Sep 17 00:00:00 2001 From: Peter Braden Date: Wed, 3 Sep 2014 21:28:43 +0200 Subject: [PATCH 13/79] travis node version --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2013351..92c7adc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: node_js node_js: - - 0.6 + - 0.10.26 install: npm install --dev From f7dae87d5cbfe94d962556d49a449796fc250336 Mon Sep 17 00:00:00 2001 From: Peter Braden Date: Wed, 3 Sep 2014 21:38:13 +0200 Subject: [PATCH 14/79] no dev --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 92c7adc..faa2206 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: node_js node_js: - 0.10.26 -install: npm install --dev +install: npm install From 23e2a2ffc33fd18725926f971e6da0e8f0b7f76f Mon Sep 17 00:00:00 2001 From: Jan Stamer Date: Sat, 13 Sep 2014 18:24:10 +0200 Subject: [PATCH 15/79] Fixed #45: RRule doesn't honor start date --- node-ical.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/node-ical.js b/node-ical.js index 9cfd92f..e6067f7 100644 --- a/node-ical.js +++ b/node-ical.js @@ -21,5 +21,12 @@ var rrule = require('rrule').RRule ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){ curr['rrule'] = rrule.fromString(line.replace("RRULE:", "")); + + // If rrule does not contain a start date + // read the start date from the current event + if (line.indexOf('DTSTART') != -1) { + curr['rrule'].options.dtstart = curr.start; + } + return curr } From 992ae19b062c6a1e52cd767d21185f8c532deaa0 Mon Sep 17 00:00:00 2001 From: Peter Braden Date: Thu, 16 Apr 2015 08:16:12 +0200 Subject: [PATCH 16/79] bump master --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 285d215..98d4968 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ical", - "version": "0.3.1", + "version": "0.4.0", "main": "index.js", "description": "A tolerant, minimal icalendar parser", "keywords": [ From ed309cf94120bb61ed610e6e02824f937c12f696 Mon Sep 17 00:00:00 2001 From: Andi Sidwell Date: Fri, 29 May 2015 01:31:58 +0100 Subject: [PATCH 17/79] Document API fully --- readme.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 27dbcfa..2daba07 100644 --- a/readme.md +++ b/readme.md @@ -21,10 +21,13 @@ ical.js is availble on npm: Parses a string with an ICS File + var data = ical.parseFile(filename) + +Reads in the specified iCal file, parses it and returns the parsed data + ical.fromURL(url, options, function(err, 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). From c939c894ec3d760cff0491dc4e67d7e4d99d304d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20Rodenburg?= Date: Thu, 4 Jun 2015 16:29:36 +0200 Subject: [PATCH 18/79] Fix undefined 'cur' variable' --- ical.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ical.js b/ical.js index 478d50b..8911569 100755 --- a/ical.js +++ b/ical.js @@ -66,7 +66,7 @@ var current = curr[name]; if (Array.isArray(current)){ current.push(data); - return cur; + return curr; } if (current != null){ From 1408a10dd15940056696a36fa2c5613842a353e2 Mon Sep 17 00:00:00 2001 From: Eric Lathrop Date: Thu, 2 Jul 2015 16:58:01 -0400 Subject: [PATCH 19/79] Specify dtstart for rrule when dtstart comes later Close #21. --- node-ical.js | 21 +++++++++++++-------- package.json | 2 +- test/test.js | 14 ++++++++++++++ test/test7.ics | 16 ++++++++++++++++ 4 files changed, 44 insertions(+), 9 deletions(-) create mode 100755 test/test7.ics diff --git a/node-ical.js b/node-ical.js index e6067f7..6ce0489 100644 --- a/node-ical.js +++ b/node-ical.js @@ -20,13 +20,18 @@ exports.parseFile = function(filename){ var rrule = require('rrule').RRule ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){ - curr['rrule'] = rrule.fromString(line.replace("RRULE:", "")); - - // If rrule does not contain a start date - // read the start date from the current event - if (line.indexOf('DTSTART') != -1) { - curr['rrule'].options.dtstart = curr.start; - } - + curr.rrule = line; return curr } +var originalEnd = ical.objectHandlers['END']; +ical.objectHandlers['END'] = function(val, params, curr, stack){ + if (curr.rrule) { + var rule = curr.rrule.replace('RRULE:', ''); + if (rule.indexOf('DTSTART') === -1) { + rule += ';DTSTART=' + curr.start.toISOString().replace(/[-:]/g, ''); + rule = rule.replace(/\.[0-9]{3}/, ''); + } + curr.rrule = rrule.fromString(rule); + } + return originalEnd.call(this, val, params, curr, stack); +} diff --git a/package.json b/package.json index 98d4968..915ae84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ical", - "version": "0.4.0", + "version": "0.4.1", "main": "index.js", "description": "A tolerant, minimal icalendar parser", "keywords": [ diff --git a/test/test.js b/test/test.js index e7b5221..3432372 100755 --- a/test/test.js +++ b/test/test.js @@ -224,6 +224,20 @@ vows.describe('node-ical').addBatch({ 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() { diff --git a/test/test7.ics b/test/test7.ics new file mode 100755 index 0000000..4379cd0 --- /dev/null +++ b/test/test7.ics @@ -0,0 +1,16 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar 0.6.3 +X-WR-CALNAME:Fête Nationale - Férié +BEGIN:VEVENT +CREATED:20090502T140513Z +DTSTAMP:20111106T124709Z +UID:FA9831E7-C238-4FEC-95E5-CD46BD466421 +SUMMARY:Fête Nationale - Férié +RRULE:FREQ=YEARLY +DTSTART;VALUE=DATE:20120714 +DTEND;VALUE=DATE:20120715 +TRANSP:OPAQUE +SEQUENCE:5 +END:VEVENT +END:VCALENDAR From ff5bf81568fc86527c0ac305508a0bb56f7c61d6 Mon Sep 17 00:00:00 2001 From: Nino D'Aversa Date: Wed, 9 Mar 2016 15:10:53 -0500 Subject: [PATCH 20/79] Preventing parsing from failing when not provided FREEBUSY dates eg. if we have the following in an ical ``` FREEBUSY;FBTYPE=FREE: ``` We ultimately end up with no start and end date values and it blows up trying to replace the strings on undefined --- ical.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ical.js b/ical.js index 8911569..0da5cca 100755 --- a/ical.js +++ b/ical.js @@ -19,6 +19,7 @@ // Unescape Text re RFC 4.3.11 var text = function(t){ + t = t || ""; return (t .replace(/\\\,/g, ',') .replace(/\\\;/g, ';') From 81f8ad8af3e2468e981f6bb582191b40efbd9088 Mon Sep 17 00:00:00 2001 From: Nicholas Hubbard Date: Wed, 13 Apr 2016 14:56:51 -0400 Subject: [PATCH 21/79] Fix Issue #58 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 915ae84..e14d816 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "url": "git://github.com/peterbraden/ical.js.git" }, "dependencies": { - "request": "2.40.0", + "request": "2.68.0", "rrule": "2.0.0" }, "devDependencies": { From 7ea22937e88fac5e0152bcbde03be5680435dd9c Mon Sep 17 00:00:00 2001 From: Nicholas Hubbard Date: Wed, 13 Apr 2016 15:01:42 -0400 Subject: [PATCH 22/79] Add multiple versions to test on Travis --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index faa2206..eaa9436 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ language: node_js node_js: - - 0.10.26 + - "0.10" + - "0.12" + - "4.2" + - "5.1" install: npm install From 16f3e505946b4148e6bf21dabcebc6c3149a3d0f Mon Sep 17 00:00:00 2001 From: Nicholas Hubbard Date: Wed, 13 Apr 2016 15:06:28 -0400 Subject: [PATCH 23/79] Update Travis to remove Node 5 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eaa9436..0a4c798 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,4 @@ node_js: - "0.10" - "0.12" - "4.2" - - "5.1" install: npm install From d7227cf4c5de232f8a51bab16fbcf2c023723dfb Mon Sep 17 00:00:00 2001 From: Peter Braden Date: Thu, 14 Apr 2016 08:40:41 +0200 Subject: [PATCH 24/79] 0.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e14d816..991b144 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ical", - "version": "0.4.1", + "version": "0.5.0", "main": "index.js", "description": "A tolerant, minimal icalendar parser", "keywords": [ From 464052abfaf25a818ae55462aa0ebb5d14b7097e Mon Sep 17 00:00:00 2001 From: Stephan Bulheller Date: Fri, 15 Apr 2016 12:48:50 +0200 Subject: [PATCH 25/79] Fix #56 for categories in multiple lines --- ical.js | 6 +++++- test/test.js | 8 ++++++++ test/test10.ics | 8 ++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ical.js b/ical.js index 0da5cca..ecab3d9 100755 --- a/ical.js +++ b/ical.js @@ -157,7 +157,11 @@ var separatorPattern = /\s*,\s*/g; return function (val, params, curr) { storeParam(val, params, curr) - curr[name] = val ? val.split(separatorPattern) : [] + if (curr[name] === undefined) + curr[name] = val ? val.split(separatorPattern) : [] + else + if (val) + curr[name] = curr[name].concat(val.split(separatorPattern)) return curr } } diff --git a/test/test.js b/test/test.js index 3432372..2aeff1e 100755 --- a/test/test.js +++ b/test/test.js @@ -316,6 +316,14 @@ vows.describe('node-ical').addBatch({ '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']); + } } }, diff --git a/test/test10.ics b/test/test10.ics index 844995e..40763b3 100644 --- a/test/test10.ics +++ b/test/test10.ics @@ -23,4 +23,12 @@ SUMMARY:Event with a category DESCRIPTION:Details for an event with a category CATEGORIES:lonely-cat END:VEVENT +BEGIN:VEVENT +UID:5 +SUMMARY:Event with a category +DESCRIPTION:Details for an event with a category +CATEGORIES:cat1 +CATEGORIES:cat2 +CATEGORIES:cat3 +END:VEVENT END:VCALENDAR From 0517290065ab053e6ed1d485b4aba886361f346d Mon Sep 17 00:00:00 2001 From: Michael Teeuw Date: Sat, 23 Apr 2016 17:25:43 +0200 Subject: [PATCH 26/79] Fix 8 digit start dates. --- node-ical.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/node-ical.js b/node-ical.js index 6ce0489..1d9d65d 100644 --- a/node-ical.js +++ b/node-ical.js @@ -28,6 +28,17 @@ ical.objectHandlers['END'] = function(val, params, curr, stack){ if (curr.rrule) { var rule = curr.rrule.replace('RRULE:', ''); if (rule.indexOf('DTSTART') === -1) { + + // In some cases, the curr.start is just an 8 digit string, like '20160423'. + // In that case we need to convert it to a date object. Otherwise, the next + // .toISOString() method will throw an exception. + 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], comps[3]); + } + } + rule += ';DTSTART=' + curr.start.toISOString().replace(/[-:]/g, ''); rule = rule.replace(/\.[0-9]{3}/, ''); } From 78a82b861f03fc5cb91a2fb2abd1e909fa9aa543 Mon Sep 17 00:00:00 2001 From: Michael Teeuw Date: Tue, 3 May 2016 11:43:44 +0200 Subject: [PATCH 27/79] Fix javascript month issue. --- node-ical.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node-ical.js b/node-ical.js index 1d9d65d..88bfabd 100644 --- a/node-ical.js +++ b/node-ical.js @@ -35,7 +35,9 @@ ical.objectHandlers['END'] = function(val, params, curr, stack){ 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], comps[3]); + // JavaScript counts months from 0 to 11. January is 0. December is 11. + // Therefor we need to substract 1 from the month. + curr.start = new Date (comps[1], comps[2] - 1, comps[3]); } } From 102a7555a1fe881e4d7c856935f9d75207494653 Mon Sep 17 00:00:00 2001 From: mbalfour Date: Sat, 16 Jul 2016 10:33:15 -0500 Subject: [PATCH 28/79] Fixes for Recurrence Rules Added better support for Recurrence Rules: - Updates to specific recurrences of events now get their own entries in a recurrences[] array, keyed by ISO date/time. (Previously, their records silently overwrote the main recurrence event) - Exceptions to recurrences now appear in an exdate[] array, keyed by ISO date/time. Also added "knowledge" of the dtstamp, created, and lastmodified fields so that they get stored as dates, not strings. This should solve issues #57 and #63. --- ical.js | 143 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 111 insertions(+), 32 deletions(-) diff --git a/ical.js b/ical.js index ecab3d9..9143d75 100755 --- a/ical.js +++ b/ical.js @@ -55,36 +55,42 @@ return val; } - var storeParam = function(name){ - return function(val, params, curr){ - var data; - if (params && params.length && !(params.length==1 && params[0]==='CHARSET=utf-8')){ - data = {params:parseParams(params), val:text(val)} + var storeValParam = function (name) { + return function (val, curr) { + var current = curr[name]; + if (Array.isArray(current)) { + current.push(val); + return curr; + } + + if (current != null) { + curr[name] = [current, val]; + return curr; + } + + curr[name] = val; + return curr } - else - data = text(val) + } - var current = curr[name]; - if (Array.isArray(current)){ - current.push(data); - return curr; - } + var storeParam = function (name) { + return function (val, params, curr) { + var data; + if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) { + data = { params: parseParams(params), val: text(val) } + } + else + data = text(val) - if (current != null){ - curr[name] = [current, data]; - return curr; + return storeValParam(name)(data, curr); } - - curr[name] = data; - return curr - } } - var addTZ = function(dt, name, params){ + var addTZ = function (dt, params) { var p = parseParams(params); if (params && p){ - dt[name].tz = p.TZID + dt.tz = p.TZID } return dt @@ -92,10 +98,10 @@ var dateParam = function(name){ - return function(val, params, curr){ + return function (val, params, curr) { + + var newDate = text(val); - // Store as string - worst case scenario - storeParam(name)(val, undefined, curr) if (params && params[0] === "VALUE=DATE") { // Just Date @@ -103,13 +109,16 @@ var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); if (comps !== null) { // No TZ info - assume same timezone as this computer - curr[name] = new Date( + newDate = new Date( comps[1], parseInt(comps[2], 10)-1, comps[3] ); - return addTZ(curr, name, params); + newDate = addTZ(newDate, params); + + // Store as string - worst case scenario + return storeValParam(name)(newDate, curr) } } @@ -118,7 +127,7 @@ var 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 - curr[name] = new Date(Date.UTC( + newDate = new Date(Date.UTC( parseInt(comps[1], 10), parseInt(comps[2], 10)-1, parseInt(comps[3], 10), @@ -128,7 +137,7 @@ )); // TODO add tz } else { - curr[name] = new Date( + newDate = new Date( parseInt(comps[1], 10), parseInt(comps[2], 10)-1, parseInt(comps[3], 10), @@ -137,10 +146,14 @@ parseInt(comps[6], 10) ); } - } - return addTZ(curr, name, params) + newDate = addTZ(newDate, params); } + + + // Store as string - worst case scenario + return storeValParam(name)(newDate, curr) + } } @@ -166,7 +179,27 @@ } } - var addFBType = function(fb, params){ + // EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4"). + // There can be more than one of these in a calendar record, so 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. + var exdateParam = function (name) { + return function (val, params, curr) { + var exdate = new Array(); + dateParam(name)(val, params, exdate); + curr[name] = curr[name] || []; + curr[name][exdate[name].toISOString()] = 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. + var recurrenceParam = function (name) { + return dateParam(name); + } + + var addFBType = function (fb, params) { var p = parseParams(params); if (params && p){ @@ -226,7 +259,47 @@ var par = stack.pop() if (curr.uid) - par[curr.uid] = curr + { + // 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 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. + + var parent = par[curr.uid]; + if (curr.recurrenceid != null) { + if (parent.recurrences === undefined) { + parent.recurrences = new Array(); + } + + // TODO: Is there ever a case where we have to worry about overwriting an existing entry here? + + parent.recurrences[curr.recurrenceid.toISOString()] = curr; + } + else + { + // 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[key] = curr[key]; + } + } + } + } else par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID @@ -247,6 +320,12 @@ , 'COMPLETED': dateParam('completed') , 'CATEGORIES': categoriesParam('categories') , 'FREEBUSY': freebusyParam('freebusy') + , 'DTSTAMP': dateParam('dtstamp') + , 'EXDATE': exdateParam('exdate') + , 'CREATED': dateParam('created') + , 'LAST-MODIFIED': dateParam('lastmodified') + , 'RECURRENCE-ID': recurrenceParam('recurrenceid') + }, From 6cb0d0383b9c1d0a996d60a1ef81125adf450f02 Mon Sep 17 00:00:00 2001 From: mbalfour Date: Mon, 18 Jul 2016 22:01:46 -0500 Subject: [PATCH 29/79] Added tests and maintained old code Code maintenance problems: * Example.js should be pointing to node-ical, not ical now. * ical should depend on rrule 2.1.0 or greater due to dependency issues with underscore Added tests: * Verify that a recurrence-id doesn't overwrite the main entry * Verify that exdates appear in the exdate array * Verify that recurrence-id entries appear in the recurrences array * Verify that the arrays are keyed by ISO date strings --- example.js | 2 +- package.json | 2 +- test/test.js | 35 ++++++++++++++++++++++++++++++++--- test/test12.ics | 19 +++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 test/test12.ics diff --git a/example.js b/example.js index aceb0e7..c93b290 100644 --- a/example.js +++ b/example.js @@ -1,4 +1,4 @@ -var ical = require('ical') +var ical = require('./node-ical') , months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] diff --git a/package.json b/package.json index 991b144..c7f246c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "request": "2.68.0", - "rrule": "2.0.0" + "rrule": "2.1.0" }, "devDependencies": { "vows": "0.7.0", diff --git a/test/test.js b/test/test.js index 2aeff1e..9b67f9f 100755 --- a/test/test.js +++ b/test/test.js @@ -197,7 +197,7 @@ vows.describe('node-ical').addBatch({ } } - , 'with test6.ics (testing assembly.org)' : { + , 'with test6.ics (testing assembly.org)': { topic: function () { return ical.parseFile('./test/test6.ics') } @@ -370,9 +370,38 @@ vows.describe('node-ical').addBatch({ 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()], undefined); + assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString()], 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()], undefined); + assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString()].summary, 'More Treasure Hunting'); + } + } + } - 'url request errors' : { + , 'url request errors' : { topic : function () { ical.fromURL('http://not.exist/', {}, this.callback); } diff --git a/test/test12.ics b/test/test12.ics new file mode 100644 index 0000000..4e78603 --- /dev/null +++ b/test/test12.ics @@ -0,0 +1,19 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:0000001 +SUMMARY:Treasure Hunting +DTSTART;TZID=America/Los_Angeles: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 +BEGIN:VEVENT +UID:0000001 +SUMMARY:More Treasure Hunting +LOCATION:The other island +DTSTART;TZID=America/Los_Angeles:20150709T150000 +DTEND;TZID=America/Los_Angeles:20150707T160000 +RECURRENCE-ID;TZID=America/Los_Angeles:20150707T120000 +END:VEVENT +END:VCALENDAR From 3b61793abad3a429fb72f344c403179e1caf0d68 Mon Sep 17 00:00:00 2001 From: mbalfour Date: Sat, 17 Sep 2016 15:01:23 -0500 Subject: [PATCH 30/79] Fix RECURRENCE-ID appearing before RRULE Some ical files have a specific RECURRENCE-ID override entry that appears before the more general RRULE parent entry for the same UID. There were two bugs related to this: 1) If multiple entries for the same UID appear, the later UID entries were supposed to overwrite fields from the previous UID entries. This wasn't working correctly, so fields weren't getting overwritten. In this case, it meant that the RECURRENCE-ID parent entry wasn't getting the RRULE added to it. 2) The correct behaviour (as far as I can tell) should be for the parent record to look the same, regardless of which entry comes first, and the RECURRENCE-ID entry should end up in the recurrences array. This is now handled by letting the RECURRENCE-ID entry be the parent *and* an entry in the array, but then letting the RRULE example overwrite the fields in the parent as appropriate. Added a test to demonstrate the case and the correct processing of it. --- ical.js | 73 +++++++++++++++++++++++++++++++++++-------------- node-ical.js | 42 ++++++++++++++-------------- test/test.js | 26 +++++++++++++++++- test/test13.ics | 57 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 42 deletions(-) create mode 100644 test/test13.ics diff --git a/ical.js b/ical.js index 9143d75..5bc06fc 100755 --- a/ical.js +++ b/ical.js @@ -260,10 +260,10 @@ if (curr.uid) { - // If this is the first time we run into this UID, just save it. - if (par[curr.uid] === undefined) + // If this is the first time we run into this UID, just save it. + if (par[curr.uid] === undefined) { - par[curr.uid] = curr + par[curr.uid] = curr; } else { @@ -273,32 +273,65 @@ // TODO: Look into proper sequence logic. - // 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. - - var parent = par[curr.uid]; - if (curr.recurrenceid != null) { - if (parent.recurrences === undefined) { - parent.recurrences = new Array(); - } - - // TODO: Is there ever a case where we have to worry about overwriting an existing entry here? - - parent.recurrences[curr.recurrenceid.toISOString()] = curr; - } - else + 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[key] = curr[key]; + 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. + par[curr.uid].recurrences[curr.recurrenceid.toISOString()] = recurrenceObj; + } + + // 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[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID diff --git a/node-ical.js b/node-ical.js index 88bfabd..77164d1 100644 --- a/node-ical.js +++ b/node-ical.js @@ -24,27 +24,27 @@ ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){ return curr } var originalEnd = ical.objectHandlers['END']; -ical.objectHandlers['END'] = function(val, params, curr, stack){ - if (curr.rrule) { - var rule = curr.rrule.replace('RRULE:', ''); - if (rule.indexOf('DTSTART') === -1) { - - // In some cases, the curr.start is just an 8 digit string, like '20160423'. - // In that case we need to convert it to a date object. Otherwise, the next - // .toISOString() method will throw an exception. - if (curr.start.length === 8) { - var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start); - if (comps) { - // JavaScript counts months from 0 to 11. January is 0. December is 11. - // Therefor we need to substract 1 from the month. - curr.start = new Date (comps[1], comps[2] - 1, comps[3]); - } - } +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) { - rule += ';DTSTART=' + curr.start.toISOString().replace(/[-:]/g, ''); - rule = rule.replace(/\.[0-9]{3}/, ''); - } - curr.rrule = rrule.fromString(rule); - } + 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]); + } + } + + rule += ';DTSTART=' + curr.start.toISOString().replace(/[-:]/g, ''); + rule = rule.replace(/\.[0-9]{3}/, ''); + } + curr.rrule = rrule.fromString(rule); + } + } return originalEnd.call(this, val, params, curr, stack); } diff --git a/test/test.js b/test/test.js index 9b67f9f..35540da 100755 --- a/test/test.js +++ b/test/test.js @@ -401,7 +401,31 @@ vows.describe('node-ical').addBatch({ } } - , 'url request errors' : { + , '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-08-26T19:00:00Z').toISOString()], undefined); + assert.equal(topic.recurrences[new Date('2016-08-26T19:00:00Z').toISOString()].summary, 'bla bla'); + } + } + } + + , 'url request errors': { topic : function () { ical.fromURL('http://not.exist/', {}, this.callback); } diff --git a/test/test13.ics b/test/test13.ics new file mode 100644 index 0000000..3118186 --- /dev/null +++ b/test/test13.ics @@ -0,0 +1,57 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:ical +X-WR-TIMEZONE:Europe/Kiev +X-WR-CALDESC: +BEGIN:VTIMEZONE +TZID:Europe/Kiev +X-LIC-LOCATION:Europe/Kiev +BEGIN:DAYLIGHT +TZOFFSETFROM:+0200 +TZOFFSETTO:+0300 +TZNAME:EEST +DTSTART:19700329T030000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0300 +TZOFFSETTO:+0200 +TZNAME:EET +DTSTART:19701025T040000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Kiev:20160826T140000 +DTEND;TZID=Europe/Kiev:20160826T150000 +DTSTAMP:20160825T061505Z +UID:6m2q7kb2l02798oagemrcgm6pk@google.com +RECURRENCE-ID;TZID=Europe/Kiev:20160826T140000 +CREATED:20160823T125221Z +DESCRIPTION: +LAST-MODIFIED:20160823T130320Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:bla bla +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Kiev:20160825T140000 +DTEND;TZID=Europe/Kiev:20160825T150000 +RRULE:FREQ=DAILY;UNTIL=20160828T110000Z +DTSTAMP:20160825T061505Z +UID:6m2q7kb2l02798oagemrcgm6pk@google.com +CREATED:20160823T125221Z +DESCRIPTION: +LAST-MODIFIED:20160823T125221Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:repeated +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR \ No newline at end of file From 6b869dfabf74f73fcdb6cf76d112393f95ced999 Mon Sep 17 00:00:00 2001 From: mbalfour Date: Sat, 17 Sep 2016 15:09:21 -0500 Subject: [PATCH 31/79] Changed test to use a static IP address Some ISPs will return a valid web page for DNS entries that don't exist. By using an invalid IP address, we can ensure the test fails correctly in those situations. --- test/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 35540da..a9ede39 100755 --- a/test/test.js +++ b/test/test.js @@ -427,7 +427,7 @@ vows.describe('node-ical').addBatch({ , 'url request errors': { topic : function () { - ical.fromURL('http://not.exist/', {}, this.callback); + ical.fromURL('http://255.255.255.255/', {}, this.callback); } , 'are passed back to the callback' : function (err, result) { assert.instanceOf(err, Error); From 0dd5b49150ed866188de6218907955ffd6c26bbe Mon Sep 17 00:00:00 2001 From: mbalfour Date: Sat, 17 Sep 2016 15:32:22 -0500 Subject: [PATCH 32/79] Fixed the dates in the test Fixed the dates in the newly-added test --- test/test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test.js b/test/test.js index a9ede39..5ca55b7 100755 --- a/test/test.js +++ b/test/test.js @@ -419,8 +419,8 @@ vows.describe('node-ical').addBatch({ } , "Has a RECURRENCE-ID override": function (topic) { assert.notEqual(topic.recurrences, undefined); - assert.notEqual(topic.recurrences[new Date('2016-08-26T19:00:00Z').toISOString()], undefined); - assert.equal(topic.recurrences[new Date('2016-08-26T19:00:00Z').toISOString()].summary, 'bla bla'); + assert.notEqual(topic.recurrences[new Date(2016, 7 ,26, 14, 0, 0).toISOString()], undefined); + assert.equal(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString()].summary, 'bla bla'); } } } From 2d88b058c28893163f4876c34211787248d7eb5b Mon Sep 17 00:00:00 2001 From: Jacob Karlsson Date: Fri, 21 Oct 2016 13:22:49 +0200 Subject: [PATCH 33/79] Update request to 2.75.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7f246c..3c893e2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "url": "git://github.com/peterbraden/ical.js.git" }, "dependencies": { - "request": "2.68.0", + "request": "2.75.0", "rrule": "2.1.0" }, "devDependencies": { From 37e119f0b19573741e9e9e328264a9d161b907e3 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 14:44:57 +0100 Subject: [PATCH 34/79] =?UTF-8?q?change=D1=95=20to=20get=20stuff=20persona?= =?UTF-8?q?lized.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 25 ++++++++++++++++++++++++- NOTICE | 13 ------------- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 +++++++------ readme.md | 51 --------------------------------------------------- 5 files changed, 82 insertions(+), 71 deletions(-) delete mode 100644 NOTICE create mode 100644 README.md delete mode 100644 readme.md 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..d54afe9 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# 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) + +[![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 nodejs. This modul is a direct fork +of the ical.js module by Peter Braden (https://github.com/peterbraden/ical.js) which is primarily targeted +for allowing to parse icalender/ics files in pure javascript, thus within the browser itself. This node-ical +module, however, primarily targets nodejs use which should allow to parse icalendar/ics files in a more flexible +way. + +## Install +node-ical is availble on npm: + + npm install node-ical + +## API + + var ical = require('node-ical'); + ical.parseICS(str); + +Parses a string with an ICS File + + var data = ical.parseFile(filename); + +Reads in the specified iCal file, parses it and returns the parsed data + + ical.fromURL(url, options, function(err, 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) +```js + var ical = require('node-ical'); + var 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 (var k in data){ + if (data.hasOwnProperty(k)) { + var ev = data[k] + console.log("Conference", + ev.summary, + 'is in', + ev.location, + 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()]); + } + } + }); +``` diff --git a/package.json b/package.json index 3c893e2..3b11f4d 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { - "name": "ical", + "name": "node-ical", "version": "0.5.0", "main": "index.js", - "description": "A tolerant, minimal icalendar parser", + "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 ", "repository": { "type": "git", - "url": "git://github.com/peterbraden/ical.js.git" + "url": "git://github.com/jens-maus/node-ical.git" }, "dependencies": { "request": "2.75.0", diff --git a/readme.md b/readme.md deleted file mode 100644 index 2daba07..0000000 --- a/readme.md +++ /dev/null @@ -1,51 +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) - -Reads in the specified iCal file, parses it and returns the parsed data - - ical.fromURL(url, options, function(err, 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) -```javascript - var ical = require('ical') - , 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 (var k in data){ - if (data.hasOwnProperty(k)) { - var ev = data[k] - console.log("Conference", - ev.summary, - 'is in', - ev.location, - 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()]); - } - } - }); -``` From c49e02ce629170135a4fa839e2c807788c871a7f Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 15:47:12 +0100 Subject: [PATCH 35/79] applied fix solving test problem with empty categories. --- ical.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ical.js b/ical.js index 5bc06fc..9079079 100755 --- a/ical.js +++ b/ical.js @@ -392,7 +392,7 @@ i += 1 } - var exp = /([^":;]+)((?:;(?:[^":;]+)(?:=(?:(?:"[^"]*")|(?:[^":;]+))))*):(.+)/; + var exp = /([^":;]+)((?:;(?:[^":;]+)(?:=(?:(?:"[^"]*")|(?:[^":;]+))))*):(.*)/; var kv = l.match(exp); if (kv === null) { From 4ab13e3dd87ecfae5ab5522d418c35e0d8f52543 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 16:00:22 +0100 Subject: [PATCH 36/79] added test case for quoted parameters --- test/test.js | 14 ++++++++++++++ test/test14.ics | 11 +++++++++++ 2 files changed, 25 insertions(+) create mode 100644 test/test14.ics diff --git a/test/test.js b/test/test.js index 5ca55b7..c3df3fc 100755 --- a/test/test.js +++ b/test/test.js @@ -425,6 +425,20 @@ vows.describe('node-ical').addBatch({ } } + , 'with test14.ics (testing quoted parameter values)': { + topic: function () { + return ical.parseFile('./test/test14.ics') + } + , 'quoted params': { + topic: function (events) { + return _.values(events)[0]; + } + , "is quoted": function (topic) { + assert.notEqual(topic.start.tz, undefined); + } + } + } + , 'url request errors': { topic : function () { ical.fromURL('http://255.255.255.255/', {}, this.callback); diff --git a/test/test14.ics b/test/test14.ics new file mode 100644 index 0000000..a9efbf2 --- /dev/null +++ b/test/test14.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 From 0eab7a3ab719c7a74ec2188f6715938b3b8b1bc5 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 16:07:34 +0100 Subject: [PATCH 37/79] revision bump to honour the integration of the first PR. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b11f4d..954c901 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.5.0", + "version": "0.5.1", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ From 0db92742de0934c968b9198833ca60d0e92dbdf3 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 17:27:08 +0100 Subject: [PATCH 38/79] Update README.md --- README.md | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d54afe9..379b8a5 100644 --- a/README.md +++ b/README.md @@ -17,35 +17,37 @@ node-ical is availble on npm: npm install node-ical ## API - - var ical = require('node-ical'); - ical.parseICS(str); - +```js + var ical = require('node-ical'); +  ical.parseICS(str); +``` Parses a string with an ICS File - var data = ical.parseFile(filename); - +```js +  var data = ical.parseFile(filename); +```     Reads in the specified iCal file, parses it and returns the parsed data - - ical.fromURL(url, options, function(err, data) {} ); +```js +  ical.fromURL(url, options, function(err, 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) ```js - var ical = require('node-ical'); - var 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 (var k in data){ - if (data.hasOwnProperty(k)) { - var ev = data[k] - console.log("Conference", - ev.summary, - 'is in', - ev.location, - 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()]); - } + var ical = require('node-ical'); + var 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 (var k in data){ + if (data.hasOwnProperty(k)) { + var ev = data[k] + console.log("Conference", + ev.summary, + 'is in', + ev.location, + 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()]); } - }); + } + }); ``` From 77effa2a8c7d9392ea707cc016aa558d818ab3cd Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 17:28:53 +0100 Subject: [PATCH 39/79] Update README.md --- README.md | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 379b8a5..e97218d 100644 --- a/README.md +++ b/README.md @@ -18,36 +18,37 @@ node-ical is availble on npm: ## API ```js - var ical = require('node-ical'); -  ical.parseICS(str); +var ical = require('node-ical'); +ical.parseICS(str); ``` -Parses a string with an ICS File +Parses a string with an ICS File ```js -  var data = ical.parseFile(filename); -```     +var data = ical.parseFile(filename); +``` + Reads in the specified iCal file, parses it and returns the parsed data ```js -  ical.fromURL(url, options, function(err, data) {} ); +ical.fromURL(url, options, function(err, 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) ```js - var ical = require('node-ical'); - var 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 (var k in data){ - if (data.hasOwnProperty(k)) { - var ev = data[k] - console.log("Conference", - ev.summary, - 'is in', - ev.location, - 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()]); - } +var ical = require('node-ical'); +var 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 (var k in data){ + if (data.hasOwnProperty(k)) { + var ev = data[k] + console.log("Conference", + ev.summary, + 'is in', + ev.location, + 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()]); } - }); + } +}); ``` From 3d074c6e28561a3db400d3d6afc34124d5812cca Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 19:49:07 +0100 Subject: [PATCH 40/79] lets add TZID before VALUE=DATE because that can also happen here. --- test/test7.ics | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7c34ea0116346d67da976c3b9489b4149eff7c99 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 19:50:22 +0100 Subject: [PATCH 41/79] revision bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 225dd02..70706e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.5.1", + "version": "0.5.2", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ From 2de23595a4442e7975376284b9fd0a28909870d0 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 20:36:21 +0100 Subject: [PATCH 42/79] added fix where ev.start/ev.end could be returned as strings rather than date objects. --- ical.js | 3 +-- test/test.js | 15 +++++++++++++++ test/test15.ics | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 test/test15.ics diff --git a/ical.js b/ical.js index 70d34a6..415a591 100755 --- a/ical.js +++ b/ical.js @@ -104,8 +104,7 @@ var UUID = require('node-uuid'); var newDate = text(val); - - if (params && params[0] === "VALUE=DATE") { + if (params && params.indexOf('VALUE=DATE') > -1) { // Just Date var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); diff --git a/test/test.js b/test/test.js index 26bfefa..f309b81 100755 --- a/test/test.js +++ b/test/test.js @@ -439,6 +439,21 @@ vows.describe('node-ical').addBatch({ } } + , 'with test15.ics (testing for non-stringified start/end time)': { + topic: function () { + return ical.parseFile('./test/test15.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); diff --git a/test/test15.ics b/test/test15.ics new file mode 100644 index 0000000..817a2f1 --- /dev/null +++ b/test/test15.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 From d88830b07fc1b4027bad69f65d97e91335cab600 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 20:37:34 +0100 Subject: [PATCH 43/79] revision bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70706e1..986329e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.5.2", + "version": "0.5.3", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ From df35fffd53c037920d83b21460b02f54820bde84 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 20:45:39 +0100 Subject: [PATCH 44/79] added .npmignore --- .npmignore | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..5b3d508 --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +Gruntfile.js +tasks +test +test.js +.travis.yml +.idea +.git +.gitignore +node_modules +appveyor.yml From 73f9f103f17c4c0b9b9d103bf2ccc8ff04036e7d Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 20:47:09 +0100 Subject: [PATCH 45/79] revision bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 986329e..e060cac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.5.3", + "version": "0.5.4", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ From bf038501dae979a38afa8f82ec0abf82dc015dda Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 17 Feb 2017 20:53:18 +0100 Subject: [PATCH 46/79] added example.js --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index 5b3d508..c8c23a3 100644 --- a/.npmignore +++ b/.npmignore @@ -2,6 +2,7 @@ Gruntfile.js tasks test test.js +example.js .travis.yml .idea .git From d1e8f1e76d386146dcf7e7369e382c329de5e2de Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Fri, 13 Apr 2018 08:05:33 +0200 Subject: [PATCH 47/79] Add asynchronous processing of OCS data, Update travis-ci, dependencies - update travis-ci testing up to nodejs 8 - update dependencies for testing - add (backwardcompatible) option to process the ICS file asynchronous to prevent blocking the eventloop on very big files --- .travis.yml | 8 +- README.md | 29 ++- ical.js | 79 +++++-- node-ical.js | 8 +- package.json | 8 +- test/testAsync.js | 524 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 619 insertions(+), 37 deletions(-) create mode 100644 test/testAsync.js diff --git a/.travis.yml b/.travis.yml index 0a4c798..d29f34a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "0.10" - - "0.12" - - "4.2" -install: npm install + - "4" + - "6" + - "8" +install: npm install diff --git a/README.md b/README.md index e97218d..9876532 100644 --- a/README.md +++ b/README.md @@ -17,24 +17,45 @@ node-ical is availble on npm: npm install node-ical ## API +Parses a string with ICS content synchronous. This can block the nodejs event loop on big files ```js var ical = require('node-ical'); ical.parseICS(str); ``` -Parses a string with an ICS File +Parses a string with ICS content asynchronous to prevent event loop from being blocked. +```js +var 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 synchronous. This can block the nodejs event loop on big files ```js var data = ical.parseFile(filename); ``` -Reads in the specified iCal file, parses it and returns the parsed data +Parses a string with an ICS File asynchronous to prevent event loop from being blocked. ```js -ical.fromURL(url, options, function(err, data) {} ); +var data = ical.parseFile(filename, function(err, data) { + if (err) console.log(err); + console.log(data); +}); +``` + +Reads in the specified iCal file, parses it and returns the parsed data. This method always work asynchronous +```js +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) +## Example 1 - Print list of upcoming node conferences (see example.js) (parses the file synchronous) ```js var ical = require('node-ical'); var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; diff --git a/ical.js b/ical.js index 415a591..9618ceb 100755 --- a/ical.js +++ b/ical.js @@ -35,9 +35,9 @@ var UUID = require('node-uuid'); for (var i = 0; i limit) { + break; + } } - // type and params are added to the list of items, get rid of them. - delete ctx.type - delete ctx.params + if (!lines.length) { + // type and params are added to the list of items, get rid of them. + delete ctx.type; + delete ctx.params; + } - return ctx + if (cb) { + if (lines.length) { + setImmediate(function() { + self.parseLines(lines, limit, ctx, stack, cb); + }); + } + else { + setImmediate(function() { + cb(null, ctx); + }); + } + } + else { + return ctx + } + + }, + + parseICS : function(str,cb){ + var self = this + var lines = str.split(/\r?\n/) + var ctx; + + if (cb) { // asynchronous execution + self.parseLines(lines, 100, cb); + } + else { // synchronous execution + ctx = self.parseLines(lines, lines.length); + return ctx; + } } } diff --git a/node-ical.js b/node-ical.js index 77164d1..ad3c8e0 100644 --- a/node-ical.js +++ b/node-ical.js @@ -8,12 +8,12 @@ exports.fromURL = function(url, opts, cb){ request(url, opts, function(err, r, data){ if (err) return cb(err, null); - cb(undefined, ical.parseICS(data)); + ical.parseICS(data, cb); }) } -exports.parseFile = function(filename){ - return ical.parseICS(fs.readFileSync(filename, 'utf8')) +exports.parseFile = function(filename, cb){ + return ical.parseICS(fs.readFileSync(filename, 'utf8'), cb) } @@ -26,7 +26,7 @@ ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){ 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 + // 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) { diff --git a/package.json b/package.json index e060cac..761d765 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.5.4", + "version": "0.6.0", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ @@ -16,15 +16,15 @@ "url": "git://github.com/jens-maus/node-ical.git" }, "dependencies": { - "request": "2.75.0", + "request": "^2.81.0", "rrule": "2.1.0", "node-uuid": "^1.4.1" }, "devDependencies": { - "vows": "0.7.0", + "vows": "0.8.1", "underscore": "1.3.0" }, "scripts": { - "test": "./node_modules/vows/bin/vows ./test/test.js" + "test": "./node_modules/vows/bin/vows ./test/test.js && ./node_modules/vows/bin/vows ./test/testAsync.js" } } diff --git a/test/testAsync.js b/test/testAsync.js new file mode 100644 index 0000000..78a2a56 --- /dev/null +++ b/test/testAsync.js @@ -0,0 +1,524 @@ +/**** + * Tests + * + * + ***/ +process.env.TZ = 'America/San_Francisco'; +var ical = require('../index') + +var vows = require('vows') + , assert = require('assert') + , _ = require('underscore') + +console.log('START Async Tests'); +vows.describe('node-ical').addBatch({ + 'when parsing test1.ics (node conferences schedule from lanyrd.com, modified)': { + topic: function () { + var self = this; + ical.parseFile('./test/test1.ics', function(err, ctx) { + self.callback(null, ctx); + }); + } + + ,'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') + } + } + , '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 () { + var 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(), 00); + assert.equal(topic.end.getUTCMinutes(), 30); + } + } + } + , 'with test3.ics (testing tvcountdown.com)' : { + topic: function() { + var 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); + } + } + } + + , 'with test4.ics (testing tripit.com)' : { + topic: function() { + var 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(), 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 () { + var 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(2011, 10, 09, 19, 0,0).toISOString()) + } + } + } + + , 'with test6.ics (testing assembly.org)': { + topic: function () { + var 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, 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() { + var self = this; + ical.parseFile('./test/test7.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + '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() { + var 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, 06, 16, 10, 57, 45).toISOString()); + } + } + } + , "with test 9.ics (VEVENT with VALARM)": { + topic: function() { + var 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() { + var self = this; + ical.parseFile('./test/test10.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'grabbing custom properties': { + topic: function(topic) { + + } + } + }, + + 'with test10.ics': { + topic: function () { + var 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 () { + var 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(), 00); + } + } + } + + , 'with test12.ics (testing recurrences and exdates)': { + topic: function () { + var 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(2015, 06, 08, 12, 0, 0).toISOString()], undefined); + assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString()], 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()], undefined); + assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString()].summary, 'More Treasure Hunting'); + } + } + } + + , 'with test13.ics (testing recurrence-id before rrule)': { + topic: function () { + var 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(2016, 7 ,26, 14, 0, 0).toISOString()], undefined); + assert.equal(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString()].summary, 'bla bla'); + } + } + } + + , 'with test14.ics (testing quoted parameter values)': { + topic: function () { + var self = this; + ical.parseFile('./test/test14.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 test15.ics (testing for non-stringified start/end time)': { + topic: function () { + var self = this; + ical.parseFile('./test/test15.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) + + +//ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', +// {}, +// function(err, data){ +// console.log("OUT:", data) +// }) From b6382d466c8aaac30d4dc30a94de4c8e3fb4a7d8 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Sat, 14 Apr 2018 22:47:04 +0200 Subject: [PATCH 48/79] 0.6.1 with performance optimizations (#2) Last changes introduced bad performance, this is corrected now. So especially big files are handled 5 times faster --- ical.js | 20 +++++++++++--------- package.json | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/ical.js b/ical.js index 9618ceb..75e5c43 100755 --- a/ical.js +++ b/ical.js @@ -378,7 +378,7 @@ var UUID = require('node-uuid'); return storeParam(name.toLowerCase())(val, params, ctx); }, - parseLines : function(lines, limit, ctx, stack, cb){ + parseLines : function(lines, limit, ctx, stack, lastIndex, cb){ var self = this if (!cb && typeof ctx === 'function') { cb = ctx; @@ -388,11 +388,13 @@ var UUID = require('node-uuid'); var stack = stack || [] var limitCounter = 0; - while (lines.length) { - l=lines.shift(); + var i = lastIndex || 0 + for (var ii = lines.length; i= lines.length) { // type and params are added to the list of items, get rid of them. delete ctx.type; delete ctx.params; } if (cb) { - if (lines.length) { + if (i < lines.length) { setImmediate(function() { - self.parseLines(lines, limit, ctx, stack, cb); + self.parseLines(lines, limit, ctx, stack, i+1, cb); }); } else { @@ -444,7 +446,7 @@ var UUID = require('node-uuid'); var ctx; if (cb) { // asynchronous execution - self.parseLines(lines, 100, cb); + self.parseLines(lines, 2000, cb); } else { // synchronous execution ctx = self.parseLines(lines, lines.length); diff --git a/package.json b/package.json index fd4bf5b..377eda1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.6.0", + "version": "0.6.1", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ From a059cd2d5211af073fbb9f9da7bd79e0feb06c05 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Mon, 4 Jun 2018 21:40:10 +0200 Subject: [PATCH 49/79] fix error in Apple calendars where dateParam returns a string and toISOString is not existing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 377eda1..b9e6efa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.6.1", + "version": "0.6.2", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ From 307f8e6739b4ff41fed52e11047cb2c5be7e0abc Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Mon, 4 Jun 2018 21:42:16 +0200 Subject: [PATCH 50/79] fix --- ical.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ical.js b/ical.js index 75e5c43..ac6795a 100755 --- a/ical.js +++ b/ical.js @@ -189,7 +189,12 @@ var UUID = require('node-uuid'); var exdate = new Array(); dateParam(name)(val, params, exdate); curr[name] = curr[name] || []; - curr[name][exdate[name].toISOString()] = exdate[name]; + if (exdate[name] instanceof Date) { + curr[name][exdate[name].toISOString()] = exdate[name]; + } + else { + curr[name][exdate[name]] = exdate[name]; + } return curr; } } From d8f7cf08921aedbb19dd5519350d266c50edb65f Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Mon, 4 Jun 2018 21:49:25 +0200 Subject: [PATCH 51/79] update all deps, change to new uid module --- ical.js | 4 ++-- package.json | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ical.js b/ical.js index ac6795a..87b947f 100755 --- a/ical.js +++ b/ical.js @@ -1,4 +1,4 @@ -var UUID = require('node-uuid'); +var UUID = require('uuid/v4'); (function(name, definition) { @@ -339,7 +339,7 @@ var UUID = require('node-uuid'); } else - par[UUID.v4()] = curr; + par[UUID()] = curr; return par } diff --git a/package.json b/package.json index b9e6efa..0e57f15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.6.2", + "version": "0.7.0", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ @@ -17,13 +17,13 @@ "url": "git://github.com/jens-maus/node-ical.git" }, "dependencies": { - "request": "^2.81.0", - "rrule": "2.1.0", - "node-uuid": "^1.4.1" + "request": "^2.87.0", + "rrule": "22.2.9", + "uuid": "^3.2.1" }, "devDependencies": { "vows": "0.8.1", - "underscore": "1.3.0" + "underscore": "1.9.1" }, "scripts": { "test": "./node_modules/vows/bin/vows ./test/test.js && ./node_modules/vows/bin/vows ./test/testAsync.js" From 70d8ffe11dff7975fd6ab8fb046b917672077c50 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Mon, 4 Jun 2018 21:50:49 +0200 Subject: [PATCH 52/79] fix deps --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e57f15..e5a920b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "request": "^2.87.0", - "rrule": "22.2.9", + "rrule": "^2.2.9", "uuid": "^3.2.1" }, "devDependencies": { From bba1c3906028e150107157540e3fe717a05cb2ec Mon Sep 17 00:00:00 2001 From: Alexander Kose Date: Tue, 9 Oct 2018 20:06:13 +0200 Subject: [PATCH 53/79] correct datatype It have to be an object, because it is an associative array. --- ical.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ical.js b/ical.js index 87b947f..39cf65b 100755 --- a/ical.js +++ b/ical.js @@ -323,7 +323,7 @@ var UUID = require('uuid/v4'); // 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(); + par[curr.uid].recurrences = {}; } // Save off our cloned recurrence object into the array, keyed by date. From 72cfac197b8bc0b414f4da86010203366ae3dfcb Mon Sep 17 00:00:00 2001 From: Mason Date: Wed, 10 Oct 2018 00:49:53 -0400 Subject: [PATCH 54/79] Add missing semicolon in example 1 in README.md It's not necessarily a breaking change, but it was just bugging me a little bit :) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9876532..a7f492f 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oc ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function(err, data) { for (var k in data){ if (data.hasOwnProperty(k)) { - var ev = data[k] + var ev = data[k]; console.log("Conference", ev.summary, 'is in', From cb33afcb0420c457613cdc2e7d32addca99a446f Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Wed, 10 Oct 2018 08:48:30 +0200 Subject: [PATCH 55/79] updated travis ci by adding nodejs 10 and removing obsolete nodejs 4 version. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a11abac..2917f99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "4" - "6" - "8" -install: npm install \ No newline at end of file + - "10" +install: npm install From 2c4304ee8bcc877c4d8bf07c0e29fecd6acc30ec Mon Sep 17 00:00:00 2001 From: Alexander Kose Date: Fri, 12 Oct 2018 12:48:21 +0200 Subject: [PATCH 56/79] just another datatype correction --- ical.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ical.js b/ical.js index 39cf65b..0746777 100755 --- a/ical.js +++ b/ical.js @@ -186,9 +186,9 @@ var UUID = require('uuid/v4'); // i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception. var exdateParam = function (name) { return function (val, params, curr) { - var exdate = new Array(); + var exdate = {}; dateParam(name)(val, params, exdate); - curr[name] = curr[name] || []; + curr[name] = curr[name] || {}; if (exdate[name] instanceof Date) { curr[name][exdate[name].toISOString()] = exdate[name]; } From c8bd9c6bdf4c6aaa2e7cfe6332c3aeb2b8bbcde3 Mon Sep 17 00:00:00 2001 From: Alexander Kose Date: Sun, 18 Nov 2018 16:55:30 +0100 Subject: [PATCH 57/79] ignore time if type is date; add date type info --- ical.js | 21 +++++++++++++++++---- package.json | 2 +- test/test.js | 11 ++++++++++- test/testAsync.js | 11 ++++++++++- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/ical.js b/ical.js index 0746777..a360cc3 100755 --- a/ical.js +++ b/ical.js @@ -74,7 +74,7 @@ var UUID = require('uuid/v4'); return curr } } - + var storeParam = function (name) { return function (val, params, curr) { var data; @@ -98,16 +98,26 @@ var UUID = require('uuid/v4'); return dt } + var typeParam = function(name, typeName) { + return function(val, params, curr) { + var ret = 'date-time'; + if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { + ret = 'date'; + } + + return storeValParam(name)(ret, curr); + } + } var dateParam = function(name){ return function (val, params, curr) { var newDate = text(val); - if (params && params.indexOf('VALUE=DATE') > -1) { + if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { // Just Date - var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); + var comps = /^(\d{4})(\d{2})(\d{2}).*$/.exec(val); if (comps !== null) { // No TZ info - assume same timezone as this computer newDate = new Date( @@ -349,7 +359,10 @@ var UUID = require('uuid/v4'); , 'URL' : storeParam('url') , 'UID' : storeParam('uid') , 'LOCATION' : storeParam('location') - , 'DTSTART' : dateParam('start') + , 'DTSTART' : function(val, params, curr) { + curr = dateParam('start')(val, params, curr); + return typeParam('datetype')(val, params, curr); + } , 'DTEND' : dateParam('end') ,' CLASS' : storeParam('class') , 'TRANSP' : storeParam('transparency') diff --git a/package.json b/package.json index e4a818d..ced80e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.8.0", + "version": "0.9.0", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ diff --git a/test/test.js b/test/test.js index f309b81..850d93a 100755 --- a/test/test.js +++ b/test/test.js @@ -33,6 +33,9 @@ vows.describe('node-ical').addBatch({ ,'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){ @@ -53,6 +56,9 @@ vows.describe('node-ical').addBatch({ , '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)' :{ @@ -131,6 +137,9 @@ vows.describe('node-ical').addBatch({ assert.equal(topic.end.getFullYear(), 2011); assert.equal(topic.end.getMonth(), 4); } + , 'datetype is date-time' : function(topic){ + assert.equal(topic.datetype, 'date-time') + } } } @@ -208,7 +217,7 @@ vows.describe('node-ical').addBatch({ })[0]; } , 'has a start' : function(topic){ - assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 12, 0,0).toISOString()) + assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 0, 0, 0).toISOString()) } } , 'event with rrule' :{ diff --git a/test/testAsync.js b/test/testAsync.js index 78a2a56..65f1fdb 100644 --- a/test/testAsync.js +++ b/test/testAsync.js @@ -37,6 +37,9 @@ vows.describe('node-ical').addBatch({ ,'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){ @@ -57,6 +60,9 @@ vows.describe('node-ical').addBatch({ , '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)' :{ @@ -141,6 +147,9 @@ vows.describe('node-ical').addBatch({ assert.equal(topic.end.getFullYear(), 2011); assert.equal(topic.end.getMonth(), 4); } + , 'datetype is date-time' : function(topic){ + assert.equal(topic.datetype, 'date-time') + } } } @@ -227,7 +236,7 @@ vows.describe('node-ical').addBatch({ })[0]; } , 'has a start' : function(topic){ - assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 12, 0,0).toISOString()) + assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 0, 0, 0).toISOString()) } } , 'event with rrule' :{ From c01b4d85fbb07a50a5ad0a5f51d75e322046d43a Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Sat, 16 Feb 2019 22:35:52 +0100 Subject: [PATCH 58/79] adapted .npmignore --- .npmignore | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index c8c23a3..00620b8 100644 --- a/.npmignore +++ b/.npmignore @@ -3,6 +3,8 @@ tasks test test.js example.js +*.ics +*.tgz .travis.yml .idea .git diff --git a/package.json b/package.json index ced80e0..ef38284 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.9.0", + "version": "0.9.1", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ From 7c5bbc12e187e1dec87414f6796f1a396007d312 Mon Sep 17 00:00:00 2001 From: Sanders DeNardi Date: Thu, 28 Feb 2019 22:46:14 -0500 Subject: [PATCH 59/79] add timezone support and equality test --- ical.js | 19 +++++++++++++++++++ package.json | 1 + test/test.js | 9 +++++++++ test/testAsync.js | 9 +++++++++ 4 files changed, 38 insertions(+) diff --git a/ical.js b/ical.js index a360cc3..1f8319c 100755 --- a/ical.js +++ b/ical.js @@ -1,4 +1,5 @@ var UUID = require('uuid/v4'); +var moment = require('moment-timezone'); (function(name, definition) { @@ -147,6 +148,24 @@ var UUID = require('uuid/v4'); parseInt(comps[6], 10 ) )); // TODO add tz + } else if (params && params[0] && params[0].indexOf('TZID=') > -1 && params[0].split('=')[1]) { + var tz = params[0].split('=')[1]; + //lookup tz + var found = moment.tz.names().filter(function(zone) { return zone === tz; })[0]; + if (found) { + var 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), diff --git a/package.json b/package.json index ced80e0..f04a159 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "url": "git://github.com/jens-maus/node-ical.git" }, "dependencies": { + "moment-timezone": "^0.5.23", "request": "^2.88.0", "rrule": "^2.5.6", "uuid": "^3.3.2" diff --git a/test/test.js b/test/test.js index 850d93a..3d8be24 100755 --- a/test/test.js +++ b/test/test.js @@ -118,6 +118,15 @@ vows.describe('node-ical').addBatch({ 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) { + var start = new Date('2002-10-28T22:00:00.000Z'); + assert.equal(event.start.valueOf(), start.valueOf()); + } + } } , 'with test3.ics (testing tvcountdown.com)' : { topic: function() { diff --git a/test/testAsync.js b/test/testAsync.js index 65f1fdb..290568d 100644 --- a/test/testAsync.js +++ b/test/testAsync.js @@ -125,6 +125,15 @@ vows.describe('node-ical').addBatch({ 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) { + var start = new Date('2002-10-28T22:00:00.000Z'); + assert.equal(event.start.valueOf(), start.valueOf()); + } + } } , 'with test3.ics (testing tvcountdown.com)' : { topic: function() { From 0f579fba411f5d43057c359afe7eb66c93a60a5c Mon Sep 17 00:00:00 2001 From: Sanders DeNardi Date: Thu, 28 Feb 2019 23:00:24 -0500 Subject: [PATCH 60/79] fix tests for other zoned dates --- test/test.js | 14 +++++++------- test/testAsync.js | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/test/test.js b/test/test.js index 3d8be24..731a7c1 100755 --- a/test/test.js +++ b/test/test.js @@ -210,7 +210,7 @@ vows.describe('node-ical').addBatch({ } , '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()) + assert.equal(topic.start.toISOString(), new Date(Date.UTC(2011, 10, 10, 02, 0,0)).toISOString()) } } } @@ -408,13 +408,13 @@ vows.describe('node-ical').addBatch({ } , "Has two EXDATES": function (topic) { assert.notEqual(topic.exdate, undefined); - assert.notEqual(topic.exdate[new Date(2015, 06, 08, 12, 0, 0).toISOString()], undefined); - assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString()], undefined); + assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 08, 19, 0, 0)).toISOString()], undefined); + assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 10, 19, 0, 0)).toISOString()], 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()], undefined); - assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString()].summary, 'More Treasure Hunting'); + assert.notEqual(topic.recurrences[new Date(Date.UTC(2015, 06, 07, 19, 0, 0)).toISOString()], undefined); + assert.equal(topic.recurrences[new Date(Date.UTC(2015, 06, 07, 19, 0, 0)).toISOString()].summary, 'More Treasure Hunting'); } } } @@ -437,8 +437,8 @@ vows.describe('node-ical').addBatch({ } , "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()], undefined); - assert.equal(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString()].summary, 'bla bla'); + assert.notEqual(topic.recurrences[new Date(Date.UTC(2016, 7 ,26, 11, 0, 0)).toISOString()], undefined); + assert.equal(topic.recurrences[new Date(Date.UTC(2016, 7, 26, 11, 0, 0)).toISOString()].summary, 'bla bla'); } } } diff --git a/test/testAsync.js b/test/testAsync.js index 290568d..6bc6db7 100644 --- a/test/testAsync.js +++ b/test/testAsync.js @@ -226,7 +226,7 @@ vows.describe('node-ical').addBatch({ } , '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()) + assert.equal(topic.start.toISOString(), new Date(Date.UTC(2011, 10, 10, 02, 0,0)).toISOString()) } } } @@ -448,13 +448,13 @@ vows.describe('node-ical').addBatch({ } , "Has two EXDATES": function (topic) { assert.notEqual(topic.exdate, undefined); - assert.notEqual(topic.exdate[new Date(2015, 06, 08, 12, 0, 0).toISOString()], undefined); - assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString()], undefined); + assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 08, 19, 0, 0)).toISOString()], undefined); + assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 10, 19, 0, 0)).toISOString()], 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()], undefined); - assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString()].summary, 'More Treasure Hunting'); + assert.notEqual(topic.recurrences[new Date(Date.UTC(2015, 06, 07, 19, 0, 0)).toISOString()], undefined); + assert.equal(topic.recurrences[new Date(Date.UTC(2015, 06, 07, 19, 0, 0)).toISOString()].summary, 'More Treasure Hunting'); } } } @@ -480,8 +480,8 @@ vows.describe('node-ical').addBatch({ } , "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()], undefined); - assert.equal(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString()].summary, 'bla bla'); + assert.notEqual(topic.recurrences[new Date(Date.UTC(2016, 7 ,26, 11, 0, 0)).toISOString()], undefined); + assert.equal(topic.recurrences[new Date(Date.UTC(2016, 7, 26, 11, 0, 0)).toISOString()].summary, 'bla bla'); } } } From b92752bcce1a9341bedca8c743b5c3d198bee265 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Fri, 1 Mar 2019 14:51:02 +0100 Subject: [PATCH 61/79] fix async tests and bump version of node-ical --- package.json | 2 +- test/testAsync.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 030a5fc..c8db41b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.9.1", + "version": "0.9.2", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ diff --git a/test/testAsync.js b/test/testAsync.js index 6bc6db7..dbe817a 100644 --- a/test/testAsync.js +++ b/test/testAsync.js @@ -448,13 +448,13 @@ vows.describe('node-ical').addBatch({ } , "Has two EXDATES": function (topic) { assert.notEqual(topic.exdate, undefined); - assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 08, 19, 0, 0)).toISOString()], undefined); - assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 10, 19, 0, 0)).toISOString()], undefined); + assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 08, 19, 0, 0)).toISOString().substring(0, 10)], undefined); + assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 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, 06, 07, 19, 0, 0)).toISOString()], undefined); - assert.equal(topic.recurrences[new Date(Date.UTC(2015, 06, 07, 19, 0, 0)).toISOString()].summary, 'More Treasure Hunting'); + assert.notEqual(topic.recurrences[new Date(Date.UTC(2015, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)], undefined); + assert.equal(topic.recurrences[new Date(Date.UTC(2015, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)].summary, 'More Treasure Hunting'); } } } @@ -480,16 +480,16 @@ vows.describe('node-ical').addBatch({ } , "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()], undefined); - assert.equal(topic.recurrences[new Date(Date.UTC(2016, 7, 26, 11, 0, 0)).toISOString()].summary, 'bla bla'); + 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 quoted parameter values)': { + , 'with test15.ics (testing quoted parameter values)': { topic: function () { var self = this; - ical.parseFile('./test/test14.ics', function(err, ctx) { + ical.parseFile('./test/test15.ics', function(err, ctx) { self.callback(null, ctx); }); } @@ -503,10 +503,10 @@ vows.describe('node-ical').addBatch({ } } - , 'with test15.ics (testing for non-stringified start/end time)': { + , 'with test16.ics (testing for non-stringified start/end time)': { topic: function () { var self = this; - ical.parseFile('./test/test15.ics', function(err, ctx) { + ical.parseFile('./test/test16.ics', function(err, ctx) { self.callback(null, ctx); }); } From 988bfd88c702266f157bd794405e5461be992bad Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Sat, 5 Oct 2019 12:06:26 -0700 Subject: [PATCH 62/79] Added ESLint and Prettier --- .eslintrc.js | 18 + .prettierrc.js | 10 + example.js | 26 +- example_rrule.js | 218 +++-- ical.js | 905 ++++++++++---------- index.js | 10 +- node-ical.js | 124 ++- package-lock.json | 2065 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 22 +- test/test.js | 1138 +++++++++++++------------ test/testAsync.js | 1081 ++++++++++++------------ 11 files changed, 3877 insertions(+), 1740 deletions(-) create mode 100644 .eslintrc.js create mode 100644 .prettierrc.js create mode 100644 package-lock.json diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..908fddd --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + 'env': { + 'commonjs': true, + 'node': true + }, + 'globals': { + 'Atomics': 'readonly', + 'SharedArrayBuffer': 'readonly' + }, + 'parserOptions': { + 'ecmaVersion': 2015 + }, + 'extends': ['eslint:recommended', 'airbnb', 'prettier'], + 'plugins': ['prettier', 'es5'], + 'rules': { + 'prettier/prettier': ['error'] + } +}; 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/example.js b/example.js index 7b41f1a..3d8a83b 100644 --- a/example.js +++ b/example.js @@ -1,16 +1,18 @@ -'use strict'; - 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')}`); +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 (const 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_rrule.js b/example_rrule.js index 0eb13e8..8c5ce2a 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'); +const ical = 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 43b455c..cd05ace 100755 --- a/ical.js +++ b/ical.js @@ -1,522 +1,483 @@ -var UUID = require('uuid/v4'); -var moment = require('moment-timezone'); +const UUID = require('uuid/v4'); +const moment = require('moment-timezone'); (function(name, definition) { - -/**************** - * 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 + * ************* */ + + 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 + const text = function(t) { + t = t || ''; + return t + .replace(/\\\,/g, ',') + .replace(/\\\;/g, ';') + .replace(/\\[nN]/g, '\n') + .replace(/\\\\/g, '\\'); + }; + + const parseParams = function(p) { + const out = {}; + for (let i = 0; i < p.length; i++) { + if (p[i].indexOf('=') > -1) { + const segs = p[i].split('='); + + out[segs[0]] = parseValue(segs.slice(1).join('=')); + } + } + return out || sp; + }; - return dt - } + var parseValue = function(val) { + if (val === 'TRUE') return true; - var typeParam = function(name, typeName) { - return function(val, params, curr) { - var ret = 'date-time'; - if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { - ret = 'date'; - } + if (val === 'FALSE') return false; - return storeValParam(name)(ret, curr); - } - } + const number = Number(val); + if (!isNaN(number)) return number; - var dateParam = function(name){ - return function (val, params, curr) { + return val; + }; - var newDate = text(val); + const storeValParam = function(name) { + return function(val, curr) { + const current = curr[name]; + if (Array.isArray(current)) { + current.push(val); + return curr; + } + + if (current != null) { + curr[name] = [current, val]; + return curr; + } - if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { - // Just Date + curr[name] = val; + return curr; + }; + }; - var 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] - ); + const storeParam = function(name) { + return function(val, params, curr) { + let data; + if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) { + data = { params: parseParams(params), val: text(val) }; + } else data = text(val); - newDate = addTZ(newDate, params); + return storeValParam(name)(data, curr); + }; + }; - // Store as string - worst case scenario - return storeValParam(name)(newDate, curr) - } - } - - - //typical RFC date-time format - var 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]) { - var tz = params[0].split('=')[1]; - //lookup tz - var found = moment.tz.names().filter(function(zone) { return zone === tz; })[0]; - if (found) { - var 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) - ); + const addTZ = function(dt, params) { + const p = parseParams(params); + + if (params && p && dt) { + dt.tz = p.TZID; } - newDate = addTZ(newDate, params); - } + return dt; + }; + const typeParam = function(name, typeName) { + return function(val, params, curr) { + let ret = 'date-time'; + if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { + ret = 'date'; + } - // Store as string - worst case scenario - return storeValParam(name)(newDate, curr) - } - } + return storeValParam(name)(ret, curr); + }; + }; + const dateParam = function(name) { + return function(val, params, curr) { + let newDate = text(val); - var geoParam = function(name){ - return function(val, params, curr){ - storeParam(val, params, curr) - var parts = val.split(';'); - curr[name] = {lat:Number(parts[0]), lon:Number(parts[1])}; - return curr - } - } - - var categoriesParam = function (name) { - var 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. - var exdateParam = function (name) { - return function (val, params, curr) { - var separatorPattern = /\s*,\s*/g; - curr[name] = curr[name] || []; - var dates = val ? val.split(separatorPattern) : []; - dates.forEach(function (entry) { - var 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 exdate[name]", exdate[name]); - } - } - } - ) - return curr; - } - } + if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { + // Just Date - // 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. - var recurrenceParam = function (name) { - return dateParam(name); - } + var 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]); - var addFBType = function (fb, params) { - var p = parseParams(params); + newDate = addTZ(newDate, params); - if (params && p){ - fb.type = p.FBTYPE || "BUSY" - } + // Store as string - worst case scenario + return storeValParam(name)(newDate, curr); + } + } - return fb; - } + // typical RFC date-time format + var 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) + ); + } - var freebusyParam = function (name) { - return function(val, params, curr){ - var fb = addFBType({}, params); - curr[name] = curr[name] || [] - curr[name].push(fb); + newDate = addTZ(newDate, params); + } - storeParam(val, params, fb); + // 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 exdate[name]', exdate[name]); + } + } + }); + return curr; + }; + }; - var parts = val.split('/'); + // 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); + }; - ['start', 'end'].forEach(function (name, index) { - dateParam(name)(parts[index], params, fb); - }); + const addFBType = function(fb, params) { + const p = parseParams(params); - return curr; - } - } + if (params && p) { + fb.type = p.FBTYPE || 'BUSY'; + } - return { + return fb; + }; + const freebusyParam = function(name) { + return function(val, params, curr) { + const fb = addFBType({}, params); + curr[name] = curr[name] || []; + curr[name].push(fb); - objectHandlers : { - 'BEGIN' : function(component, params, curr, stack){ - stack.push(curr) + storeParam(val, params, fb); - return {type:component, params:params} - } + const parts = val.split('/'); - , '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]; - } - } - } + ['start', 'end'].forEach(function(name, index) { + dateParam(name)(parts[index], params, fb); + }); - return curr - } + return curr; + }; + }; - var par = stack.pop() + return { + objectHandlers: { + 'BEGIN': function(component, params, curr, stack) { + stack.push(curr); - 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. + return { type: component, params }; + }, + + '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; + let obj; + for (key in curr) { - par[curr.uid][key] = curr[key]; + if (curr.hasOwnProperty(key)) { + obj = curr[key]; + if (typeof obj === 'string') { + delete curr[key]; + } + } } + return curr; } - } - // 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 = {}; - } - - // 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); - } + 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. + 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.) + const 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 = {}; + } + + // 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; + }, + + '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'), + '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'), + }, + + handleObject(name, val, params, ctx, stack, line) { + const self = this; + + 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); } - // 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 storeParam(name.toLowerCase())(val, params, ctx); + }, + + parseLines(lines, limit, ctx, stack, lastIndex, cb) { + const self = this; + if (!cb && typeof ctx === 'function') { + cb = ctx; + ctx = undefined; } + var ctx = ctx || {}; + var 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++; + } - } - else - par[UUID()] = curr; - - return par - } - - , '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') - , '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') - - }, - - - handleObject : function(name, val, params, ctx, stack, line){ - var self = this - - 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); - } - - return storeParam(name.toLowerCase())(val, params, ctx); - }, - - parseLines : function(lines, limit, ctx, stack, lastIndex, cb){ - var self = this - if (!cb && typeof ctx === 'function') { - cb = ctx; - ctx = undefined; - } - var ctx = ctx || {} - var stack = stack || [] - var limitCounter = 0; - - var i = lastIndex || 0 - for (var ii = lines.length; i limit) { + break; + } + } - ctx = self.handleObject(name, value, params, ctx, stack, l) || {} - if (++limitCounter > limit) { - break; - } - } - - if (i >= lines.length) { - // type and params are added to the list of items, get rid of them. - delete ctx.type; - delete ctx.params; - } - - 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 - } - - }, - - parseICS : function(str,cb){ - var self = this - var lines = str.split(/\r?\n/) - var ctx; - - if (cb) { // asynchronous execution - self.parseLines(lines, 2000, cb); - } - else { // synchronous execution - ctx = self.parseLines(lines, lines.length); - return ctx; - } - } + if (i >= lines.length) { + // type and params are added to the list of items, get rid of them. + delete ctx.type; + delete ctx.params; + } - } -})) + 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; + } + }, + + parseICS(str, cb) { + const self = this; + const lines = str.split(/\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..56e6e11 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,8 @@ -module.exports = require('./ical') +module.exports = require('./ical'); -var node = require('./node-ical') +const node = require('./node-ical'); // Copy node functions across to exports -for (var i in node){ - module.exports[i] = node[i] -} \ No newline at end of file +for (const i in node) { + module.exports[i] = node[i]; +} diff --git a/node-ical.js b/node-ical.js index dca6769..24046e7 100644 --- a/node-ical.js +++ b/node-ical.js @@ -1,66 +1,60 @@ -var ical = require('./ical') - , request = require('request') - , fs = require('fs') - -exports.fromURL = function(url, opts, cb){ - if (!cb) - return; - request(url, opts, function(err, r, data){ - if (err) - { - return cb(err, null); - } - else if (r.statusCode != 200) - { - return cb(r.statusCode + ": " + r.statusMessage, null); - } - - cb(undefined, ical.parseICS(data)); - }) -} - -exports.parseFile = function(filename, cb){ - return ical.parseICS(fs.readFileSync(filename, 'utf8'), cb) -} - - -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); - } +const request = require('request'); +const fs = require('fs'); + +exports.fromURL = function(url, opts, cb) { + if (!cb) return; + request(url, opts, function(err, r, data) { + if (err) { + return cb(err, null); + } + if (r.statusCode != 200) { + return cb(`${r.statusCode}: ${r.statusMessage}`, null); + } + + cb(undefined, ical.parseICS(data)); + }); +}; + +exports.parseFile = function(filename, cb) { + return ical.parseICS(fs.readFileSync(filename, 'utf8'), cb); +}; + +const rrule = require('rrule').RRule; +const ical = require('./ical'); + +ical.objectHandlers.RRULE = function(val, params, curr, stack, line) { + curr.rrule = line; + return curr; +}; +const 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) { + 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); -} + 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 new file mode 100644 index 0000000..3cc61f5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2065 @@ +{ + "name": "node-ical", + "version": "0.9.2", + "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" + } + }, + "@babel/runtime": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.2.tgz", + "integrity": "sha512-EXxN64agfUqqIGeEjI5dL5z0Sw0ZwWo1mLTi4mQowCZ42O59b7DRpZAnTC6OqdF28wMBMFKNb/4uFGrVaigSpg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "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" + } + }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "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=" + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "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==" + }, + "axobject-query": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", + "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7" + } + }, + "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", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "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" + } + }, + "commander": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", + "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", + "dev": true + }, + "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 + }, + "confusing-browser-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", + "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", + "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 + } + } + }, + "damerau-levenshtein": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", + "integrity": "sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA==", + "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.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.1.0.tgz", + "integrity": "sha512-QhrbdRD7ofuV09IuE2ySWBz0FyXCq0rriLTZXZqaWSI79CVtHVRdkFuFTViiqzZhkCgfOh9USpriuGN2gIpZDQ==", + "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.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^6.0.0", + "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-airbnb": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz", + "integrity": "sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ==", + "dev": true, + "requires": { + "eslint-config-airbnb-base": "^14.0.0", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0" + } + }, + "eslint-config-airbnb-base": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", + "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.7", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0" + } + }, + "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-es5": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es5/-/eslint-plugin-es5-1.4.1.tgz", + "integrity": "sha512-kktkmkF2O7pnSZYgrMiYMbt3wCKRIiXePwILv8USDG95YgP0PzhIxSIROLLKmiQQ/Z6LuhDGWTHK04gnbXBvkg==", + "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-jsx-a11y": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", + "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5", + "aria-query": "^3.0.0", + "array-includes": "^3.0.3", + "ast-types-flow": "^0.0.7", + "axobject-query": "^2.0.2", + "damerau-levenshtein": "^1.0.4", + "emoji-regex": "^7.0.2", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.1" + } + }, + "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-plugin-react": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.16.0.tgz", + "integrity": "sha512-GacBAATewhhptbK3/vTP09CbFrgUJmBSaaRcWdbQLFvUZy9yVcQxigBNHGPU/KE2AyHpzj3AWXpxoMTsIDiHug==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.1", + "object.entries": "^1.1.0", + "object.fromentries": "^2.0.0", + "object.values": "^1.1.0", + "prop-types": "^15.7.2", + "resolve": "^1.12.0" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-plugin-react-hooks": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", + "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", + "dev": true + }, + "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.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "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": { + "is-glob": "^4.0.1" + } + }, + "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 + }, + "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 + }, + "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": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "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": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "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" + } + }, + "jsx-ast-utils": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz", + "integrity": "sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "object.assign": "^4.1.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 + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "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": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "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-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "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.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.entries": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", + "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "object.fromentries": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.1.tgz", + "integrity": "sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.15.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "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": "https://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 + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "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==" + }, + "react-is": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", + "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==", + "dev": true + }, + "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" + } + }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", + "dev": true + }, + "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": { + "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": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "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", + "integrity": "sha1-aR95qybM3oC6cm3en+yOc9a88us=", + "dev": true, + "requires": { + "diff": "~1.0.8", + "eyes": "~0.1.6", + "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 c8db41b..92c6ec3 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,27 @@ "uuid": "^3.3.2" }, "devDependencies": { - "vows": "0.8.2", - "underscore": "1.9.1", + "eslint": "^6.1.0", + "eslint-config-airbnb": "^18.0.1", + "eslint-config-prettier": "^6.3.0", + "eslint-plugin-es5": "^1.4.1", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-react": "^7.16.0", + "eslint-plugin-react-hooks": "^1.7.0", + "prettier": "^1.18.2", "request": "^2.88.0", "rrule": "^2.5.6", - "uuid": "^3.3.2" + "underscore": "1.9.1", + "uuid": "^3.3.2", + "vows": "0.8.2" }, "scripts": { - "test": "./node_modules/vows/bin/vows ./test/test.js && ./node_modules/vows/bin/vows ./test/testAsync.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" } } diff --git a/test/test.js b/test/test.js index e16987c..4373b1d 100755 --- a/test/test.js +++ b/test/test.js @@ -1,541 +1,597 @@ -/**** - * 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()) - } - ,'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 () { - 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(), 00); - 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) { - var 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(), 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(Date.UTC(2011, 10, 10, 02, 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, 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){ - 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(Date.UTC(2015, 06, 08, 19, 0, 0)).toISOString().substring(0, 10)], undefined); - assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 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, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)], undefined); - assert.equal(topic.recurrences[new Date(Date.UTC(2015, 06, 07, 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 quoted parameter values)': { - topic: function () { - return ical.parseFile('./test/test15.ics') - } - , 'quoted params': { - topic: function (events) { - return _.values(events)[0]; - } - , "is quoted": function (topic) { - assert.notEqual(topic.start.tz, undefined); - } - } - } - - , 'with test16.ics (testing for non-stringified start/end time)': { - topic: function () { - return ical.parseFile('./test/test16.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) - - -//ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', -// {}, -// function(err, data){ -// console.log("OUT:", data) -// }) +/**** + * 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()); + }, + '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() { + 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(), 00); + 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) { + var 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(), 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(Date.UTC(2011, 10, 10, 02, 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, 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) { + 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(Date.UTC(2015, 06, 08, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.notEqual( + topic.exdate[new Date(Date.UTC(2015, 06, 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, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.equal( + topic.recurrences[new Date(Date.UTC(2015, 06, 07, 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 quoted parameter values)': { + 'topic': function() { + return ical.parseFile('./test/test15.ics'); + }, + 'quoted params': { + 'topic': function(events) { + return _.values(events)[0]; + }, + 'is quoted': function(topic) { + assert.notEqual(topic.start.tz, undefined); + }, + }, + }, + + 'with test16.ics (testing for non-stringified start/end time)': { + 'topic': function() { + return ical.parseFile('./test/test16.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); + +//ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', +// {}, +// function(err, data){ +// console.log("OUT:", data) +// }) diff --git a/test/testAsync.js b/test/testAsync.js index dbe817a..cb61e8c 100644 --- a/test/testAsync.js +++ b/test/testAsync.js @@ -4,536 +4,571 @@ * ***/ process.env.TZ = 'America/San_Francisco'; -var ical = require('../index') +var ical = require('../index'); -var vows = require('vows') - , assert = require('assert') - , _ = require('underscore') +var vows = require('vows'), + assert = require('assert'), + _ = require('underscore'); console.log('START Async Tests'); -vows.describe('node-ical').addBatch({ - 'when parsing test1.ics (node conferences schedule from lanyrd.com, modified)': { - topic: function () { - var self = this; - ical.parseFile('./test/test1.ics', function(err, ctx) { - self.callback(null, ctx); - }); - } - - ,'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()) - } - ,'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 () { - var 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(), 00); - 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) { - var start = new Date('2002-10-28T22:00:00.000Z'); - assert.equal(event.start.valueOf(), start.valueOf()); - } - } - } - , 'with test3.ics (testing tvcountdown.com)' : { - topic: function() { - var 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() { - var 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(), 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 () { - var 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, 02, 0,0)).toISOString()) - } - } - } - - , 'with test6.ics (testing assembly.org)': { - topic: function () { - var 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, 07, 04, 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() { - var self = this; - ical.parseFile('./test/test7.ics', function(err, ctx) { - self.callback(null, ctx); - }); - }, - '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)); +vows.describe('node-ical') + .addBatch({ + 'when parsing test1.ics (node conferences schedule from lanyrd.com, modified)': { + 'topic': function() { + var self = this; + ical.parseFile('./test/test1.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + + '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()); + }, + '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'); + }, + }, }, - '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() { - var self = this; - ical.parseFile('./test/test8.ics', function(err, ctx) { - self.callback(null, ctx); - }); - }, - 'grabbing VTODO task': { - topic: function(topic) { - return _.values(topic)[0]; + 'with test2.ics (testing ical features)': { + 'topic': function() { + var 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(), 00); + 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) { + var start = new Date('2002-10-28T22:00:00.000Z'); + assert.equal(event.start.valueOf(), start.valueOf()); + }, + }, }, - '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() { - var self = this; - ical.parseFile('./test/test9.ics', function(err, ctx) { - self.callback(null, ctx); - }); - }, - 'grabbing VEVENT task': { - topic: function(topic) { - return _.values(topic)[0]; + 'with test3.ics (testing tvcountdown.com)': { + 'topic': function() { + var 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'); + }, + }, }, - 'task completed': function(task){ - assert.equal(task.summary, "Event with an alarm"); - } - } - } - , 'with test 11.ics (VEVENT with custom properties)': { - topic: function() { - var self = this; - ical.parseFile('./test/test10.ics', function(err, ctx) { - self.callback(null, ctx); - }); - }, - 'grabbing custom properties': { - topic: function(topic) { - - } - } - }, - - 'with test10.ics': { - topic: function () { - var 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 () { - var 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(), 00); - } - } - } - - , 'with test12.ics (testing recurrences and exdates)': { - topic: function () { - var 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, 06, 08, 19, 0, 0)).toISOString().substring(0, 10)], undefined); - assert.notEqual(topic.exdate[new Date(Date.UTC(2015, 06, 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, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)], undefined); - assert.equal(topic.recurrences[new Date(Date.UTC(2015, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)].summary, 'More Treasure Hunting'); - } - } - } - - , 'with test13.ics (testing recurrence-id before rrule)': { - topic: function () { - var 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 quoted parameter values)': { - topic: function () { - var self = this; - ical.parseFile('./test/test15.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 test16.ics (testing for non-stringified start/end time)': { - topic: function () { - var self = this; - ical.parseFile('./test/test16.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) + 'with test4.ics (testing tripit.com)': { + 'topic': function() { + var 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(), 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() { + var 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, 02, 0, 0)).toISOString()); + }, + }, + }, + + 'with test6.ics (testing assembly.org)': { + 'topic': function() { + var 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, 07, 04, 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() { + var self = this; + ical.parseFile('./test/test7.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + '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() { + var 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, 06, 16, 10, 57, 45).toISOString()); + }, + }, + }, + 'with test 9.ics (VEVENT with VALARM)': { + 'topic': function() { + var 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() { + var self = this; + ical.parseFile('./test/test10.ics', function(err, ctx) { + self.callback(null, ctx); + }); + }, + 'grabbing custom properties': { + topic: function(topic) {}, + }, + }, + + 'with test10.ics': { + 'topic': function() { + var 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() { + var 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(), 00); + }, + }, + }, + + 'with test12.ics (testing recurrences and exdates)': { + 'topic': function() { + var 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, 06, 08, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.notEqual( + topic.exdate[new Date(Date.UTC(2015, 06, 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, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)], + undefined + ); + assert.equal( + topic.recurrences[new Date(Date.UTC(2015, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)] + .summary, + 'More Treasure Hunting' + ); + }, + }, + }, + + 'with test13.ics (testing recurrence-id before rrule)': { + 'topic': function() { + var 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 quoted parameter values)': { + 'topic': function() { + var self = this; + ical.parseFile('./test/test15.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 test16.ics (testing for non-stringified start/end time)': { + 'topic': function() { + var self = this; + ical.parseFile('./test/test16.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); //ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', // {}, From b99683e4b18324f2d166150da4de8c5333b71672 Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Sat, 5 Oct 2019 12:08:06 -0700 Subject: [PATCH 63/79] Removed React support from ESLint --- .eslintrc.js | 2 +- package.json | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 908fddd..7a9da9d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,7 @@ module.exports = { 'parserOptions': { 'ecmaVersion': 2015 }, - 'extends': ['eslint:recommended', 'airbnb', 'prettier'], + 'extends': ['eslint:recommended', 'airbnb/base', 'prettier'], 'plugins': ['prettier', 'es5'], 'rules': { 'prettier/prettier': ['error'] diff --git a/package.json b/package.json index 92c6ec3..78e5dc8 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,7 @@ "eslint-config-prettier": "^6.3.0", "eslint-plugin-es5": "^1.4.1", "eslint-plugin-import": "^2.18.2", - "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-prettier": "^3.1.1", - "eslint-plugin-react": "^7.16.0", - "eslint-plugin-react-hooks": "^1.7.0", "prettier": "^1.18.2", "request": "^2.88.0", "rrule": "^2.5.6", From d492338095a20f68a4a25784c1ec0bd2af0e21b8 Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Sat, 5 Oct 2019 12:49:34 -0700 Subject: [PATCH 64/79] Rewrote node-ical.js public functions. --- .eslintrc.js | 5 +- example.js | 13 ++- example_rrule.js | 16 ++-- ical.js | 8 +- node-ical.js | 107 ++++++++++++++++++++---- package-lock.json | 209 ---------------------------------------------- package.json | 1 - 7 files changed, 115 insertions(+), 244 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 7a9da9d..ac42e29 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,12 +5,13 @@ module.exports = { }, 'globals': { 'Atomics': 'readonly', - 'SharedArrayBuffer': 'readonly' + 'SharedArrayBuffer': 'readonly', + 'Promise': 'readonly' }, 'parserOptions': { 'ecmaVersion': 2015 }, - 'extends': ['eslint:recommended', 'airbnb/base', 'prettier'], + 'extends': ['eslint:recommended', 'prettier', 'plugin:es5/no-es2015'], 'plugins': ['prettier', 'es5'], 'rules': { 'prettier/prettier': ['error'] diff --git a/example.js b/example.js index 3d8a83b..b866081 100644 --- a/example.js +++ b/example.js @@ -8,9 +8,16 @@ ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function(err, dat 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')}` + '' + + 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 8c5ce2a..2d320d9 100644 --- a/example_rrule.js +++ b/example_rrule.js @@ -22,10 +22,10 @@ for (const k in data) { // 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('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(); } @@ -88,10 +88,10 @@ for (const k in data) { } 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('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 cd05ace..0ece093 100755 --- a/ical.js +++ b/ical.js @@ -269,7 +269,7 @@ const moment = require('moment-timezone'); 'BEGIN': function(component, params, curr, stack) { stack.push(curr); - return { type: component, params }; + return { type: component, params: params }; }, 'END': function(component, params, curr, stack) { @@ -391,7 +391,7 @@ const moment = require('moment-timezone'); 'RECURRENCE-ID': recurrenceParam('recurrenceid'), }, - handleObject(name, val, params, ctx, stack, line) { + handleObject: function(name, val, params, ctx, stack, line) { const self = this; if (self.objectHandlers[name]) return self.objectHandlers[name](val, params, ctx, stack, line); @@ -406,7 +406,7 @@ const moment = require('moment-timezone'); return storeParam(name.toLowerCase())(val, params, ctx); }, - parseLines(lines, limit, ctx, stack, lastIndex, cb) { + parseLines: function(lines, limit, ctx, stack, lastIndex, cb) { const self = this; if (!cb && typeof ctx === 'function') { cb = ctx; @@ -465,7 +465,7 @@ const moment = require('moment-timezone'); } }, - parseICS(str, cb) { + parseICS: function(str, cb) { const self = this; const lines = str.split(/\r?\n/); let ctx; diff --git a/node-ical.js b/node-ical.js index 24046e7..c2e631d 100644 --- a/node-ical.js +++ b/node-ical.js @@ -1,26 +1,99 @@ -const request = require('request'); -const fs = require('fs'); +var request = require('request'); +var fs = require('fs'); +var rrule = require('rrule').RRule; -exports.fromURL = function(url, opts, cb) { - if (!cb) return; - request(url, opts, function(err, r, data) { - if (err) { - return cb(err, null); - } - if (r.statusCode != 200) { - return cb(`${r.statusCode}: ${r.statusMessage}`, null); - } +var ical = require('./ical'); - cb(undefined, ical.parseICS(data)); - }); +/** + * Callback for iCal parsing functions with error and iCal data as a JavaScript object. + * @callback icsCallback + * @param {Error} err + * @param {Object} ics + */ +/** + * A Promise that is undefined if a compatible callback is passed. + * @typedef {(Promise|undefined)} optionalPromise + */ + +/** + * 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. + */ +exports.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. + */ exports.parseFile = function(filename, cb) { - return ical.parseICS(fs.readFileSync(filename, 'utf8'), 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); }; -const rrule = require('rrule').RRule; -const ical = require('./ical'); +// utility to allow callbacks to be used for promises +function promiseCallback(fn, cb) { + var promise = new Promise(fn); + if (!cb) { + return promise; + } else { + promise() + .then(function(ret) { + cb(null, ret); + }) + .catch(function(err) { + cb(err, null); + }); + return; + } +} ical.objectHandlers.RRULE = function(val, params, curr, stack, line) { curr.rrule = line; @@ -44,7 +117,7 @@ ical.objectHandlers.END = function(val, params, curr, stack) { if (typeof curr.start.toISOString === 'function') { try { - rule += `;DTSTART=${curr.start.toISOString().replace(/[-:]/g, '')}`; + 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); diff --git a/package-lock.json b/package-lock.json index 3cc61f5..dbbc628 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,15 +24,6 @@ "js-tokens": "^4.0.0" } }, - "@babel/runtime": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.2.tgz", - "integrity": "sha512-EXxN64agfUqqIGeEjI5dL5z0Sw0ZwWo1mLTi4mQowCZ42O59b7DRpZAnTC6OqdF28wMBMFKNb/4uFGrVaigSpg==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.2" - } - }, "acorn": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", @@ -86,16 +77,6 @@ "sprintf-js": "~1.0.2" } }, - "aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, "array-includes": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", @@ -119,12 +100,6 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -146,15 +121,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, - "axobject-query": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", - "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -245,24 +211,12 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", - "dev": true - }, "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 }, - "confusing-browser-globals": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", - "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", - "dev": true - }, "contains-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", @@ -295,12 +249,6 @@ } } }, - "damerau-levenshtein": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", - "integrity": "sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA==", - "dev": true - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -457,28 +405,6 @@ "v8-compile-cache": "^2.0.3" } }, - "eslint-config-airbnb": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz", - "integrity": "sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ==", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^14.0.0", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - } - }, - "eslint-config-airbnb-base": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", - "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", - "dev": true, - "requires": { - "confusing-browser-globals": "^1.0.7", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - } - }, "eslint-config-prettier": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.3.0.tgz", @@ -594,23 +520,6 @@ } } }, - "eslint-plugin-jsx-a11y": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", - "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.4.5", - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", - "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1" - } - }, "eslint-plugin-prettier": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz", @@ -620,40 +529,6 @@ "prettier-linter-helpers": "^1.0.0" } }, - "eslint-plugin-react": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.16.0.tgz", - "integrity": "sha512-GacBAATewhhptbK3/vTP09CbFrgUJmBSaaRcWdbQLFvUZy9yVcQxigBNHGPU/KE2AyHpzj3AWXpxoMTsIDiHug==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1", - "object.entries": "^1.1.0", - "object.fromentries": "^2.0.0", - "object.values": "^1.1.0", - "prop-types": "^15.7.2", - "resolve": "^1.12.0" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - } - } - }, - "eslint-plugin-react-hooks": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", - "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", - "dev": true - }, "eslint-scope": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", @@ -1158,16 +1033,6 @@ "verror": "1.10.0" } }, - "jsx-ast-utils": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz", - "integrity": "sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "object.assign": "^4.1.0" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -1206,15 +1071,6 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "luxon": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.19.3.tgz", @@ -1326,12 +1182,6 @@ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, "object-inspect": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", @@ -1344,42 +1194,6 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.entries": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", - "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "object.fromentries": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.1.tgz", - "integrity": "sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.15.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, "object.values": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", @@ -1552,17 +1366,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, "psl": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", @@ -1578,12 +1381,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, - "react-is": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", - "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==", - "dev": true - }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -1605,12 +1402,6 @@ "read-pkg": "^2.0.0" } }, - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", - "dev": true - }, "regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", diff --git a/package.json b/package.json index 78e5dc8..e2bc4b4 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ }, "devDependencies": { "eslint": "^6.1.0", - "eslint-config-airbnb": "^18.0.1", "eslint-config-prettier": "^6.3.0", "eslint-plugin-es5": "^1.4.1", "eslint-plugin-import": "^2.18.2", From e01aaaa7f7f68cc23ed6a21965a9d94f9087ff14 Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Sat, 5 Oct 2019 13:10:27 -0700 Subject: [PATCH 65/79] Added typing info and moving node-ical.js to index.js --- index-old.js | 8 +++ index.js | 190 +++++++++++++++++++++++++++++++++++++++++++++++++-- node-ical.js | 133 ------------------------------------ 3 files changed, 193 insertions(+), 138 deletions(-) create mode 100644 index-old.js delete mode 100644 node-ical.js diff --git a/index-old.js b/index-old.js new file mode 100644 index 0000000..8673635 --- /dev/null +++ b/index-old.js @@ -0,0 +1,8 @@ +module.exports = require('./ical'); + +const node = require('.'); + +// Copy node functions across to exports +for (const i in node) { + module.exports[i] = node[i]; +} diff --git a/index.js b/index.js index 56e6e11..71dc9a0 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,188 @@ -module.exports = require('./ical'); +var request = require('request'); +var fs = require('fs'); +var rrule = require('rrule').RRule; -const node = require('./node-ical'); +var ical = require('./ical.js'); -// Copy node functions across to exports -for (const i in node) { - module.exports[i] = node[i]; +/** + * iCal event object. + * @typedef iCalEvent + * @type {object} + * + * @property {string} type - Type of event. + * @property {Array} params + * + * @property {?object} start - When this event starts. + * @property {?object} end - When this event ends. + * + * @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} summary - Event summary string. + * @property {?string} description - Event description. + * + * @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) { + var promise = new Promise(fn); + if (!cb) { + return promise; + } else { + promise() + .then(function(ret) { + cb(null, ret); + }) + .catch(function(err) { + cb(err, null); + }); + return; + } } + +/** + * 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. + */ +exports.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. + */ +exports.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. + */ +exports.parseICS = function(data, cb) { + return promiseCallback(function(resolve, reject) { + ical.parseICS(data, function(err, ics) { + if (err) { + reject(err); + return; + } + resolve(ics); + }); + }, cb); +}; + +ical.objectHandlers.RRULE = function(val, params, curr, stack, line) { + curr.rrule = line; + return curr; +}; +const 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) { + 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); +}; diff --git a/node-ical.js b/node-ical.js deleted file mode 100644 index c2e631d..0000000 --- a/node-ical.js +++ /dev/null @@ -1,133 +0,0 @@ -var request = require('request'); -var fs = require('fs'); -var rrule = require('rrule').RRule; - -var ical = require('./ical'); - -/** - * Callback for iCal parsing functions with error and iCal data as a JavaScript object. - * @callback icsCallback - * @param {Error} err - * @param {Object} ics - */ -/** - * A Promise that is undefined if a compatible callback is passed. - * @typedef {(Promise|undefined)} optionalPromise - */ - -/** - * 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. - */ -exports.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. - */ -exports.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); -}; - -// utility to allow callbacks to be used for promises -function promiseCallback(fn, cb) { - var promise = new Promise(fn); - if (!cb) { - return promise; - } else { - promise() - .then(function(ret) { - cb(null, ret); - }) - .catch(function(err) { - cb(err, null); - }); - return; - } -} - -ical.objectHandlers.RRULE = function(val, params, curr, stack, line) { - curr.rrule = line; - return curr; -}; -const 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) { - 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); -}; From 4224ea91916a2cbe1e578bd8adadcd551153637e Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Sat, 5 Oct 2019 13:57:22 -0700 Subject: [PATCH 66/79] Reformatted ical.js and set up APIs to be backwards-compatible --- ical.js | 684 ++++++++++++++++++++++++---------------------- index-old.js | 8 - index.js | 124 ++++++--- package-lock.json | 12 +- package.json | 7 +- test/test.js | 26 +- 6 files changed, 457 insertions(+), 404 deletions(-) delete mode 100644 index-old.js diff --git a/ical.js b/ical.js index 0ece093..6d4fcf0 100755 --- a/ical.js +++ b/ical.js @@ -1,161 +1,141 @@ -const UUID = require('uuid/v4'); -const moment = require('moment-timezone'); - -(function(name, definition) { - /** ************** - * 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 - const text = function(t) { - t = t || ''; - return t - .replace(/\\\,/g, ',') - .replace(/\\\;/g, ';') - .replace(/\\[nN]/g, '\n') - .replace(/\\\\/g, '\\'); - }; - - const parseParams = function(p) { - const out = {}; - for (let i = 0; i < p.length; i++) { - if (p[i].indexOf('=') > -1) { - const segs = p[i].split('='); - - out[segs[0]] = parseValue(segs.slice(1).join('=')); - } +var UUID = require('uuid/v4'); +var moment = require('moment-timezone'); +var rrule = require('rrule').RRule; + +// 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 < p.length; i++) { + if (p[i].indexOf('=') > -1) { + var segs = p[i].split('='); + + out[segs[0]] = parseValue(segs.slice(1).join('=')); } - return out || sp; - }; - - var parseValue = function(val) { - if (val === 'TRUE') return true; + } + // sp is not defined in this scope, typo? + // original code from peterbraden + // return out || sp; + return out; +}; - if (val === 'FALSE') return false; +var parseValue = function(val) { + if (val === 'TRUE') return true; + if (val === 'FALSE') return false; - const number = Number(val); - if (!isNaN(number)) return number; + var number = Number(val); + if (!isNaN(number)) return number; - return val; - }; + return val; +}; - const storeValParam = function(name) { - return function(val, curr) { - const current = curr[name]; - if (Array.isArray(current)) { - current.push(val); - return curr; - } +var storeValParam = function(name) { + return function(val, curr) { + var current = curr[name]; - if (current != null) { - curr[name] = [current, val]; - return curr; - } + if (Array.isArray(current)) { + current.push(val); + return curr; + } - curr[name] = val; + if (current != null) { + curr[name] = [current, val]; return curr; - }; + } + + curr[name] = val; + return curr; }; +}; - const storeParam = function(name) { - return function(val, params, curr) { - let data; - if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) { - data = { params: parseParams(params), val: text(val) }; - } else data = text(val); +var storeParam = function(name) { + return function(val, params, curr) { + var data; + if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) { + data = { params: parseParams(params), val: text(val) }; + } else data = text(val); - return storeValParam(name)(data, curr); - }; + return storeValParam(name)(data, curr); }; +}; - const addTZ = function(dt, params) { - const p = parseParams(params); +var addTZ = function(dt, params) { + var p = parseParams(params); - if (params && p && dt) { - dt.tz = p.TZID; - } + if (params && p && dt) { + dt.tz = p.TZID; + } - return dt; - }; + return dt; +}; - const typeParam = function(name, typeName) { - return function(val, params, curr) { - let ret = 'date-time'; - if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { - ret = 'date'; - } +var typeParam = function(name, typeName) { + // typename is not used in this function? + return function(val, params, curr) { + var ret = 'date-time'; + if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { + ret = 'date'; + } - return storeValParam(name)(ret, curr); - }; + return storeValParam(name)(ret, curr); }; +}; - const dateParam = function(name) { - return function(val, params, curr) { - let newDate = text(val); +var dateParam = function(name) { + return function(val, params, curr) { + var newDate = text(val); - if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { - // Just Date + if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { + // Just Date - var 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]); + var 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]); - newDate = addTZ(newDate, params); + newDate = addTZ(newDate, params); - // Store as string - worst case scenario - return storeValParam(name)(newDate, curr); - } + // Store as string - worst case scenario + return storeValParam(name)(newDate, curr); } + } - // typical RFC date-time format - var 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) - ); - } + // typical RFC date-time format + // WARNING: comps has already been defined! + var 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]) { + var tz = params[0].split('=')[1]; + // lookup tz + var found = moment.tz.names().filter(function(zone) { + return zone === tz; + })[0]; + if (found) { + var 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, @@ -165,133 +145,143 @@ const moment = require('moment-timezone'); parseInt(comps[6], 10) ); } - - newDate = addTZ(newDate, params); + } 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) + ); } - // Store as string - worst case scenario - return storeValParam(name)(newDate, curr); - }; - }; + newDate = addTZ(newDate, params); + } - 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; - }; + // Store as string - worst case scenario + return storeValParam(name)(newDate, 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; - }; +}; + +var geoParam = function(name) { + return function(val, params, curr) { + storeParam(val, params, curr); + var parts = val.split(';'); + curr[name] = { lat: Number(parts[0]), lon: Number(parts[1]) }; + 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 exdate[name]', exdate[name]); - } +}; + +var categoriesParam = function(name) { + var 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. +var exdateParam = function(name) { + return function(val, params, curr) { + var separatorPattern = /\s*,\s*/g; + curr[name] = curr[name] || []; + var dates = val ? val.split(separatorPattern) : []; + dates.forEach(function(entry) { + var 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 exdate[name]', exdate[name]); } - }); - return curr; - }; + } + }); + 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); - }; +// 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. +var recurrenceParam = function(name) { + return dateParam(name); +}; - const addFBType = function(fb, params) { - const p = parseParams(params); +var addFBType = function(fb, params) { + var p = parseParams(params); - if (params && p) { - fb.type = p.FBTYPE || 'BUSY'; - } + if (params && p) { + fb.type = p.FBTYPE || 'BUSY'; + } - return fb; - }; + return fb; +}; - const freebusyParam = function(name) { - return function(val, params, curr) { - const fb = addFBType({}, params); - curr[name] = curr[name] || []; - curr[name].push(fb); +var freebusyParam = function(name) { + return function(val, params, curr) { + var fb = addFBType({}, params); + curr[name] = curr[name] || []; + curr[name].push(fb); - storeParam(val, params, fb); + storeParam(val, params, fb); - const parts = val.split('/'); + var parts = val.split('/'); - ['start', 'end'].forEach(function(name, index) { - dateParam(name)(parts[index], params, fb); - }); + ['start', 'end'].forEach(function(name, index) { + dateParam(name)(parts[index], params, fb); + }); - return curr; - }; + return curr; }; +}; - return { - objectHandlers: { - 'BEGIN': function(component, params, curr, stack) { - stack.push(curr); +module.exports = { + objectHandlers: { + 'BEGIN': function(component, params, curr, stack) { + stack.push(curr); - return { type: component, params: params }; - }, - - 'END': function(component, params, curr, stack) { + return { type: component, params: params }; + }, + 'END': function(val, params, curr, stack) { + // original end function + var 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 var key; - let obj; + var obj; for (key in curr) { - if (curr.hasOwnProperty(key)) { - obj = curr[key]; - if (typeof obj === 'string') { - delete curr[key]; - } + if (!{}.hasOwnProperty.call(curr, key)) continue; + obj = curr[key]; + if (typeof obj === 'string') { + delete curr[key]; } } return curr; } - const par = stack.pop(); + var par = stack.pop(); if (curr.uid) { // If this is the first time we run into this UID, just save it. @@ -308,7 +298,7 @@ const moment = require('moment-timezone'); // 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; + var key; // WARNING key is already defined for (key in curr) { par[curr.uid][key] = curr[key]; } @@ -332,8 +322,8 @@ const moment = require('moment-timezone'); // 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(); - var key; + var recurrenceObj = new Object(); + var key; // WARNING key is already defined for (key in curr) { recurrenceObj[key] = curr[key]; } @@ -365,119 +355,153 @@ const moment = require('moment-timezone'); } else par[UUID()] = curr; return par; - }, - - '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'), - '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'), - }, - - handleObject: function(name, val, params, ctx, stack, line) { - const self = this; - - if (self.objectHandlers[name]) return self.objectHandlers[name](val, params, ctx, stack, line); + }; + // 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]); + } + } - // 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); + 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 storeParam(name.toLowerCase())(val, params, ctx); + 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; + }, + }, - parseLines: function(lines, limit, ctx, stack, lastIndex, cb) { - const self = this; - if (!cb && typeof ctx === 'function') { - cb = ctx; - ctx = undefined; - } - var ctx = ctx || {}; - var 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++; - } + handleObject: function(name, val, params, ctx, stack, line) { + var self = this; - const exp = /([^":;]+)((?:;(?:[^":;]+)(?:=(?:(?:"[^"]*")|(?:[^":;]+))))*):(.*)/; - let kv = l.match(exp); + if (self.objectHandlers[name]) return self.objectHandlers[name](val, params, ctx, stack, line); - if (kv === null) { - // Invalid line - must have k&v - continue; - } - kv = kv.slice(1); + // 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); + } - const value = kv[kv.length - 1]; - const name = kv[0]; - const params = kv[1] ? kv[1].split(';').slice(1) : []; + return storeParam(name.toLowerCase())(val, params, ctx); + }, - ctx = self.handleObject(name, value, params, ctx, stack, l) || {}; - if (++limitCounter > limit) { - break; - } + parseLines: function(lines, limit, ctx, stack, lastIndex, cb) { + var self = this; + + if (!cb && typeof ctx === 'function') { + cb = ctx; + ctx = undefined; + } + ctx = ctx || {}; + stack = stack || []; + + var limitCounter = 0; + + var i = lastIndex || 0; + for (var ii = lines.length; i < ii; i++) { + var 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 (i >= lines.length) { - // type and params are added to the list of items, get rid of them. - delete ctx.type; - delete ctx.params; + var exp = /([^":;]+)((?:;(?:[^":;]+)(?:=(?:(?:"[^"]*")|(?:[^":;]+))))*):(.*)/; + var kv = l.match(exp); + + if (kv === null) { + // Invalid line - must have k&v + continue; } + kv = kv.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; + var value = kv[kv.length - 1]; + var name = kv[0]; + var params = kv[1] ? kv[1].split(';').slice(1) : []; + + ctx = self.handleObject(name, value, params, ctx, stack, l) || {}; + if (++limitCounter > limit) { + break; } - }, + } - parseICS: function(str, cb) { - const self = this; - const lines = str.split(/\r?\n/); - let ctx; + if (i >= lines.length) { + // type and params are added to the list of items, get rid of them. + delete ctx.type; + delete ctx.params; + } - if (cb) { - // asynchronous execution - self.parseLines(lines, 2000, cb); + if (cb) { + if (i < lines.length) { + setImmediate(function() { + self.parseLines(lines, limit, ctx, stack, i + 1, cb); + }); } else { - // synchronous execution - ctx = self.parseLines(lines, lines.length); - return ctx; + setImmediate(function() { + cb(null, ctx); + }); } - }, - }; -}); + } else { + return ctx; + } + }, + + parseICS: function(str, cb) { + var self = this; + var lines = str.split(/\r?\n/); + var 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-old.js b/index-old.js deleted file mode 100644 index 8673635..0000000 --- a/index-old.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = require('./ical'); - -const node = require('.'); - -// Copy node functions across to exports -for (const i in node) { - module.exports[i] = node[i]; -} diff --git a/index.js b/index.js index 71dc9a0..e21ef6e 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ var request = require('request'); var fs = require('fs'); -var rrule = require('rrule').RRule; var ical = require('./ical.js'); @@ -9,12 +8,19 @@ var ical = require('./ical.js'); * @typedef iCalEvent * @type {object} * + * These two fields are always present: * @property {string} type - Type of event. * @property {Array} 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. + * This is a general guide for most iCal VEVENTs. * @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. @@ -22,8 +28,9 @@ var ical = require('./ical.js'); * * @property {?string} uid - Unique event identifier. * - * @property {?string} summary - Event summary string. - * @property {?string} description - Event description. + * @property {?string} status - Event status. + * + * @property {?string} sequence - Event sequence. * * @property {?string} url - URL of this event. * @@ -32,7 +39,7 @@ var ical = require('./ical.js'); * lat: number, lon: number * }} geo - Lat/lon location of this event. * - * @property {?Array.} - Array of event catagories. + * @property {?Array.} - Array of event catagories. */ /** * Object containing iCal events. @@ -55,7 +62,7 @@ function promiseCallback(fn, cb) { if (!cb) { return promise; } else { - promise() + promise .then(function(ret) { cb(null, ret); }) @@ -66,6 +73,13 @@ function promiseCallback(fn, cb) { } } +// sync functions +var sync = {}; +// async functions +var async = {}; +// auto-detect functions for backwards compatibility. +var autodetect = {}; + /** * Download an iCal file from the web and parse it. * @@ -78,7 +92,7 @@ function promiseCallback(fn, cb) { * * @returns {optionalPromise} Promise is returned if no callback is passed. */ -exports.fromURL = function(url, opts, cb) { +async.fromURL = function(url, opts, cb) { return promiseCallback(function(resolve, reject) { request(url, opts, function(err, res, data) { if (err) { @@ -88,7 +102,7 @@ exports.fromURL = function(url, opts, cb) { // 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)); + reject(new Error(res.statusCode + ' ' + res.statusMessage)); return; } ical.parseICS(data, function(err, ics) { @@ -111,7 +125,7 @@ exports.fromURL = function(url, opts, cb) { * * @returns {optionalPromise} Promise is returned if no callback is passed. */ -exports.parseFile = function(filename, cb) { +async.parseFile = function(filename, cb) { return promiseCallback(function(resolve, reject) { fs.readFile(filename, 'utf8', function(err, data) { if (err) { @@ -138,7 +152,7 @@ exports.parseFile = function(filename, cb) { * * @returns {optionalPromise} Promise is returned if no callback is passed. */ -exports.parseICS = function(data, cb) { +async.parseICS = function(data, cb) { return promiseCallback(function(resolve, reject) { ical.parseICS(data, function(err, ics) { if (err) { @@ -150,39 +164,67 @@ exports.parseICS = function(data, cb) { }, cb); }; -ical.objectHandlers.RRULE = function(val, params, curr, stack, line) { - curr.rrule = line; - return curr; +/** + * 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) { + var data = fs.readFileSync(filename, 'utf8'); + return ical.parseICS(data); }; -const 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) { - 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); - } +/** + * 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); + else { + async.parseFile(filename, cb); + return; + } +}; + +/** + * 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); + else { + async.parseICS(data, cb); + return; } - return originalEnd.call(this, val, params, curr, stack); +}; + +// export +module.exports = { + parseFile: autodetect.parseFile, + parseICS: autodetect.parseICS, + sync: sync, + async: async, }; diff --git a/package-lock.json b/package-lock.json index dbbc628..281745b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -361,9 +361,9 @@ "dev": true }, "eslint": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.1.0.tgz", - "integrity": "sha512-QhrbdRD7ofuV09IuE2ySWBz0FyXCq0rriLTZXZqaWSI79CVtHVRdkFuFTViiqzZhkCgfOh9USpriuGN2gIpZDQ==", + "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", @@ -373,9 +373,9 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "eslint-scope": "^5.0.0", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^6.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", diff --git a/package.json b/package.json index e2bc4b4..534bc93 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "uuid": "^3.3.2" }, "devDependencies": { - "eslint": "^6.1.0", + "eslint": "^6.5.1", "eslint-config-prettier": "^6.3.0", "eslint-plugin-es5": "^1.4.1", "eslint-plugin-import": "^2.18.2", @@ -33,13 +33,14 @@ "rrule": "^2.5.6", "underscore": "1.9.1", "uuid": "^3.3.2", - "vows": "0.8.2" + "vows": "^0.8.2" }, "scripts": { "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" + "format": "prettier --config .prettierrc.js --write *.js test/*.js", + "precommit": "npm run format && npm run fix" } } diff --git a/test/test.js b/test/test.js index 4373b1d..40659de 100755 --- a/test/test.js +++ b/test/test.js @@ -121,7 +121,7 @@ vows.describe('node-ical') 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.getUTCHours(), 0); assert.equal(topic.end.getUTCMinutes(), 30); }, }, @@ -175,7 +175,7 @@ vows.describe('node-ical') }, 'has a start datetime': function(topic) { assert.equal(topic.start.getFullYear(), 2011); - assert.equal(topic.start.getMonth(), 09); + assert.equal(topic.start.getMonth(), 9); assert.equal(topic.start.getDate(), 11); }, @@ -217,7 +217,7 @@ vows.describe('node-ical') }, 'has a start': function(topic) { assert.equal(topic.start.tz, 'America/Phoenix'); - assert.equal(topic.start.toISOString(), new Date(Date.UTC(2011, 10, 10, 02, 0, 0)).toISOString()); + assert.equal(topic.start.toISOString(), new Date(Date.UTC(2011, 10, 10, 2, 0, 0)).toISOString()); }, }, }, @@ -233,7 +233,7 @@ vows.describe('node-ical') })[0]; }, 'has a start': function(topic) { - assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 0, 0, 0).toISOString()); + assert.equal(topic.start.toISOString(), new Date(2011, 7, 4, 0, 0, 0).toISOString()); }, }, 'event with rrule': { @@ -274,7 +274,7 @@ vows.describe('node-ical') }, 'task completed': function(task) { assert.equal(task.completion, 100); - assert.equal(task.completed.toISOString(), new Date(2013, 06, 16, 10, 57, 45).toISOString()); + assert.equal(task.completed.toISOString(), new Date(2013, 6, 16, 10, 57, 45).toISOString()); }, }, }, @@ -400,7 +400,7 @@ vows.describe('node-ical') assert.equal(topic.end.getFullYear(), 2014); assert.equal(topic.end.getMonth(), 3); assert.equal(topic.end.getUTCHours(), 19); - assert.equal(topic.end.getUTCMinutes(), 00); + assert.equal(topic.end.getUTCMinutes(), 0); }, }, }, @@ -424,22 +424,22 @@ vows.describe('node-ical') 'Has two EXDATES': function(topic) { assert.notEqual(topic.exdate, undefined); assert.notEqual( - topic.exdate[new Date(Date.UTC(2015, 06, 08, 19, 0, 0)).toISOString().substring(0, 10)], + 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, 06, 10, 19, 0, 0)).toISOString().substring(0, 10)], + 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, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)], + 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, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)] + topic.recurrences[new Date(Date.UTC(2015, 6, 7, 19, 0, 0)).toISOString().substring(0, 10)] .summary, 'More Treasure Hunting' ); @@ -589,9 +589,3 @@ vows.describe('node-ical') }, }) .export(module); - -//ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', -// {}, -// function(err, data){ -// console.log("OUT:", data) -// }) From 12c2802bcdcbbdc5c1d39b1c8ef6fffd00b89c8b Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Sat, 5 Oct 2019 14:05:31 -0700 Subject: [PATCH 67/79] All tests pass and all code is correctly formatted and compatible --- example.js | 37 ++++++++++++++++++------------------- example_rrule.js | 40 ++++++++++++++++++++-------------------- index.js | 1 + package.json | 3 --- test/testAsync.js | 26 ++++++++++---------------- 5 files changed, 49 insertions(+), 58 deletions(-) diff --git a/example.js b/example.js index b866081..57ffda5 100644 --- a/example.js +++ b/example.js @@ -1,25 +1,24 @@ -const ical = require('ical'); +var ical = require('./index'); -const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +var 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 (const 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') - ); - } + for (var k in data) { + if (!{}.hasOwnProperty.call(data, k)) continue; + 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') + ); } } }); diff --git a/example_rrule.js b/example_rrule.js index 2d320d9..568b7e3 100644 --- a/example_rrule.js +++ b/example_rrule.js @@ -1,24 +1,24 @@ -const moment = require('moment'); -const ical = require('./node-ical'); +var moment = require('moment-timezone'); +var ical = require('./index'); // require('node-ical'); -const data = ical.parseFile('./examples/example_rrule.ics'); +var data = ical.parseFile('./examples/example_rrule.ics'); // Complicated example demonstrating how to handle recurrence rules and exceptions. -for (const k in data) { +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. - const rangeStart = moment('2017-01-01'); - const rangeEnd = moment('2017-12-31'); + var rangeStart = moment('2017-01-01'); + var rangeEnd = moment('2017-12-31'); - const event = data[k]; + var event = data[k]; if (event.type === 'VEVENT') { - const title = event.summary; - let startDate = moment(event.start); - let endDate = moment(event.end); + 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. - const duration = parseInt(endDate.format('x')) - parseInt(startDate.format('x')); + 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') { @@ -33,7 +33,7 @@ for (const k in data) { 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) { + var dates = event.rrule.between(rangeStart.toDate(), rangeEnd.toDate(), true, function(date, i) { return true; }); @@ -43,7 +43,7 @@ for (const k in data) { // 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) { + 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) { @@ -53,16 +53,16 @@ for (const k in data) { } // 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; + 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) - const dateLookupKey = date.toISOString().substring(0, 10); + 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) { @@ -78,7 +78,7 @@ for (const k in data) { } // Set the the title and the end date from either the regular event or the recurrence override. - const recurrenceTitle = curEvent.summary; + 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, diff --git a/index.js b/index.js index e21ef6e..9aa8b0f 100644 --- a/index.js +++ b/index.js @@ -223,6 +223,7 @@ autodetect.parseICS = function(data, cb) { // export module.exports = { + fromURL: async.fromURL, parseFile: autodetect.parseFile, parseICS: autodetect.parseICS, sync: sync, diff --git a/package.json b/package.json index 534bc93..0635225 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,7 @@ "eslint-plugin-import": "^2.18.2", "eslint-plugin-prettier": "^3.1.1", "prettier": "^1.18.2", - "request": "^2.88.0", - "rrule": "^2.5.6", "underscore": "1.9.1", - "uuid": "^3.3.2", "vows": "^0.8.2" }, "scripts": { diff --git a/test/testAsync.js b/test/testAsync.js index cb61e8c..0c4dbb8 100644 --- a/test/testAsync.js +++ b/test/testAsync.js @@ -128,7 +128,7 @@ vows.describe('node-ical') 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.getUTCHours(), 0); assert.equal(topic.end.getUTCMinutes(), 30); }, }, @@ -188,7 +188,7 @@ vows.describe('node-ical') }, 'has a start datetime': function(topic) { assert.equal(topic.start.getFullYear(), 2011); - assert.equal(topic.start.getMonth(), 09); + assert.equal(topic.start.getMonth(), 9); assert.equal(topic.start.getDate(), 11); }, @@ -233,7 +233,7 @@ vows.describe('node-ical') }, 'has a start': function(topic) { assert.equal(topic.start.tz, 'America/Phoenix'); - assert.equal(topic.start.toISOString(), new Date(Date.UTC(2011, 10, 10, 02, 0, 0)).toISOString()); + assert.equal(topic.start.toISOString(), new Date(Date.UTC(2011, 10, 10, 2, 0, 0)).toISOString()); }, }, }, @@ -252,7 +252,7 @@ vows.describe('node-ical') })[0]; }, 'has a start': function(topic) { - assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 0, 0, 0).toISOString()); + assert.equal(topic.start.toISOString(), new Date(2011, 7, 4, 0, 0, 0).toISOString()); }, }, 'event with rrule': { @@ -299,7 +299,7 @@ vows.describe('node-ical') }, 'task completed': function(task) { assert.equal(task.completion, 100); - assert.equal(task.completed.toISOString(), new Date(2013, 06, 16, 10, 57, 45).toISOString()); + assert.equal(task.completed.toISOString(), new Date(2013, 6, 16, 10, 57, 45).toISOString()); }, }, }, @@ -437,7 +437,7 @@ vows.describe('node-ical') assert.equal(topic.end.getFullYear(), 2014); assert.equal(topic.end.getMonth(), 3); assert.equal(topic.end.getUTCHours(), 19); - assert.equal(topic.end.getUTCMinutes(), 00); + assert.equal(topic.end.getUTCMinutes(), 0); }, }, }, @@ -464,22 +464,22 @@ vows.describe('node-ical') 'Has two EXDATES': function(topic) { assert.notEqual(topic.exdate, undefined); assert.notEqual( - topic.exdate[new Date(Date.UTC(2015, 06, 08, 19, 0, 0)).toISOString().substring(0, 10)], + 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, 06, 10, 19, 0, 0)).toISOString().substring(0, 10)], + 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, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)], + 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, 06, 07, 19, 0, 0)).toISOString().substring(0, 10)] + topic.recurrences[new Date(Date.UTC(2015, 6, 7, 19, 0, 0)).toISOString().substring(0, 10)] .summary, 'More Treasure Hunting' ); @@ -569,9 +569,3 @@ vows.describe('node-ical') }, }) .export(module); - -//ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', -// {}, -// function(err, data){ -// console.log("OUT:", data) -// }) From b88eb5a190ac8a9b7e9164ec071f30f2d97494e4 Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Sat, 5 Oct 2019 14:11:20 -0700 Subject: [PATCH 68/79] Improved JSDoc for iCalEvent --- index.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 9aa8b0f..3d70052 100644 --- a/index.js +++ b/index.js @@ -5,16 +5,21 @@ var ical = require('./ical.js'); /** * 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} * - * These two fields are always present: * @property {string} type - Type of event. - * @property {Array} params + * @property {Array} params - Extra event parameters. * - * 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. - * This is a general guide for most iCal VEVENTs. * @property {?object} start - When this event starts. * @property {?object} end - When this event ends. * From a1157276cad5e78bfd2979d8195536378bf6eb26 Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Sat, 5 Oct 2019 14:54:27 -0700 Subject: [PATCH 69/79] Rewrote README to fix typos and add new API info --- README.md | 185 +++++++++++++++++++++++++++++++++++++++++++++--------- ical.js | 7 +++ 2 files changed, 162 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 19fff8a..b5f5b69 100644 --- a/README.md +++ b/README.md @@ -5,69 +5,194 @@ [![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 nodejs. This modul is a direct fork +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 allowing to parse icalender/ics files in pure javascript, thus within the browser itself. This node-ical -module, however, primarily targets nodejs use which should allow to parse icalendar/ics files in a more flexible -way. +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: - - npm install node-ical +```sh +npm install node-ical +``` ## API -Parses a string with ICS content synchronous. This can block the nodejs event loop on big files -```js -var ical = require('node-ical'); +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 asynchronous to prevent event loop from being blocked. -```js -var ical = require('node-ical'); +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 synchronous. This can block the nodejs event loop on big files -```js -var data = ical.parseFile(filename); +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 asynchronous to prevent event loop from being blocked. -```js -var data = ical.parseFile(filename, function(err, data) { +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, parses it and returns the parsed data. This method always work asynchronous -```js +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). +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) -```js +#### 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)) { - 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')}`); - } + 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')}`); + } + } } - } }); ``` diff --git a/ical.js b/ical.js index 6d4fcf0..a71e210 100755 --- a/ical.js +++ b/ical.js @@ -2,6 +2,13 @@ var UUID = require('uuid/v4'); var moment = require('moment-timezone'); var rrule = require('rrule').RRule; +/**************** + * A tolerant, minimal icalendar parser + * (http://tools.ietf.org/html/rfc5545) + * + * + * **************/ + // Unescape Text re RFC 4.3.11 var text = function(t) { t = t || ''; From dd7bdfe4e99f19df71f2abf1e2f2125f33158126 Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Sat, 5 Oct 2019 15:10:51 -0700 Subject: [PATCH 70/79] Added support for ical.objectHandlers / ical.handleObject / ical.parseLines --- index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 3d70052..971fc05 100644 --- a/index.js +++ b/index.js @@ -226,11 +226,18 @@ autodetect.parseICS = function(data, cb) { } }; -// export +// export api functions module.exports = { + // autodetect fromURL: async.fromURL, parseFile: autodetect.parseFile, parseICS: autodetect.parseICS, + // sync sync: sync, + // async async: async, + // other backwards compat things + objectHandlers: ical.objectHandlers, + handleObject: ical.handleObject, + parseLines: ical.parseLines, }; From 2a0677847916f7351b4c5cf56e027988e8da7373 Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Wed, 9 Oct 2019 16:44:04 -0700 Subject: [PATCH 71/79] Added and applied ESLint rule no-var --- .eslintrc.js | 10 ++-- README.md | 2 +- example.js | 21 +++---- example_rrule.js | 56 +++++++++---------- ical.js | 139 +++++++++++++++++++++++----------------------- index.js | 50 ++++++++--------- package-lock.json | 6 -- package.json | 1 - test/test.js | 22 ++++---- test/testAsync.js | 54 +++++++++--------- 10 files changed, 171 insertions(+), 190 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ac42e29..6c5765d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,7 @@ module.exports = { 'env': { 'commonjs': true, + 'es6': true, 'node': true }, 'globals': { @@ -9,11 +10,12 @@ module.exports = { 'Promise': 'readonly' }, 'parserOptions': { - 'ecmaVersion': 2015 + 'ecmaVersion': 2018 }, - 'extends': ['eslint:recommended', 'prettier', 'plugin:es5/no-es2015'], - 'plugins': ['prettier', 'es5'], + 'extends': ['eslint:recommended', 'prettier'], + 'plugins': ['prettier'], 'rules': { - 'prettier/prettier': ['error'] + 'prettier/prettier': ['error'], + 'no-var': 'error', 'prefer-const': 'warn' } }; diff --git a/README.md b/README.md index b5f5b69..57754ae 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', ' 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]; + 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 57ffda5..edc4b04 100644 --- a/example.js +++ b/example.js @@ -1,23 +1,16 @@ -var ical = require('./index'); +const ical = require('./index'); -var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +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 (var k in data) { + for (const k in data) { if (!{}.hasOwnProperty.call(data, k)) continue; - var ev = data[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') + `${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 568b7e3..94db051 100644 --- a/example_rrule.js +++ b/example_rrule.js @@ -1,31 +1,31 @@ -var moment = require('moment-timezone'); -var ical = require('./index'); // require('node-ical'); +const moment = require('moment-timezone'); +const ical = require('./index'); // require('node-ical'); -var data = ical.parseFile('./examples/example_rrule.ics'); +const data = ical.parseFile('./examples/example_rrule.ics'); // Complicated example demonstrating how to handle recurrence rules and exceptions. -for (var k in data) { +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. - var rangeStart = moment('2017-01-01'); - var rangeEnd = moment('2017-12-31'); + const rangeStart = moment('2017-01-01'); + const rangeEnd = moment('2017-12-31'); - var event = data[k]; + const event = data[k]; if (event.type === 'VEVENT') { - var title = event.summary; - var startDate = moment(event.start); - var endDate = moment(event.end); + 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. - var duration = parseInt(endDate.format('x')) - parseInt(startDate.format('x')); + 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(`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(); } @@ -33,7 +33,7 @@ for (var k in data) { 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) { + const dates = event.rrule.between(rangeStart.toDate(), rangeEnd.toDate(), true, function(date, i) { return true; }); @@ -43,7 +43,7 @@ for (var k in data) { // 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) { + 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) { @@ -53,16 +53,16 @@ for (var k in data) { } // 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; + 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) - var dateLookupKey = date.toISOString().substring(0, 10); + 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) { @@ -78,7 +78,7 @@ for (var k in data) { } // Set the the title and the end date from either the regular event or the recurrence override. - var recurrenceTitle = curEvent.summary; + 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, @@ -88,10 +88,10 @@ for (var k in data) { } 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(`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 a71e210..27d6ab4 100755 --- a/ical.js +++ b/ical.js @@ -1,16 +1,16 @@ -var UUID = require('uuid/v4'); -var moment = require('moment-timezone'); -var rrule = require('rrule').RRule; +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) * * - * **************/ + * ************* */ // Unescape Text re RFC 4.3.11 -var text = function(t) { +const text = function(t) { t = t || ''; return t .replace(/\\,/g, ',') @@ -19,11 +19,11 @@ var text = function(t) { .replace(/\\\\/g, '\\'); }; -var parseParams = function(p) { - var out = {}; - for (var i = 0; i < p.length; i++) { +const parseParams = function(p) { + const out = {}; + for (let i = 0; i < p.length; i++) { if (p[i].indexOf('=') > -1) { - var segs = p[i].split('='); + const segs = p[i].split('='); out[segs[0]] = parseValue(segs.slice(1).join('=')); } @@ -34,19 +34,19 @@ var parseParams = function(p) { return out; }; -var parseValue = function(val) { +const parseValue = function(val) { if (val === 'TRUE') return true; if (val === 'FALSE') return false; - var number = Number(val); + const number = Number(val); if (!isNaN(number)) return number; return val; }; -var storeValParam = function(name) { +const storeValParam = function(name) { return function(val, curr) { - var current = curr[name]; + const current = curr[name]; if (Array.isArray(current)) { current.push(val); @@ -63,9 +63,9 @@ var storeValParam = function(name) { }; }; -var storeParam = function(name) { +const storeParam = function(name) { return function(val, params, curr) { - var data; + let data; if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) { data = { params: parseParams(params), val: text(val) }; } else data = text(val); @@ -74,8 +74,8 @@ var storeParam = function(name) { }; }; -var addTZ = function(dt, params) { - var p = parseParams(params); +const addTZ = function(dt, params) { + const p = parseParams(params); if (params && p && dt) { dt.tz = p.TZID; @@ -84,10 +84,10 @@ var addTZ = function(dt, params) { return dt; }; -var typeParam = function(name, typeName) { +const typeParam = function(name, typeName) { // typename is not used in this function? return function(val, params, curr) { - var ret = 'date-time'; + let ret = 'date-time'; if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { ret = 'date'; } @@ -96,14 +96,14 @@ var typeParam = function(name, typeName) { }; }; -var dateParam = function(name) { +const dateParam = function(name) { return function(val, params, curr) { - var newDate = text(val); + let newDate = text(val); if (params && params.indexOf('VALUE=DATE') > -1 && params.indexOf('VALUE=DATE-TIME') == -1) { // Just Date - var comps = /^(\d{4})(\d{2})(\d{2}).*$/.exec(val); + 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]); @@ -116,8 +116,7 @@ var dateParam = function(name) { } // typical RFC date-time format - // WARNING: comps has already been defined! - var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val); + 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 @@ -133,13 +132,13 @@ var dateParam = function(name) { ); // TODO add tz } else if (params && params[0] && params[0].indexOf('TZID=') > -1 && params[0].split('=')[1]) { - var tz = params[0].split('=')[1]; + const tz = params[0].split('=')[1]; // lookup tz - var found = moment.tz.names().filter(function(zone) { + const found = moment.tz.names().filter(function(zone) { return zone === tz; })[0]; if (found) { - var zoneDate = moment.tz(val, 'YYYYMMDDTHHmmss', tz); + const zoneDate = moment.tz(val, 'YYYYMMDDTHHmmss', tz); newDate = zoneDate.toDate(); } else { // fallback if tz not found @@ -171,17 +170,17 @@ var dateParam = function(name) { }; }; -var geoParam = function(name) { +const geoParam = function(name) { return function(val, params, curr) { storeParam(val, params, curr); - var parts = val.split(';'); + const parts = val.split(';'); curr[name] = { lat: Number(parts[0]), lon: Number(parts[1]) }; return curr; }; }; -var categoriesParam = function(name) { - var separatorPattern = /\s*,\s*/g; +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) : []; @@ -206,13 +205,13 @@ var categoriesParam = function(name) { // 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. -var exdateParam = function(name) { +const exdateParam = function(name) { return function(val, params, curr) { - var separatorPattern = /\s*,\s*/g; + const separatorPattern = /\s*,\s*/g; curr[name] = curr[name] || []; - var dates = val ? val.split(separatorPattern) : []; + const dates = val ? val.split(separatorPattern) : []; dates.forEach(function(entry) { - var exdate = new Array(); + const exdate = new Array(); dateParam(name)(entry, params, exdate); if (exdate[name]) { @@ -229,12 +228,12 @@ var exdateParam = function(name) { // 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. -var recurrenceParam = function(name) { +const recurrenceParam = function(name) { return dateParam(name); }; -var addFBType = function(fb, params) { - var p = parseParams(params); +const addFBType = function(fb, params) { + const p = parseParams(params); if (params && p) { fb.type = p.FBTYPE || 'BUSY'; @@ -243,15 +242,15 @@ var addFBType = function(fb, params) { return fb; }; -var freebusyParam = function(name) { +const freebusyParam = function(name) { return function(val, params, curr) { - var fb = addFBType({}, params); + const fb = addFBType({}, params); curr[name] = curr[name] || []; curr[name].push(fb); storeParam(val, params, fb); - var parts = val.split('/'); + const parts = val.split('/'); ['start', 'end'].forEach(function(name, index) { dateParam(name)(parts[index], params, fb); @@ -266,16 +265,16 @@ module.exports = { 'BEGIN': function(component, params, curr, stack) { stack.push(curr); - return { type: component, params: params }; + return { type: component, params }; }, 'END': function(val, params, curr, stack) { // original end function - var originalEnd = function(component, params, curr, stack) { + 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 - var key; - var obj; + let key; + let obj; for (key in curr) { if (!{}.hasOwnProperty.call(curr, key)) continue; @@ -288,7 +287,7 @@ module.exports = { return curr; } - var par = stack.pop(); + const par = stack.pop(); if (curr.uid) { // If this is the first time we run into this UID, just save it. @@ -305,7 +304,7 @@ module.exports = { // 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; // WARNING key is already defined + let key; // WARNING key is already defined for (key in curr) { par[curr.uid][key] = curr[key]; } @@ -329,8 +328,8 @@ module.exports = { // 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; // WARNING key is already defined + const recurrenceObj = new Object(); + let key; // WARNING key is already defined for (key in curr) { recurrenceObj[key] = curr[key]; } @@ -368,10 +367,10 @@ module.exports = { // due to the subtypes. if (val === 'VEVENT' || val === 'VTODO' || val === 'VJOURNAL') { if (curr.rrule) { - var rule = curr.rrule.replace('RRULE:', ''); + let 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); + const comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start); if (comps) { curr.start = new Date(comps[1], comps[2] - 1, comps[3]); } @@ -379,7 +378,7 @@ module.exports = { if (typeof curr.start.toISOString === 'function') { try { - rule += ';DTSTART=' + curr.start.toISOString().replace(/[-:]/g, ''); + 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); @@ -421,8 +420,8 @@ module.exports = { }, }, - 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); @@ -436,8 +435,8 @@ module.exports = { return storeParam(name.toLowerCase())(val, params, ctx); }, - parseLines: function(lines, limit, ctx, stack, lastIndex, cb) { - var self = this; + parseLines(lines, limit, ctx, stack, lastIndex, cb) { + const self = this; if (!cb && typeof ctx === 'function') { cb = ctx; @@ -446,19 +445,19 @@ module.exports = { ctx = ctx || {}; stack = stack || []; - var limitCounter = 0; + let limitCounter = 0; - var i = lastIndex || 0; - for (var ii = lines.length; i < ii; i++) { - var l = lines[i]; + 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++; } - var exp = /([^":;]+)((?:;(?:[^":;]+)(?:=(?:(?:"[^"]*")|(?:[^":;]+))))*):(.*)/; - var kv = l.match(exp); + const exp = /([^":;]+)((?:;(?:[^":;]+)(?:=(?:(?:"[^"]*")|(?:[^":;]+))))*):(.*)/; + let kv = l.match(exp); if (kv === null) { // Invalid line - must have k&v @@ -466,9 +465,9 @@ module.exports = { } kv = kv.slice(1); - var value = kv[kv.length - 1]; - var name = kv[0]; - var params = kv[1] ? kv[1].split(';').slice(1) : []; + const value = kv[kv.length - 1]; + const name = kv[0]; + const params = kv[1] ? kv[1].split(';').slice(1) : []; ctx = self.handleObject(name, value, params, ctx, stack, l) || {}; if (++limitCounter > limit) { @@ -497,10 +496,10 @@ module.exports = { } }, - parseICS: function(str, cb) { - var self = this; - var lines = str.split(/\r?\n/); - var ctx; + parseICS(str, cb) { + const self = this; + const lines = str.split(/\r?\n/); + let ctx; if (cb) { // asynchronous execution diff --git a/index.js b/index.js index 971fc05..3c2c839 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ -var request = require('request'); -var fs = require('fs'); +const request = require('request'); +const fs = require('fs'); -var ical = require('./ical.js'); +const ical = require('./ical.js'); /** * iCal event object. @@ -63,27 +63,25 @@ var ical = require('./ical.js'); // utility to allow callbacks to be used for promises function promiseCallback(fn, cb) { - var promise = new Promise(fn); + const promise = new Promise(fn); if (!cb) { return promise; - } else { - promise - .then(function(ret) { - cb(null, ret); - }) - .catch(function(err) { - cb(err, null); - }); - return; } + promise + .then(function(ret) { + cb(null, ret); + }) + .catch(function(err) { + cb(err, null); + }); } // sync functions -var sync = {}; +const sync = {}; // async functions -var async = {}; +const async = {}; // auto-detect functions for backwards compatibility. -var autodetect = {}; +const autodetect = {}; /** * Download an iCal file from the web and parse it. @@ -107,7 +105,7 @@ async.fromURL = function(url, opts, cb) { // 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)); + reject(new Error(`${res.statusCode} ${res.statusMessage}`)); return; } ical.parseICS(data, function(err, ics) { @@ -177,7 +175,7 @@ async.parseICS = function(data, cb) { * @returns {iCalData} Parsed iCal data. */ sync.parseFile = function(filename) { - var data = fs.readFileSync(filename, 'utf8'); + const data = fs.readFileSync(filename, 'utf8'); return ical.parseICS(data); }; @@ -203,10 +201,8 @@ sync.parseICS = function(data) { */ autodetect.parseFile = function(filename, cb) { if (!cb) return sync.parseFile(filename); - else { - async.parseFile(filename, cb); - return; - } + + async.parseFile(filename, cb); }; /** @@ -220,10 +216,8 @@ autodetect.parseFile = function(filename, cb) { */ autodetect.parseICS = function(data, cb) { if (!cb) return sync.parseICS(data); - else { - async.parseICS(data, cb); - return; - } + + async.parseICS(data, cb); }; // export api functions @@ -233,9 +227,9 @@ module.exports = { parseFile: autodetect.parseFile, parseICS: autodetect.parseICS, // sync - sync: sync, + sync, // async - async: async, + async, // other backwards compat things objectHandlers: ical.objectHandlers, handleObject: ical.handleObject, diff --git a/package-lock.json b/package-lock.json index 281745b..afc999e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -468,12 +468,6 @@ } } }, - "eslint-plugin-es5": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es5/-/eslint-plugin-es5-1.4.1.tgz", - "integrity": "sha512-kktkmkF2O7pnSZYgrMiYMbt3wCKRIiXePwILv8USDG95YgP0PzhIxSIROLLKmiQQ/Z6LuhDGWTHK04gnbXBvkg==", - "dev": true - }, "eslint-plugin-import": { "version": "2.18.2", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", diff --git a/package.json b/package.json index 0635225..dee215a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "devDependencies": { "eslint": "^6.5.1", "eslint-config-prettier": "^6.3.0", - "eslint-plugin-es5": "^1.4.1", "eslint-plugin-import": "^2.18.2", "eslint-plugin-prettier": "^3.1.1", "prettier": "^1.18.2", diff --git a/test/test.js b/test/test.js index 40659de..b337cff 100755 --- a/test/test.js +++ b/test/test.js @@ -1,14 +1,14 @@ -/**** +/** ** * Tests * * - ***/ + ** */ process.env.TZ = 'America/San_Francisco'; -var ical = require('../index'); -var vows = require('vows'), - assert = require('assert'), - _ = require('underscore'); +const vows = require('vows'); +const assert = require('assert'); +const _ = require('underscore'); +const ical = require('../index'); vows.describe('node-ical') .addBatch({ @@ -18,7 +18,7 @@ vows.describe('node-ical') }, 'we get 9 events': function(topic) { - var events = _.select(_.values(topic), function(x) { + const events = _.select(_.values(topic), function(x) { return x.type === 'VEVENT'; }); assert.equal(events.length, 9); @@ -134,7 +134,7 @@ vows.describe('node-ical') })[0]; }, 'tzid offset correctly applied': function(event) { - var start = new Date('2002-10-28T22:00:00.000Z'); + const start = new Date('2002-10-28T22:00:00.000Z'); assert.equal(event.start.valueOf(), start.valueOf()); }, }, @@ -185,7 +185,7 @@ vows.describe('node-ical') }, 'has a description': function(topic) { - var desired = + 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' + @@ -256,7 +256,7 @@ vows.describe('node-ical') }, 'recurring yearly event (14 july)': { 'topic': function(events) { - var ev = _.values(events)[0]; + 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) { @@ -296,7 +296,7 @@ vows.describe('node-ical') return ical.parseFile('./test10.ics'); }, 'grabbing custom properties': { - topic: function(topic) {}, + topic(topic) {}, }, }, diff --git a/test/testAsync.js b/test/testAsync.js index 0c4dbb8..68f9e34 100644 --- a/test/testAsync.js +++ b/test/testAsync.js @@ -1,28 +1,28 @@ -/**** +/** ** * Tests * * - ***/ + ** */ process.env.TZ = 'America/San_Francisco'; -var ical = require('../index'); -var vows = require('vows'), - assert = require('assert'), - _ = require('underscore'); +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() { - var self = this; + const self = this; ical.parseFile('./test/test1.ics', function(err, ctx) { self.callback(null, ctx); }); }, 'we get 9 events': function(topic) { - var events = _.select(_.values(topic), function(x) { + const events = _.select(_.values(topic), function(x) { return x.type === 'VEVENT'; }); assert.equal(events.length, 9); @@ -81,7 +81,7 @@ vows.describe('node-ical') }, 'with test2.ics (testing ical features)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test2.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -141,14 +141,14 @@ vows.describe('node-ical') })[0]; }, 'tzid offset correctly applied': function(event) { - var start = new Date('2002-10-28T22:00:00.000Z'); + 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() { - var self = this; + const self = this; ical.parseFile('./test/test3.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -175,7 +175,7 @@ vows.describe('node-ical') 'with test4.ics (testing tripit.com)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test4.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -198,7 +198,7 @@ vows.describe('node-ical') }, 'has a description': function(topic) { - var desired = + 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' + @@ -220,7 +220,7 @@ vows.describe('node-ical') 'with test5.ics (testing meetup.com)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test5.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -240,7 +240,7 @@ vows.describe('node-ical') 'with test6.ics (testing assembly.org)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test6.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -271,14 +271,14 @@ vows.describe('node-ical') }, 'with test7.ics (testing dtstart of rrule)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test7.ics', function(err, ctx) { self.callback(null, ctx); }); }, 'recurring yearly event (14 july)': { 'topic': function(events) { - var ev = _.values(events)[0]; + 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) { @@ -288,7 +288,7 @@ vows.describe('node-ical') }, 'with test 8.ics (VTODO completion)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test8.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -305,7 +305,7 @@ vows.describe('node-ical') }, 'with test 9.ics (VEVENT with VALARM)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test9.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -321,19 +321,19 @@ vows.describe('node-ical') }, 'with test 11.ics (VEVENT with custom properties)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test10.ics', function(err, ctx) { self.callback(null, ctx); }); }, 'grabbing custom properties': { - topic: function(topic) {}, + topic(topic) {}, }, }, 'with test10.ics': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test10.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -396,7 +396,7 @@ vows.describe('node-ical') 'with test11.ics (testing zimbra freebusy)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test11.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -444,7 +444,7 @@ vows.describe('node-ical') 'with test12.ics (testing recurrences and exdates)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test12.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -489,7 +489,7 @@ vows.describe('node-ical') 'with test13.ics (testing recurrence-id before rrule)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test13.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -523,7 +523,7 @@ vows.describe('node-ical') 'with test15.ics (testing quoted parameter values)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test15.ics', function(err, ctx) { self.callback(null, ctx); }); @@ -540,7 +540,7 @@ vows.describe('node-ical') 'with test16.ics (testing for non-stringified start/end time)': { 'topic': function() { - var self = this; + const self = this; ical.parseFile('./test/test16.ics', function(err, ctx) { self.callback(null, ctx); }); From 9baa905d62567a3d32c86be048847b4823919388 Mon Sep 17 00:00:00 2001 From: jackmxyz Date: Wed, 9 Oct 2019 16:46:03 -0700 Subject: [PATCH 72/79] Removed warning comments generated by not using let/const --- ical.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ical.js b/ical.js index 27d6ab4..79c796b 100755 --- a/ical.js +++ b/ical.js @@ -304,7 +304,7 @@ module.exports = { // 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; // WARNING key is already defined + let key; for (key in curr) { par[curr.uid][key] = curr[key]; } @@ -329,7 +329,7 @@ module.exports = { // 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; // WARNING key is already defined + let key; for (key in curr) { recurrenceObj[key] = curr[key]; } From 54b28f8dc13b20d5df5fec83fc98b65eb55a27fe Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Mon, 14 Oct 2019 14:51:53 +0200 Subject: [PATCH 73/79] lets travis check nodejs 12 compatibility as well. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2917f99..60d33a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,5 @@ node_js: - "6" - "8" - "10" + - "12" install: npm install From 3ce830dd86104a7ccc8d880b629c5e9dbce0b60c Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Mon, 14 Oct 2019 15:09:18 +0200 Subject: [PATCH 74/79] version bump to 0.10.0 to reflect API changes. --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index afc999e..d6a129b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.9.2", + "version": "0.10.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index dee215a..993c52b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ical", - "version": "0.9.2", + "version": "0.10.0", "main": "index.js", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ From c51e8579cec3201b25029ee6878139f5b90664f4 Mon Sep 17 00:00:00 2001 From: Sander Koenders Date: Thu, 17 Oct 2019 09:30:52 +0200 Subject: [PATCH 75/79] Add dependency on request types --- package-lock.json | 43 +++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 44 insertions(+) diff --git a/package-lock.json b/package-lock.json index d6a129b..4b10149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,49 @@ "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.1.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", diff --git a/package.json b/package.json index 993c52b..6120586 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "uuid": "^3.3.2" }, "devDependencies": { + "@types/request": "^2.48.3", "eslint": "^6.5.1", "eslint-config-prettier": "^6.3.0", "eslint-plugin-import": "^2.18.2", From 3ca3e04ae5ca7c13763b8c816c5c3614030603e3 Mon Sep 17 00:00:00 2001 From: Sander Koenders Date: Thu, 17 Oct 2019 09:30:59 +0200 Subject: [PATCH 76/79] Add types for nodeIcal --- node-ical.d.ts | 121 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 node-ical.d.ts 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'; +} From 67cb340ee6265da77ee0b82d0c90ad9093c6338f Mon Sep 17 00:00:00 2001 From: Sander Koenders Date: Thu, 17 Oct 2019 09:31:28 +0200 Subject: [PATCH 77/79] Add types entry in package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 6120586..52faf07 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "node-ical", "version": "0.10.0", "main": "index.js", + "types": "node-ical.d.ts", "description": "A minimal icalendar/ics parser for nodejs", "keywords": [ "ical", From 821a7661182f585d070edd27a4b401fc10d736e6 Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Sun, 17 May 2020 12:15:07 +0200 Subject: [PATCH 78/79] fix some vulernability dependencies. --- package-lock.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b10149..3366b55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,9 +68,9 @@ "dev": true }, "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", "dev": true }, "acorn-jsx": { @@ -1143,18 +1143,18 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "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.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "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": "0.0.8" + "minimist": "^1.2.5" } }, "moment": { @@ -1331,7 +1331,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, From 8eb4b4bca28b047749de46a033b05d9f8a13f54f Mon Sep 17 00:00:00 2001 From: Jens Maus Date: Sun, 17 May 2020 12:17:18 +0200 Subject: [PATCH 79/79] add snyk badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 57754ae..d046d0b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![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/)