diff --git a/README.md b/README.md index 4371ea7..235d14a 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,19 @@ With [npm](http://npmjs.org): ## Examples ``` js -var iCalEvent = require('icalevent'); +var iCalendar = require('./icalevent'); -var event = new iCalEvent({ - uid: 9873647, - offset: new Date().getTimezoneOffset(), +var calendar = new iCalendar({ method: 'request', + name: 'Personal calendar', + description: 'Personal calendar of Johnny Boy' +}, [{ + uid: 9873647, status: 'confirmed', attendees: [ { name: 'Johnny Boy', - email: 'johnny@numberfive.com' + email: 'johnny@numberfivevent.com' }, { name: 'Homer Simpson', @@ -36,28 +38,49 @@ var event = new iCalEvent({ description: 'Home flu visit.', location: 'Casa', organizer: { - name: 'Nacho Libre', + name: 'Libre, Nacho', email: 'luchador@monastery.org' }, - url: 'http://google.com/search?q=nacho+libre' -}); + url: 'http://googlevent.com/search?q=nacho+libre' +}, { + status: 'confirmed', + attendees: [ + { + name: 'Johnny Boy', + email: 'johnny@numberfivevent.com' + } + ], + start: '2014-07-02T10:00:00-05:00', + end: '2014-07-02T11:30:00-05:00', + timezone: 'US/Central', + summary: 'Meet with Jane, bring cake', + location: 'Somwhere', + organizer: { + name: 'Doe, Jane', + email: 'jane@doevent.com' + } +}]); ``` Or: ``` js -var iCalEvent = require('icalevent'); +var iCalendar = require('./icalevent'); -var event = new iCalEvent(); +var calendar = new iCalendar(); + +calendar.set('method', 'request'); +calendar.set('name', 'Personal calendar'); +calendar.set('description', 'Personal calendar of Johnny Boy'); + +var event = calendar.addEvent(); event.set('uid', 9873647); -event.set('offset', new Date().getTimezoneOffset()); -event.set('method', 'request'); event.set('status', 'confirmed'); event.set('attendees', [ { name: 'Johnny Boy', - email: 'johnny@numberfive.com' + email: 'johnny@numberfivevent.com' }, { name: 'Homer Simpson', @@ -70,14 +93,30 @@ event.set('timezone', 'US/Central'); event.set('summary', 'Priestly Duties.'); event.set('description', 'Home flu visit.'); event.set('location', 'Casa'); -event.set('organizer', { name: 'Nacho Libre', email: 'luchador@monastery.org' }); -event.set('url', 'http://google.com/search?q=nacho+libre'); +event.set('organizer', { name: 'Libre, Nacho', email: 'luchador@monastery.org' }); +event.set('url', 'http://googlevent.com/search?q=nacho+libre'); + +var event2 = calendar.addEvent(); + +event2.set('status', 'confirmed'); +event2.set('attendees', [ + { + name: 'Johnny Boy', + email: 'johnny@numberfivevent.com' + } +]); +event2.set('start', '2014-07-02T10:00:00-05:00'); +event2.set('end', '2014-07-02T11:30:00-05:00'); +event2.set('timezone', 'US/Central'); +event2.set('summary', 'Meet with Jane, bring cake'); +event2.set('location', 'Somwhere'); +event2.set('organizer', {name: 'Doe, Jane', email: 'jane@doevent.com'}); ``` To ics string: ``` js -event.toFile(); +calendar.toFile(); ``` Returns: @@ -86,40 +125,153 @@ Returns: BEGIN:VCALENDAR VERSION:2.0 PRODID:-//iCalEvent.js v0.3//EN +METHOD:REQUEST +NAME:Personal calendar +X-WR-CALNAME:Personal calendar +DESCRIPTION:Personal calendar of Johnny Boy +X-WR-CALDESC:Personal calendar of Johnny Boy BEGIN:VEVENT UID:9873647 -DTSTAMP:20140316T003036 +DTSTAMP:20140729T115736 +STATUS:CONFIRMED +DTSTART;TZID=US/Central:20140701T090000 +DTEND;TZID=US/Central:20140701T093000 +SUMMARY:Priestly Duties +DESCRIPTION:Home flu visit. +ORGANIZER;CN=Libre\, Nacho:mailto:luchador@monastery.org +LOCATION:Casa +URL;VALUE=URI:http://google.com/search?q=nacho+libre +ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Johnny Boy:MAILTO:johnny@numberfive.com +ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Homer Simpson:MAILTO:homer@powerplant.com +END:VEVENT +BEGIN:VEVENT +UID:104a0a1f-2b0b-4fcb-9a36-83d868e67a08 +DTSTAMP:20140729T115736 +STATUS:CONFIRMED +DTSTART;TZID=US/Central:20140702T170000 +DTEND;TZID=US/Central:20140702T183000 +SUMMARY:Meet with Jane\, bring cake +ORGANIZER;CN=Doe\, Jane:mailto:jane@doe.com +LOCATION:Somewhere +ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Johnny Boy:MAILTO:johnny@numberfive.com +END:VEVENT +END:VCALENDAR +``` + +## Calendar-API + +### Properties + +* **method** (String) _The calendar method. For example, publish, request, reply, add, cancel, refresh, counter, and decline-counter_ +* **id** (String) _The identifier of the product, that created this calendar. Defaults to "-//iCalEvent.js v0.3//EN"._ +* **name** (String) _A name, that describes the events in this calendar in short._ +* **description** (String) _A full description of the kind of events in this calendar._ + +### Methods + +#### iCalendar(calendar, events) + +The constructor for a new iCalendar. Initializes the calendar properties with the values provided by the `calendar` hash and adds all events from the `events` array to the calendar. + +#### .addEvent(event) + +Creates a new event in this calendar and initializes it with the values provided by the `event` hash. Returns the newly created iCalEvent object. (see [Event-API](#event-api)) + +``` js +event = calendar.addEvent({ + status: 'confirmed', + attendees: [ + { + name: 'Johnny Boy', + email: 'johnny@numberfivevent.com' + } + ], + start: '2014-07-02T10:00:00-05:00', + end: '2014-07-02T11:30:00-05:00', + timezone: 'US/Central', + summary: 'Meet with Jane, bring cake', + location: 'Somwhere', + organizer: { + name: 'Doe, Jane', + email: 'jane@doevent.com' + } +}); +``` + +#### .set(property, value) +``` js +calendar.set('method', 'publish'); +``` + +#### .get(property) +``` js +calendar.get('method'); +``` + +Returns: + +``` +publish +``` + +#### .toFile() +``` js +calendar.toFile(); +``` + +Returns: + +``` +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//iCalEvent.js v0.3//EN METHOD:REQUEST +NAME:Personal calendar +X-WR-CALNAME:Personal calendar +DESCRIPTION:Personal calendar of Johnny Boy +X-WR-CALDESC:Personal calendar of Johnny Boy +BEGIN:VEVENT +UID:9873647 +DTSTAMP:20140729T115736 STATUS:CONFIRMED -DTSTART;TZID=US/Central:20140701T020000 -DTEND;TZID=US/Central:20140701T023000 -SUMMARY:Priestly Duties. +DTSTART;TZID=US/Central:20140701T090000 +DTEND;TZID=US/Central:20140701T093000 +SUMMARY:Priestly Duties DESCRIPTION:Home flu visit. -ORGANIZER;CN=Nacho Libre:mailto:luchador@monastery.org +ORGANIZER;CN=Libre\, Nacho:mailto:luchador@monastery.org LOCATION:Casa URL;VALUE=URI:http://google.com/search?q=nacho+libre ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Johnny Boy:MAILTO:johnny@numberfive.com ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Homer Simpson:MAILTO:homer@powerplant.com END:VEVENT +BEGIN:VEVENT +UID:104a0a1f-2b0b-4fcb-9a36-83d868e67a08 +DTSTAMP:20140729T115736 +STATUS:CONFIRMED +DTSTART;TZID=US/Central:20140702T170000 +DTEND;TZID=US/Central:20140702T183000 +SUMMARY:Meet with Jane\, bring cake +ORGANIZER;CN=Doe\, Jane:mailto:jane@doe.com +LOCATION:Somewhere +ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Johnny Boy:MAILTO:johnny@numberfive.com +END:VEVENT END:VCALENDAR ``` -## API +## Event-API ### Properties #### Required -* **offset** (Integer) _The Date.getTimezoneOffset() of the timezone the event is set in. Important: the offset has to be set before start and end times_ * **start** (Date Object) _The start date object of the event_ * **end** (Date Object) _The end date object of the event_ #### Optional * **uid** (String or Integer) _The event uid. If you don't set the uid it will be set for you._ -* **method** (String) _The event method. For example, publish, request, reply, add, cancel, refresh, counter, and decline-counter_ * **status** (String) _The event status. For example, cancelled, confirmed, tentative_ -* **timezone** (String) _The event's timezone in [ICS timezone format](https://github.com/shanebo/tzone/blob/master/lib/tzone.js). If you don't set the timezone it will be set for you._ +* **timezone** (String) _The event's timezone in [ICS timezone format](https://github.com/shanebo/tzone/blob/master/lib/tzonevent.js). If you don't set the timezone it will be set for you._ * **location** (String) _The event location of the event. For example, monastery_ * **url** (String) _A url corresponding to the event_ * **summary** (String) _A summary of the event_ @@ -138,7 +290,7 @@ END:VCALENDAR [ { name: 'Johnny Boy', - email: 'johnny@numberfive.com' + email: 'johnny@numberfivevent.com' }, { name: 'Homer Simpson', @@ -151,7 +303,7 @@ END:VCALENDAR #### .set(property, value) ``` js -event.set('url', 'http://google.com/search?q=nacho+libre'); +event.set('url', 'http://googlevent.com/search?q=nacho+libre'); ``` #### .get(property) @@ -162,7 +314,7 @@ event.get('url'); Returns: ``` -http://google.com/search?q=nacho+libre +http://googlevent.com/search?q=nacho+libre ``` #### .toFile() @@ -173,25 +325,20 @@ event.toFile(); Returns: ``` -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//iCalEvent.js v0.3//EN BEGIN:VEVENT UID:9873647 -DTSTAMP:20140316T003036 -METHOD:REQUEST +DTSTAMP:20140729T115736 STATUS:CONFIRMED -DTSTART;TZID=US/Central:20140701T020000 -DTEND;TZID=US/Central:20140701T023000 -SUMMARY:Priestly Duties. +DTSTART;TZID=US/Central:20140701T090000 +DTEND;TZID=US/Central:20140701T093000 +SUMMARY:Priestly Duties DESCRIPTION:Home flu visit. -ORGANIZER;CN=Nacho Libre:mailto:luchador@monastery.org +ORGANIZER;CN=Libre\, Nacho:mailto:luchador@monastery.org LOCATION:Casa URL;VALUE=URI:http://google.com/search?q=nacho+libre ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Johnny Boy:MAILTO:johnny@numberfive.com ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=Homer Simpson:MAILTO:homer@powerplant.com END:VEVENT -END:VCALENDAR ``` diff --git a/example.js b/example.js index e8c21b9..c753010 100644 --- a/example.js +++ b/example.js @@ -3,11 +3,13 @@ var iCalEvent = require('icalevent'); var tzone = require('tzone'); var http = require('http'); - -var event = new iCalEvent({ +var calendar = new iCalendar({ + method: 'request', + name: 'Personal calendar', + description: 'Personal calendar of Johnny Boy' +}, [{ uid: 9873647, offset: new Date().getTimezoneOffset(), - method: 'request', status: 'confirmed', attendees: [ { @@ -26,21 +28,44 @@ var event = new iCalEvent({ description: 'Home flu visit.', location: 'Casa', organizer: { - name: 'Nacho Libre', + name: 'Libre, Nacho', email: 'luchador@monastery.org' }, url: 'http://google.com/search?q=nacho+libre' -}); +}, { + offset: new Date().getTimezoneOffset(), + status: 'confirmed', + attendees: [ + { + name: 'Johnny Boy', + email: 'johnny@numberfive.com' + } + ], + start: '2014-07-02T10:00:00-05:00', + end: '2014-07-02T11:30:00-05:00', + timezone: 'US/Central', + summary: 'Meet with Jane, bring cake', + location: 'Somewhere', + organizer: { + name: 'Doe, Jane', + email: 'jane@doe.com' + } +}]); // or -var e = new iCalEvent(); +var c = new iCalendar(); + +c.set('method', 'request'); +c.set('name', 'Personal calendar'); +c.set('description', 'Personal calendar of Johnny Boy'); + +var e = c.addEvent(); e.set('uid', 9873647); e.set('offset', new Date().getTimezoneOffset()); -e.set('method', 'request'); e.set('status', 'confirmed'); e.set('attendees', [ { @@ -58,17 +83,37 @@ e.set('timezone', 'US/Central'); e.set('summary', 'Priestly Duties.'); e.set('description', 'Home flu visit.'); e.set('location', 'Casa'); -e.set('organizer', { name: 'Nacho Libre', email: 'luchador@monastery.org' }); +e.set('organizer', { name: 'Libre, Nacho', email: 'luchador@monastery.org' }); e.set('url', 'http://google.com/search?q=nacho+libre'); +var e2 = c.addEvent(); + +e2.set('offset', new Date().getTimezoneOffset()); +e2.set('status', 'confirmed'); +e2.set('attendees', [ + { + name: 'Johnny Boy', + email: 'johnny@numberfive.com' + } +]); +e2.set('start', '2014-07-02T10:00:00-05:00'); +e2.set('end', '2014-07-02T11:30:00-05:00'); +e2.set('timezone', 'US/Central'); +e2.set('summary', 'Meet with Jane, bring cake'); +e2.set('location', 'Somewhere'); +e2.set('organizer', {name: 'Doe, Jane', email: 'jane@doe.com'}); + + +console.log('\n'); +console.log(c.toFile()); console.log('\n'); -console.log(e.toFile()); +console.log(calendar.toFile()); http.createServer(function(request, response){ response.writeHead(200, {'Content-Type': 'text/calendar'}); - var file = event.toFile(); + var file = calendar.toFile(); response.end(file); }).listen(9999, '127.0.0.1'); diff --git a/icalevent.js b/icalevent.js index 085ab95..c87e19d 100644 --- a/icalevent.js +++ b/icalevent.js @@ -1,8 +1,73 @@ var tzone = require('tzone'); +var moment = require('moment'); +var escapeText = function(text) { + return text.toString().replace(/([,;\\])/g, "\\$1").replace(/\n/g, "\\n").replace(/\r/g, "\\r"); +} + +var iCalendar = function(calendar, events) { + this.calendar = {} + this.calendar.id = '-//iCalEvent.js v0.3//EN'; + this.events = []; + if(calendar) { + this.create(calendar); + } + if(events) { + for(var i = 0; i < events.length; i++) { + this.addEvent(events[i]); + } + } +} + +iCalendar.prototype = { + create: function(calendar){ + for (var key in calendar) { + if (calendar.hasOwnProperty(key)) this.set(key, calendar[key]); + } + }, + + get: function(key){ + if (this.calendar[key]) return this.calendar[key]; + }, + + set: function(key, value){ + if (this[key]) this[key](value) + else this.calendar[key] = value; + }, + + addEvent: function(event) { + newEvent = new iCalEvent(event); + this.events.push(newEvent); + return newEvent; + }, + + toFile: function() { + var result = ''; + + result += 'BEGIN:VCALENDAR\r\n'; + result += 'VERSION:2.0\r\n'; + result += 'PRODID:' + escapeText(this.calendar.id) + '\r\n'; + if (this.calendar.method) result += 'METHOD:' + escapeText(this.calendar.method.toUpperCase()) + '\r\n'; + if (this.calendar.name) { + result += 'NAME:' + escapeText(this.calendar.name) + '\r\n'; + result += 'X-WR-CALNAME:' + escapeText(this.calendar.name) + '\r\n'; + } + if (this.calendar.description) { + result += 'DESCRIPTION:' + escapeText(this.calendar.description) + '\r\n'; + result += 'X-WR-CALDESC:' + escapeText(this.calendar.description) + '\r\n'; + } + + for(var i = 0; i < this.events.length; i++) { + result += this.events[i].toFile(); + } + + result += 'END:VCALENDAR\r\n'; + + return result; + } +} var iCalEvent = function(event){ - this.id = '-//iCalEvent.js v0.3//EN'; this.event = {}; if (event) this.create(event); if (!this.event.uid) this.event.uid = this.createUID(); @@ -23,22 +88,12 @@ iCalEvent.prototype = { }); }, - format: function(datetime){ - function pad(n){ - return (n < 10 ? '0' : '') + n; + format: function(datetime, onlyDate){ + if(onlyDate) { + return moment(datetime).format('YYYYMMDD'); + } else { + return moment(datetime).format('YYYYMMDD[T]HHmmss'); } - - var d = new Date(datetime); - d.setUTCMinutes(d.getUTCMinutes() - this.event.offset); - - padded = (d.getUTCFullYear() - + pad(d.getUTCMonth() + 1) - + pad(d.getUTCDate()) + 'T' - + pad(d.getUTCHours()) - + pad(d.getUTCMinutes()) - + pad(d.getUTCSeconds())); - - return padded; }, get: function(key){ @@ -52,44 +107,43 @@ iCalEvent.prototype = { start: function(datetime){ if (!this.event.timezone) { - var d = new Date(datetime); - d.setUTCMinutes(d.getUTCMinutes() - this.event.offset); - this.event.timezone = tzone.getLocation(d); + this.event.timezone = tzone.getLocation(moment(datetime).toDate()); } - this.event.start = this.format(datetime); - }, - - end: function(datetime){ - this.event.end = this.format(datetime); + this.event.start = datetime; }, toFile: function(){ var result = ''; - result += 'BEGIN:VCALENDAR\r\n'; - result += 'VERSION:2.0\r\n'; - result += 'PRODID:' + this.id + '\r\n'; result += 'BEGIN:VEVENT\r\n'; - result += 'UID:' + this.event.uid + '\r\n'; + result += 'UID:' + escapeText(this.event.uid) + '\r\n'; result += 'DTSTAMP:' + this.format(new Date()) + '\r\n'; - if (this.event.method) result += 'METHOD:' + this.event.method.toUpperCase() + '\r\n'; - if (this.event.status) result += 'STATUS:' + this.event.status.toUpperCase() + '\r\n'; - if (this.event.start) result += 'DTSTART;TZID=' + this.event.timezone + ':' + this.event.start + '\r\n'; - if (this.event.end) result += 'DTEND;TZID=' + this.event.timezone + ':' + this.event.end + '\r\n'; - if (this.event.summary) result += 'SUMMARY:' + this.event.summary + '\r\n'; - if (this.event.description) result += 'DESCRIPTION:' + this.event.description + '\r\n'; - if (this.event.organizer) result += 'ORGANIZER;CN=' + this.event.organizer.name + ':mailto:' + this.event.organizer.email + '\r\n'; - if (this.event.location) result += 'LOCATION:' + this.event.location + '\r\n'; + if (this.event.status) result += 'STATUS:' + escapeText(this.event.status.toUpperCase()) + '\r\n'; + if (this.event.start) result += 'DTSTART;TZID=' + this.event.timezone + (this.event.allDay ? ';VALUE=DATE' : '') + ':' + this.format(this.event.start, this.event.allDay) + '\r\n'; + if (this.event.end) result += 'DTEND;TZID=' + this.event.timezone + (this.event.allDay ? ';VALUE=DATE' : '') + ':' + this.format(this.event.end, this.event.allDay) + '\r\n'; + if (this.event.summary) result += 'SUMMARY:' + escapeText(this.event.summary) + '\r\n'; + if (this.event.description) result += 'DESCRIPTION:' + escapeText(this.event.description) + '\r\n'; + if (this.event.organizer) { + result += 'ORGANIZER'; + if (this.event.organizer.name) { + result += ';CN=' + escapeText(this.event.organizer.name); + } + result += ':'; + if (this.event.organizer.email) { + result += 'mailto:' + this.event.organizer.email; + } + result += '\r\n'; + } + if (this.event.location) result += 'LOCATION:' + escapeText(this.event.location) + '\r\n'; if (this.event.url) result += 'URL;VALUE=URI:' + this.event.url + '\r\n'; if (this.event.attendees) { this.event.attendees.forEach(function(attendee){ - result += 'ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=' + attendee.name + ':MAILTO:' + attendee.email + '\r\n'; + result += 'ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=' + escapeText(attendee.name) + ':MAILTO:' + attendee.email + '\r\n'; }); } result += 'END:VEVENT\r\n'; - result += 'END:VCALENDAR\r\n'; return result; } @@ -97,4 +151,4 @@ iCalEvent.prototype = { } -module.exports = iCalEvent; \ No newline at end of file +module.exports = iCalendar; \ No newline at end of file diff --git a/package.json b/package.json index d72ee91..e107ca9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "repository": "git://github.com/shanebo/icalevent.git", "author": "Shane Thacker ", "dependencies": { - "tzone": "0.0.2" + "tzone": "0.0.2", + "moment": "2.7.0" }, "engine": ">= 0.4.1", "main": "./icalevent"